PageSourceSearch

https://bureaucracy.es/dashboard

html bureaucracy.es collected 2026-10-01 23:56:43 UTC 332,737 bytes, 6,912 lines download raw bytes

1<!doctype html>
2<html lang="en">
3
4<head>
5  <meta charset="UTF-8" />
6  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7  <title>Dashboard — Bureaucracy.es</title>
8  <meta name="description" content="Your visa process dashboard" />
9  
10  <link rel="canonical" href="https://www.bureaucracy.es/dashboard" />
11  <link rel="shortcut icon" href="https://w-cdn.b-cdn.net/vw_bureaucracy/img/favicon-bureaucracy.png" />
12  <link rel="stylesheet" href="/static/dashboard/css/dashboard.css?v=11" />
13
14  
15  
16  
17<script data-cookieyes="cookieyes-necessary">
18    window.dataLayer = window.dataLayer || [];
19    function gtag() { dataLayer.push(arguments); }
20
21    gtag('consent', 'default', {
22        ad_storage: 'denied',
23        ad_user_data: 'denied',
24        ad_personalization: 'denied',
25        analytics_storage: 'denied',
26        functionality_storage: 'denied',
27        personalization_storage: 'denied',
28        security_storage: 'granted',
29        // CookieYes answers within a few hundred ms. Without this wait the first
30        // events would leave before it has spoken and be counted as denied.
31        wait_for_update: 500
32    });
33
34    // A denied ad click is reported without identifiers rather than not at all,
35    // and the gclid travels in the URL instead of a cookie — so an ad click that
36    // ends in a signup stays attributable either way.
37    gtag('set', 'ads_data_redaction', true);
38    gtag('set', 'url_passthrough', true);
39</script>
vendor: 109 bytes, lines 39-41
39
40<script async data-cookieyes="cookieyes-necessary"
41        src="https://www.googletagmanager.com/gtag/js?id=
41G-RTHN4P0055
vendor: 12 bytes, line 41
41"></script>
42<script data-cookieyes="cookieyes-necessary">
43    gtag('js', new Date());
44    gtag('config', 'G-RTHN4P0055', {
45        // One visit across the marketing site, the vanity domains and the
46        // dashboard the signup form lives on — instead of three, each blaming
47        // the previous one for the traffic.
48        linker: {
49            domains: ["bureaucracy.es", "www.bureaucracy.es", "digital-nomad-visa.com", "www.digital-nomad-visa.com", "digitalnomadvisa.es", "www.digitalnomadvisa.es", "digitalnomadsvisa.es", "www.digitalnomadsvisa.es", "digitalvisaspain.com", "www.digitalvisaspain.com", "visadosdigitales.com", "www.visadosdigitales.com", "nonlucrativevisaspain.es", "www.nonlucrativevisaspain.es", "nlvisa.es", "www.nlvisa.es", "spainstudentvisa.com", "www.spainstudentvisa.com", "studentvisa.es", "www.studentvisa.es", "movetospainvisa.com", "www.movetospainvisa.com", "gospainvisa.com", "www.gospainvisa.com", "gotospainvisa.com", "www.gotospainvisa.com", "helpwithvisa.com", "www.helpwithvisa.com", "visahelpspain.com", "www.visahelpspain.com", "visaservicesspain.com", "www.visaservicesspain.com", "spainvisaagency.com", "www.spainvisaagency.com", "spainvisaexperts.com", "www.spainvisaexperts.com", "spainvisaportal.com", "www.spainvisaportal.com", "bureaucracyonline.com", "www.bureaucracyonline.com", "spainexpatguide.com", "www.spainexpatguide.com", "christvisa.com", "www.christvisa.com", "faithvisa.com", "www.faithvisa.com", "godsvisa.com", "www.godsvisa.com", "hinduvisa.com", "www.hinduvisa.com", "islamvisa.com", "www.islamvisa.com", "lawyer-spain.com", "www.lawyer-spain.com", "spainvisa.us", "www.spainvisa.us", "non-lucrative-visa.com", "www.non-lucrative-visa.com", "bureaucracy.us", "www.bureaucracy.us", "pinoyspain.com", "www.pinoyspain.com"],
50            accept_incoming: true
51        }
52    });
53    // Google Ads conversion tag. Rides the same gtag.js load and the same
54    // consent state as GA4 above; login_main.html carries its own copy for
55    // pages that do not include this partial.
56    gtag('config', 'AW-16706649976');
57</script>
57
58  
59
60  
61  
62  <style>
63    /* Hold the scrollbar's space open whether or not there is one to show.
64       Every section except Help is taller than the viewport, so switching to
65       Help was the one case where the page stopped scrolling — the scrollbar
66       went away, the viewport got that much wider, and every centred thing on
67       the page slid right by the width of it. Reserving the gutter means the
68       content box is the same width in both states and nothing moves.
69
70       No effect where scrollbars are drawn as overlays (macOS, iOS, phones),
71       which is why this only ever showed up for some people. */
72    html {
73      scrollbar-gutter: stable;
74    }
75
76    /* Override: No sidebar, full-width layout */
77    .dn-main {
78      margin-left: 0;
79    }
80
81    /* ============================================================
82       Section navigation — a sticky pill bar under the header groups
83       the page's cards into sections and shows one at a time. The
84       cards themselves are untouched; hiding is a class toggle, so it
85       composes with the per-card Hide (eye) controls further down.
86       ============================================================ */
87    
88    .dnv-nav {
89      position: sticky;
90      top: 64px;               /* JS keeps this in sync with the real header height */
91      z-index: 85;             /* under the header (90), over card animations */
92      background: var(--bl-bg);
93      padding: 10px 14px 4px;
94    }
95
96    .dnv-nav-in {
97      max-width: 1200px;
98      margin: 0 auto;
99      display: flex;
100      gap: 4px;
101      padding: 6px;
102      background: var(--bl-bg-card);
103      border: 1px solid var(--bl-border);
104      border-radius: 12px;
105      box-shadow: var(--bl-shadow-md);
106      overflow-x: auto;
107      -webkit-overflow-scrolling: touch;
108      scrollbar-width: none;
109    }
110
111    .dnv-nav-in::-webkit-scrollbar { display: none; }
112
113    .dnv-pill {
114      flex: 1 0 auto;
115      display: inline-flex;
116      align-items: center;
117      justify-content: center;
118      gap: 7px;
119      /* Each pill carries its own surface. Unselected, these were bare text on
120         a white bar — indistinguishable from a caption, with only the already
121         selected one looking like a control at all, so there was nothing to
122         suggest the rest could be tapped. Giving every pill an edge makes the
123         bar a segmented control, a shape people already read as tappable. */
124      background: var(--bl-bg);
125      border: 1px solid var(--bl-border);
126      cursor: pointer;
127      border-radius: 9px;
128      padding: 10px 15px;
129      font-size: 13px;
130      font-weight: 600;
131      font-family: inherit;
132      /* Full-strength text, not the secondary grey. Grey read as a disabled
133         label, and on the pill's own surface it measured 3.6:1 — under the 4.5
134         WCAG asks of 13px text. This is 15.7:1, and a label at full contrast
135         looks like something you act on. */
136      color: var(--bl-text-primary);
137      white-space: nowrap;
138      transition: background .15s ease, color .15s ease,
139                  border-color .15s ease, transform .08s ease;
140      /* Real-tap hygiene: no 300ms double-tap delay, no text selection on a
141         long-press, no grey tap flash. */
142      touch-action: manipulation;
143      user-select: none;
144      -webkit-user-select: none;
145      -webkit-tap-highlight-color: transparent;
146    }
147
148    .dnv-pill:hover {
149      color: var(--bl-text-primary);
150      background: var(--bl-bg-card);
151      border-color: var(--bl-text-muted);
152    }
153    .dnv-pill.on {
154      background: var(--bl-primary);
155      border-color: var(--bl-primary);
156      color: #fff;
157    }
158    /* Presses back under the finger. On a phone there is no hover, so this is
159       the only feedback confirming the tap landed on a control. */
160    .dnv-pill:active { transform: scale(.97); }
161    /* Keyboard users get the same outline the rest of the dashboard uses. */
162    .dnv-pill:focus-visible {
163      outline: 2px solid var(--bl-primary);
164      outline-offset: 2px;
165    }
166    .dnv-pill.gone { display: none; }   /* section empty for this client */
167
168    .dnv-pill .n {
169      font-size: 10.5px;
170      font-weight: 700;
171      /* White, not --bl-bg: the pill itself is --bl-bg now, and a badge the
172         same colour as the thing it sits on disappears. */
173      background: var(--bl-bg-card);
174      border: 1px solid var(--bl-border);
175      color: var(--bl-text-secondary);
176      border-radius: 999px;
177      padding: 2px 8px;
178    }
179
180    .dnv-pill.on .n { background: rgba(255, 255, 255, .18); border-color: transparent; color: #fff; }
181
182    /* Cards hidden by the section nav. !important beats inline card styles. */
183    .dnv-hide { display: none !important; }
184
185    @media (max-width: 700px) {
186      /* Six short labels don't fit one row on a phone, and a horizontal scroll
187         clips the trailing pills against the card's rounded corner — looking
188         broken rather than scrollable. Wrap them instead: every section stays
189         visible.
190
191         Sized off a third of the row rather than off the label, which is what
192         made the wrapped bar look accidental: pills took their own text width,
193         so the first row ran Overview/Tasks/Documents/Help at four different
194         sizes and the two left over stretched to half the bar each. A fixed
195         basis puts three equal pills on a row, and flex-grow still shares the
196         last row out evenly for clients who have a section hidden. */
197      .dnv-nav { padding: 8px 10px 4px; }
198      .dnv-nav-in { flex-wrap: wrap; overflow-x: visible; gap: 6px; }
199      .dnv-pill {
200        flex: 1 1 calc(33.333% - 4px);
201        min-width: 0;
202        /* The pills were 36px, under the size a thumb reliably hits, and this
203           bar is the only way between sections on a phone. Pinned rather than
204           padded to a height, because the count badge on Tasks made its row
205           3px taller than the other one. */
206        min-height: 44px;
207        padding: 6px;
208        font-size: 12.5px;
209        gap: 5px;
210      }
211      .dnv-pill .n { font-size: 10px; padding: 2px 6px; }
212
213      /* Four sections (or two) divide into halves, not thirds — 3+1 leaves a
214         lone full-width pill that reads as a mistake. Set from JS, which is the
215         only place that knows how many sections this client actually has. */
216      .dnv-nav-in.dnv-even .dnv-pill { flex-basis: calc(50% - 3px); }
217    }
218
219    /* The Carlos / referral / current-task grid: one column when the open
220       section shows only one of its children. */
221    .dn-grid-2.dnv-single { grid-template-columns: 1fr !important; }
222
223    /* Help shows two of this grid's children — Carlos and the case-manager
224       card — side by side. The base .dn-grid-2 collapses to one column below
225       1200px; keep this row two-up down to a phone width instead (the higher
226       specificity here beats that rule). When only one card is visible in the
227       grid (Overview / Socials) dnv-single still forces a single column. */
228    /* minmax(0, 1fr), not 1fr: a bare 1fr is minmax(auto, 1fr), and `auto` as a
229       track minimum is the item's min-content width. The conversation preview
230       in the case-manager card is white-space:nowrap, so a client whose latest
231       message starts with a long sentence gave that card a min-content width of
232       most of the row and squeezed Carlos into a sliver — while a client with a
233       short message got a clean 50/50. min-width:0 further down does not help:
234       it lets the snippet shrink as a flex item, but the grid item itself still
235       reports the wide min-content. Zeroing the track minimum pins both columns
236       to half regardless of what is in them. */
237    .dn-help-grid.dn-grid-2 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
238    @media (max-width: 760px) { .dn-help-grid.dn-grid-2 { grid-template-columns: minmax(0, 1fr); } }
239
240    /* Entry animations are for page load; without this they replay every
241       time a section brings a card back. Set on body after first paint.
242       The delay classes start cards at opacity 0 and the animation's
243       `forwards` fill supplies the end state, so removing the animation
244       must pin that end state too or the cards stay invisible. */
245    .dnv-ready .dn-animate { animation: none !important; opacity: 1 !important; transform: none !important; }
246
247    /* Height lives here rather than inline so the phone rule below can win
248       without reaching for !important. */
249    .dn-header-logo { height: 32px; width: auto; }
250
251    @media (max-width: 600px) {
252      /* The round mark <picture> swaps in is square, so both axes are pinned:
253         the box is the right shape before the image has loaded, and the header
254         does not reflow around it. */
255      .dn-header-logo { height: 34px; width: 34px; }
256    }
257
258    /* The notifications panel hangs off the bell, and the bell sits inboard of
259       the avatar — fine on a desktop, but on a phone a 340px panel anchored to
260       a button that is already near the edge runs off the left of the screen.
261       Anchor it to the screen instead. Fixed rather than absolute so it also
262       escapes the sticky header's stacking context; `top` comes from JS,
263       because the header is a different height at each breakpoint. */
264    @media (max-width: 600px) {
265      #notifMenu {
266        position: fixed;
267        left: auto;
268        right: 8px;
269        width: min(340px, calc(100vw - 16px));
270        min-width: 0;
271        max-width: none;
272      }
273    }
274
275
276    /* Required-documents list: a single full-width column wastes most of the
277       row on wide screens (long empty gap between name and buttons) and makes
278       a 16-item list very tall. Lay the items out two-up on wider screens.
279       align-items:start keeps each item its natural height — an item expanded
280       with uploaded files doesn't stretch its shorter neighbour. */
281    .dn-doc-req-grid { display: grid; grid-template-columns: 1fr; gap: 8px; align-items: start; }
282
283    /* ── Locked documents (before the first payment) ── */
284    .dn-doc-locked-row {
285      display: flex; align-items: center; gap: 10px;
286      border: 1px solid var(--bl-border); border-radius: 10px;
287      background: var(--bl-bg); padding: 14px 12px;
288      filter: blur(3px); opacity: .65; user-select: none; pointer-events: none;
289    }
290    .dn-doc-locked-num {
291      width: 28px; height: 28px; border-radius: 50%; flex-shrink: 0;
292      background: rgba(148, 163, 184, .35);
293    }
294    .dn-doc-locked-lines { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
295    .dn-doc-locked-bar { height: 9px; border-radius: 5px; background: rgba(148, 163, 184, .40); }
296    .dn-doc-locked-bar.short { height: 7px; background: rgba(148, 163, 184, .26); }
297    .dn-doc-locked-btn {
298      width: 62px; height: 22px; border-radius: 6px; flex-shrink: 0;
299      background: rgba(148, 163, 184, .30);
300    }
301
302    .dn-doc-unlock {
303      display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
304      border: 1px solid var(--bl-border); border-radius: 12px;
305      background: var(--bl-bg-card, #fff); padding: 16px;
306    }
307    .dn-doc-unlock-icon {
308      width: 38px; height: 38px; border-radius: 10px; flex-shrink: 0;
309      display: flex; align-items: center; justify-content: center;
310      background: rgba(59, 130, 246, .10); color: var(--bl-primary);
311    }
312    .dn-doc-unlock-title {
313      font-size: 13.5px; font-weight: 700; color: var(--bl-text-primary); margin-bottom: 2px;
314    }
315    .dn-doc-unlock-sub { font-size: 12px; color: var(--bl-text-muted); line-height: 1.5; }
316    .dn-doc-unlock-btn {
317      display: inline-flex; align-items: center; gap: 7px; flex-shrink: 0;
318      padding: 10px 16px; border: none; border-radius: 9px; cursor: pointer;
319      background: linear-gradient(135deg, #22c55e, #16a34a); color: #fff;
320      font-size: 12.5px; font-weight: 700; font-family: inherit;
321    }
322    .dn-doc-unlock-btn:hover { opacity: .93; }
323
324    .dn-upload-btn.dn-upload-locked { opacity: .55; cursor: not-allowed; }
325    @media (min-width: 760px) { .dn-doc-req-grid { grid-template-columns: 1fr 1fr; } }
326
327    /* "Other" is not one of the requested documents — it is the catch-all, and
328       anything can be dropped on it. Given its own full-width row at the foot
329       of the list so it reads as the end of the list rather than as item 18. */
330    .dn-doc-other {
331      grid-column: 1 / -1; margin-top: 6px;
332      border: 2px dashed var(--bl-border); border-radius: 12px; background: white;
333      padding: 22px 20px; text-align: center; cursor: pointer;
334      transition: border-color .15s, background .15s;
335    }
336    .dn-doc-other:hover { border-color: var(--bl-primary); background: rgba(59,130,246,0.03); }
337    /* While a file is over the zone. Set from the drag handlers, because a
338       dragged file fires no :hover. */
339    .dn-doc-other.is--over {
340      border-color: var(--bl-primary); background: rgba(59,130,246,0.08);
341    }
342    .dn-doc-other-icon {
343      width: 42px; height: 42px; border-radius: 50%; margin: 0 auto 10px;
344      display: flex; align-items: center; justify-content: center;
345      background: rgba(59,130,246,0.1); color: var(--bl-primary);
346    }
347    .dn-doc-other-title { font-size: 14px; font-weight: 700; color: var(--bl-text-primary); }
348    .dn-doc-other-sub { font-size: 12px; color: var(--bl-text-muted); margin-top: 3px; }
349    .dn-doc-other-btn {
350      display: inline-flex; align-items: center; gap: 6px; margin-top: 12px;
351      padding: 9px 18px; border-radius: 8px; border: none; cursor: pointer;
352      background: var(--bl-primary); color: white; font-size: 13px; font-weight: 600;
353    }
354    /* Dropping is a desktop gesture; on a phone the button is the whole story. */
355    @media (max-width: 760px) { .dn-doc-other-drag { display: none; } }
356
357    /* My Uploads / From Case Manager rows: filename + meta on the left, then
358       tag, status and delete. On a phone all of that on one line crushes the
359       name column so the "size · date" wraps character-by-character. Group the
360       trailing controls so they drop to their own line under the name. */
361    .dn-file-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; flex-wrap: wrap; }
362    @media (max-width: 620px) {
363      .dn-file-row { flex-wrap: wrap; row-gap: 8px; }
364      .dn-file-row .dn-doc-meta { white-space: nowrap; }
365      .dn-file-actions { flex-basis: 100%; margin-left: 46px; }  /* align under the name, past the 36px icon + gap */
366    }
367
368    /* --- Referral rules modal: numbered steps, then the small print.
369           The page resets list markers globally, so the fine print brings its
370           own bullets back. --- */
371    .dn-rules-list {
372      display: flex;
373      flex-direction: column;
374      gap: 16px;
375      margin-bottom: 22px;
376    }
377
378    .dn-rules-item {
379      display: flex;
380      gap: 12px;
381      align-items: flex-start;
382      font-size: 13px;
383      line-height: 1.6;
384      color: var(--bl-text-secondary);
385    }
386
387    .dn-rules-num {
388      min-width: 24px;
389      height: 24px;
390      border-radius: 50%;
391      background: rgba(34, 197, 94, 0.12);
392      color: #16a34a;
393      font-size: 11.5px;
394      font-weight: 700;
395      display: flex;
396      align-items: center;
397      justify-content: center;
398      flex-shrink: 0;
399    }
400
401    .dn-rules-title {
402      font-size: 13px;
403      font-weight: 700;
404      color: var(--bl-text-primary);
405      margin-bottom: 2px;
406    }
407
408    .dn-rules-fine {
409      border-top: 1px solid var(--bl-border);
410      padding-top: 16px;
411    }
412
413    .dn-rules-fine-head {
414      font-size: 11px;
415      font-weight: 600;
416      text-transform: uppercase;
417      letter-spacing: 0.5px;
418      color: var(--bl-text-muted);
419      margin-bottom: 10px;
420    }
421
422    .dn-rules-fine ul {
423      margin: 0;
424      padding-left: 18px;
425      list-style: disc;
426      list-style-position: outside;
427    }
428
429    .dn-rules-fine li {
430      font-size: 12.5px;
431      line-height: 1.6;
432      color: var(--bl-text-secondary);
433      margin-bottom: 8px;
434    }
435
436    .dn-rules-fine li:last-child { margin-bottom: 0; }
437    .dn-rules-fine a { color: var(--bl-primary); }
438
439    /* --- Social media: the two feeds side by side, stacked on mobile --- */
440    .dn-social-grid {
441      display: grid;
442      grid-template-columns: 1fr;
443      gap: 16px;
444    }
445
446    @media (min-width: 760px) {
447      .dn-social-grid { grid-template-columns: 1fr 1fr; }
448    }
449
450    .dn-social {
451      border: 1px solid var(--bl-border);
452      border-radius: 12px;
453      padding: 14px;
454      background: var(--bl-bg);
455      min-width: 0;
456      display: flex;
457      flex-direction: column;
458    }
459
460    /* Side by side, the two feeds are given one shared height rather than each
461       taking its own: the Instagram embed runs out of posts well before the
462       YouTube gallery ends, which left the left half half-empty. Both panels
463       are the same box now and whichever has more in it scrolls inside. */
464    @media (min-width: 760px) {
465      .dn-social { height: 700px; }
466    }
467
468    .dn-social-head {
469      display: flex;
470      align-items: center;
471      gap: 8px;
472      font-size: 13px;
473      font-weight: 700;
474      color: var(--bl-text-primary);
475      margin-bottom: 12px;
476    }
477
478    .dn-social-head svg { flex-shrink: 0; }
479
480    .dn-social-link {
481      margin-left: auto;
482      font-size: 11.5px;
483      font-weight: 600;
484      color: var(--bl-primary);
485      text-decoration: none;
486      white-space: nowrap;
487    }
488
489    /* The embeds size themselves; this stops a wide one from pushing the card
490       out of the grid, and lets each fill its panel's shared height. */
491    .dn-social > div[class*="elfsight-app-"] {
492      max-width: 100%;
493      flex: 1;
494      min-height: 0;
495      overflow-y: auto;
496      overflow-x: hidden;
497    }
498
499    /* --- Referral program: ten steps, the link, the discount badge --- */
500    .dn-ref-steps {
501      display: flex;
502      flex-wrap: wrap;
503      gap: 6px;
504      margin-bottom: 14px;
505    }
506
507    .dn-ref-step {
508      width: 30px;
509      height: 30px;
510      flex: 0 0 auto;
511      border-radius: 50%;
512      border: 1px solid var(--bl-border);
513      background: var(--bl-bg);
514      color: var(--bl-text-muted);
515      font-size: 11.5px;
516      font-weight: 700;
517      display: flex;
518      align-items: center;
519      justify-content: center;
520      transition: background .15s, border-color .15s, color .15s;
521    }
522
523    .dn-ref-step.is-done {
524      background: #22c55e;
525      border-color: #22c55e;
526      color: #fff;
527    }
528
529    .dn-ref-status {
530      display: flex;
531      flex-wrap: wrap;
532      align-items: center;
533      gap: 8px;
534      margin-bottom: 14px;
535    }
536
537    .dn-ref-next {
538      font-size: 11.5px;
539      color: var(--bl-text-muted);
540      font-weight: 600;
541    }
542
543    .dn-ref-link {
544      display: flex;
545      gap: 8px;
546    }
547
548    .dn-ref-link input {
549      flex: 1;
550      min-width: 0;
551      padding: 8px 12px;
552      border-radius: 8px;
553      border: 1px solid var(--bl-border);
554      background: var(--bl-bg);
555      font-size: 12.5px;
556      font-weight: 600;
557      color: var(--bl-text-primary);
558      font-family: inherit;
559      outline: none;
560    }
561
562    .dn-ref-link button {
563      display: inline-flex;
564      align-items: center;
565      gap: 5px;
566      padding: 8px 14px;
567      border-radius: 8px;
568      border: none;
569      background: var(--bl-primary);
570      color: #fff;
571      font-size: 12.5px;
572      font-weight: 600;
573      font-family: inherit;
574      cursor: pointer;
575      white-space: nowrap;
576      transition: opacity .15s;
577    }
578
579    .dn-ref-link button:hover { opacity: .9; }
580    .dn-ref-link button:disabled { opacity: .6; cursor: default; }
581
582    .dn-ref-hint {
583      display: none;
584      margin-top: 8px;
585      font-size: 11.5px;
586      font-weight: 600;
587    }
588
589    .dn-ref-rules-btn {
590      display: inline-flex;
591      align-items: center;
592      justify-content: center;
593      gap: 6px;
594      width: 100%;
595      margin-top: 14px;
596      padding: 8px 14px;
597      border-radius: 8px;
598      border: 1px solid var(--bl-primary);
599      background: rgba(59, 130, 246, 0.06);
600      color: var(--bl-primary);
601      font-size: 12.5px;
602      font-weight: 600;
603      font-family: inherit;
604      cursor: pointer;
605      transition: all 0.15s;
606    }
607
608    .dn-ref-rules-btn:hover {
609      background: var(--bl-primary);
610      color: #fff;
611    }
612
613    .dn-ref-rules-btn svg { flex-shrink: 0; }
614
615    /* --- When a paid instalment was settled, beside its number --- */
616    .dn-pay-head-left {
617      display: flex;
618      align-items: center;
619      gap: 8px;
620      min-width: 0;
621    }
622
623    .dn-pay-date {
624      display: inline-flex;
625      align-items: center;
626      gap: 5px;
627      font-size: 11.5px;
628      font-weight: 500;
629      color: var(--bl-text-muted);
630      line-height: 1.3;
631      min-width: 0;
632    }
633
634    .dn-pay-date svg {
635      flex-shrink: 0;
636      opacity: 0.75;
637    }
638
639    /* --- Services directory: category sections, filter chips, flags --- */
640    .dn-svc-filters {
641      display: flex;
642      flex-wrap: wrap;
643      gap: 8px;
644      margin-bottom: 20px;
645    }
646
647    .dn-svc-chip {
648      display: inline-flex;
649      align-items: center;
650      gap: 6px;
651      padding: 7px 14px;
652      border-radius: 999px;
653      border: 1px solid var(--bl-border);
654      background: var(--bl-bg-card);
655      color: var(--bl-text-secondary);
656      font-size: 12px;
657      font-weight: 600;
658      font-family: inherit;
659      cursor: pointer;
660      transition: all 0.15s;
661    }
662
663    .dn-svc-chip:hover {
664      border-color: var(--bl-primary);
665      color: var(--bl-primary);
666    }
667
668    .dn-svc-chip.is-active {
669      background: var(--bl-primary);
670      border-color: var(--bl-primary);
671      color: #fff;
672    }
673
674    .dn-svc-chip-count {
675      font-size: 11px;
676      opacity: 0.7;
677      font-weight: 500;
678    }
679
680    .dn-svc-section {
681      margin-bottom: 26px;
682    }
683
684    .dn-svc-section:last-of-type {
685      margin-bottom: 6px;
686    }
687
688    .dn-svc-section-head {
689      display: flex;
690      align-items: center;
691      gap: 8px;
692      margin-bottom: 12px;
693      padding-bottom: 8px;
694      border-bottom: 1px solid var(--bl-border);
695    }
696
697    .dn-svc-section-icon {
698      font-size: 15px;
699      line-height: 1;
700    }
701
702    .dn-svc-section-title {
703      margin: 0;
704      font-size: 13px;
705      font-weight: 700;
706      letter-spacing: 0.3px;
707      text-transform: uppercase;
708      color: var(--bl-text-secondary);
709    }
710
711    .dn-svc-section-count {
712      font-size: 11px;
713      font-weight: 600;
714      color: var(--bl-text-muted);
715      background: var(--bl-bg);
716      border-radius: 999px;
717      padding: 2px 8px;
718    }
719
720    /* "Free Consultation" and similar flags sit above the description so they
721       read before the sales copy. */
722    .dn-svc-flag {
723      display: inline-block;
724      align-self: flex-start;
725      width: fit-content;
726      margin-bottom: 8px;
727      padding: 3px 10px;
728      border-radius: 999px;
729      background: rgba(34, 197, 94, 0.12);
730      color: #16a34a;
731      font-size: 11px;
732      font-weight: 700;
733      letter-spacing: 0.2px;
734    }
735
736    /* A category we have not signed a partner for yet. */
737    .dn-svc-card--soon {
738      border-style: dashed;
739      cursor: default;
740      opacity: 0.85;
741    }
742
743    .dn-svc-card--soon:hover {
744      transform: none;
745      box-shadow: none;
746    }
747
748    .dn-svc-soon-flag {
749      display: inline-block;
750      align-self: flex-start;
751      width: fit-content;
752      margin-top: 10px;
753      padding: 3px 10px;
754      border-radius: 999px;
755      background: var(--bl-bg);
756      color: var(--bl-text-muted);
757      font-size: 11px;
758      font-weight: 700;
759      letter-spacing: 0.2px;
760    }
761
762    /* --- Documents card highlight after jumping to it from a task --- */
763    .dn-doc-flash {
764      animation: dnDocFlash 1.8s ease-out;
765    }
766
767    @keyframes dnDocFlash {
768      0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.55); }
769      60% { box-shadow: 0 0 0 6px rgba(59, 130, 246, 0.12); }
770      100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); }
771    }
772
773    /* --- What's included (My Service section) --- */
774    .dn-inc-highlights {
775      display: grid;
776      grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
777      gap: 12px;
778      margin-bottom: 18px;
779    }
780
781    .dn-inc-highlight {
782      border: 1px solid rgba(16, 185, 129, 0.22);
783      background: rgba(16, 185, 129, 0.05);
784      border-radius: 12px;
785      padding: 14px 16px;
786    }
787
788    .dn-inc-highlight-value {
789      font-size: 22px;
790      font-weight: 800;
791      color: #059669;
792      line-height: 1.1;
793      margin-bottom: 6px;
794    }
795
796    .dn-inc-highlight-label {
797      font-size: 12.5px;
798      color: var(--bl-text-secondary);
799      line-height: 1.5;
800    }
801
802    .dn-inc-list {
803      list-style: none;
804      margin: 0;
805      padding: 0;
806      display: flex;
807      flex-direction: column;
808      gap: 12px;
809    }
810
811    .dn-inc-list li {
812      display: flex;
813      gap: 10px;
814      align-items: flex-start;
815      font-size: 13.5px;
816      color: var(--bl-text-secondary);
817      line-height: 1.5;
818    }
819
820    .dn-inc-list li svg { flex-shrink: 0; margin-top: 3px; }
821
822    .dn-inc-foot {
823      font-size: 11.5px;
824      color: var(--bl-text-muted);
825      margin: 18px 0 0;
826      line-height: 1.5;
827    }
828
829    .dn-inc-foot a { color: var(--bl-primary); text-decoration: underline; }
830
831    @media (max-width: 640px) {
832      .dn-inc-highlights { grid-template-columns: 1fr; }
833    }
834
835    /* --- Payment Cards --- */
836    .dn-pay-card {
837      border: 1px solid var(--bl-border);
838      border-radius: 12px;
839      padding: 16px;
840      background: var(--bl-bg);
841      display: flex;
842      flex-direction: column;
843      gap: 10px;
844      transition: border-color 0.2s, box-shadow 0.2s;
845    }
846
847    .dn-pay-card:hover {
848      border-color: rgba(59, 130, 246, 0.2);
849      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
850    }
851
852    .dn-pay-card.done {
853      border-color: rgba(34, 197, 94, 0.25);
854      background: rgba(34, 197, 94, 0.03);
855    }
856
857    .dn-pay-header {
858      display: flex;
859      align-items: center;
860      justify-content: space-between;
861    }
862
863    .dn-pay-num {
864      width: 28px;
865      height: 28px;
866      border-radius: 50%;
867      background: rgba(59, 130, 246, 0.1);
868      color: var(--bl-primary);
869      font-size: 13px;
870      font-weight: 700;
871      display: flex;
872      align-items: center;
873      justify-content: center;
874      flex-shrink: 0;
875    }
876
877    .dn-pay-card.done .dn-pay-num {
878      background: rgba(34, 197, 94, 0.1);
879      color: #22c55e;
880    }
881
882    .dn-pay-amount {
883      font-size: 18px;
884      font-weight: 700;
885      color: var(--bl-text-primary);
886    }
887
888    .dn-pay-desc {
889      font-size: 12px;
890      color: var(--bl-text-secondary);
891      line-height: 1.5;
892    }
893
894    /* The full price of the case, the first thing people look for. */
895    .dn-pay-total {
896      font-size: 34px;
897      font-weight: 800;
898      line-height: 1.1;
899      letter-spacing: -0.5px;
900      color: var(--bl-text-primary);
901      margin: 6px 0 4px;
902    }
903
904    @media (max-width: 575px) {
905      .dn-pay-total { font-size: 28px; }
906    }
907
908    .dn-pay-action {
909      margin-top: auto;
910    }
911
912    .dn-pay-btn {
913      display: inline-flex;
914      align-items: center;
915      gap: 6px;
916      padding: 8px 16px;
917      border-radius: 8px;
918      border: none;
919      background: var(--bl-primary);
920      color: white;
921      font-size: 12px;
922      font-weight: 600;
923      cursor: pointer;
924      transition: opacity 0.15s, transform 0.1s;
925    }
926
927    .dn-pay-btn:hover {
928      opacity: 0.9;
929      transform: translateY(-1px);
930    }
931
932    .dn-pay-btn:active {
933      transform: translateY(0);
934    }
935
936    /* --- Mollie Components --- */
937    .dn-mc-field {
938      position: relative;
939    }
940
941    .dn-mc-field label {
942      display: block;
943      font-size: 11px;
944      font-weight: 600;
945      color: var(--bl-text-secondary);
946      text-transform: uppercase;
947      letter-spacing: 0.5px;
948      margin-bottom: 6px;
949    }
950
951    .dn-mc-input {
952      padding: 10px 12px;
953      border: 1.5px solid var(--bl-border);
954      border-radius: 8px;
955      background: var(--bl-bg);
956      min-height: 42px;
957      transition: border-color 0.2s, box-shadow 0.2s;
958    }
959
960    .dn-mc-field.is-focussed .dn-mc-input {
961      border-color: var(--bl-primary);
962      box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
963    }
964
965    .dn-mc-field.is-invalid .dn-mc-input {
966      border-color: #ef4444;
967    }
968
969    .dn-mc-field.is-valid .dn-mc-input {
970      border-color: #22c55e;
971    }
972
973    .dn-mc-error {
974      color: #ef4444;
975      font-size: 11px;
976      font-weight: 500;
977      margin-top: 4px;
978      min-height: 14px;
979    }
980
981    @media (max-width: 600px) {
982      .dn-mc-field+div {
983        grid-template-columns: 1fr !important;
984      }
985    }
986  </style>
987</head>
988
989<body class="dashboard-new-body">
990
991  
992  
993  <style>@keyframes mollieReturnSpin{to{transform:rotate(360deg)}}</style>
994  <div id="mollieReturnOverlay" style="display:none;position:fixed;inset:0;z-index:20000;background:var(--bl-bg-body,#fff);flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:24px;">
995    <div style="width:44px;height:44px;border:3px solid var(--bl-border,#e5e7eb);border-top-color:var(--bl-primary,#02b7bf);border-radius:50%;animation:mollieReturnSpin 0.7s linear infinite;margin-bottom:18px;"></div>
996    <div style="font-size:17px;font-weight:700;color:var(--bl-text-primary,#0f172a);margin-bottom:6px;">Confirming your payment…</div>
997    <p style="font-size:13.5px;color:var(--bl-text-secondary,#64748b);margin:0;max-width:34ch;">One moment while we update your dashboard. Please don't close this page.</p>
998  </div>
999  
999<script>
1000    (function () {
1001      if (window.location.search.indexOf('mollie_return=1') === -1) return;
1002      var overlay = document.getElementById('mollieReturnOverlay');
1003      overlay.style.display = 'flex';
1004      // replace(), not assign(): a back button that lands on the return URL
1005      // would otherwise start the whole wait again. Stripping the parameter
1006      // also means the reloaded page cannot trigger this a second time.
1007      window.setTimeout(function () {
1008        window.location.replace(window.location.pathname);
1009      }, 3500);
1010    })();
1011  </script>
1011
1012
1013  
1014  
1015
1016  
1017      <!-- ===== Login Modal (non-dismissable, 2FA) ===== -->
1018  <div class="dn-modal-overlay" id="loginModal" style="display:flex;z-index:99999;">
1019    <div class="dn-modal" id="loginModalBox" style="max-width:420px;text-align:center;transition:max-width 0.3s ease;">
1020      <div class="dn-modal-body" style="padding:40px 30px 32px;">
1021
1022        <!-- Step 1: Email -->
1023        <div id="loginStep1">
1024          <div style="margin-bottom:20px;">
1025            <img src="https://w-cdn.b-cdn.net/vw_bureaucracy/img/bureaucracy_logo.png" alt="Bureaucracy.es"
1026              style="height:36px;margin-bottom:16px;">
1027            <div style="font-size:18px;font-weight:700;color:var(--bl-text-primary);margin-bottom:10px;">Welcome to your
1028              Dashboard</div>
1029            <div style="font-size:13px;color:var(--bl-text-secondary);line-height:1.38;">Enter your email address to
1030              receive a login code.</div>
1031          </div>
1032
1033          
1034
1035          <!-- Error message -->
1036          <div id="loginErrorMsg"
1037            style="display:none;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.2);border-radius:10px;padding:10px 14px;margin-bottom:14px;text-align:left;">
1038            <span style="font-size:12px;font-weight:600;color:#ef4444;" id="loginErrorText">Please enter a valid
1039              email.</span>
1040          </div>
1041
1042          <div style="text-align:left;">
1043            <div class="dn-modal-field" style="margin-bottom:16px;">
1044              <label for="loginEmail">Email Address</label>
1045              <input type="email" id="loginEmail" placeholder="[email protected]"
1046                style="width:100%;padding:12px 14px;border-radius:10px;border:1px solid var(--bl-border);background:var(--bl-bg);font-size:14px;color:var(--bl-text-primary);outline:none;transition:border-color 0.2s;font-family:inherit;box-sizing:border-box;">
1047            </div>
1048          </div>
1049          <button type="button" id="btnSendCode" onclick="sendLoginCode()" class="dn-modal-btn-primary"
1050            style="width:100%;padding:12px;font-size:14px;border-radius:10px;display:flex;align-items:center;justify-content:center;gap:8px;">
1051            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
1052              stroke-linecap="round" stroke-linejoin="round">
1053              <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
1054              <polyline points="22,6 12,13 2,6" />
1055            </svg>
1056            Send Login Code
1057          </button>
1058          <div style="margin-top:16px;font-size:11px;color:var(--bl-text-muted);line-height:1.5;">
1059            Don't have an account yet?
1060            <a href="javascript:void(0)" onclick="showSignup()"
1061              style="color:var(--bl-primary);font-weight:600;text-decoration:none;">Create a free account</a>
1062          </div>
1063          <a href="/"
1064            style="display:block;margin-top:12px;text-align:center;padding:10px 14px;border-radius:10px;border:1px solid var(--bl-border);background:var(--bl-bg);font-size:13px;font-weight:600;color:var(--bl-text-secondary);text-decoration:none;transition:border-color 0.2s,background 0.2s;">
1065            ← Return to Website
1066          </a>
1067          <div style="margin-top:14px;font-size:11px;color:var(--bl-text-muted);line-height:1.5;">
1068            Are you part of the Bureaucracy team or have a business account? Login here:
1069            <a href="/login-workspace" style="color:#299cdb;font-weight:600;text-decoration:none;">Workspace</a>
1070          </div>
1071        </div>
1072
1073        <!-- Step 2: Verification Code -->
1074        <div id="loginStep2" style="display:none;">
1075          <div style="margin-bottom:20px;">
1076            <img src="https://w-cdn.b-cdn.net/vw_bureaucracy/img/bureaucracy_logo.png" alt="Bureaucracy.es"
1077              style="height:36px;margin-bottom:16px;">
1078            <div style="font-size:18px;font-weight:700;color:var(--bl-text-primary);margin-bottom:10px;">Enter
1079              Verification Code</div>
1080            <div style="font-size:13px;color:var(--bl-text-secondary);line-height:1.38;">We sent a 6-digit code to <b
1081                id="displayLoginEmail"></b></div>
1082          </div>
1083
1084          <!-- Error message -->
1085          <div id="codeErrorMsg"
1086            style="display:none;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.2);border-radius:10px;padding:10px 14px;margin-bottom:14px;text-align:left;">
1087            <span style="font-size:12px;font-weight:600;color:#ef4444;" id="codeErrorText">Invalid code.</span>
1088          </div>
1089
1090          <div style="text-align:left;">
1091            <div class="dn-modal-field" style="margin-bottom:16px;">
1092              <label for="loginCode">Verification Code</label>
1093              <input type="text" id="loginCode" placeholder="000000" maxlength="6" autocomplete="one-time-code"
1094                style="width:100%;padding:14px;border-radius:10px;border:1px solid var(--bl-border);background:var(--bl-bg);font-size:24px;font-weight:700;color:var(--bl-text-primary);outline:none;transition:border-color 0.2s;font-family:inherit;box-sizing:border-box;text-align:center;letter-spacing:10px;">
1095            </div>
1096          </div>
1097          <button type="button" id="btnVerifyCode" onclick="verifyLoginCode()" class="dn-modal-btn-primary"
1098            style="width:100%;padding:12px;font-size:14px;border-radius:10px;display:flex;align-items:center;justify-content:center;gap:8px;">
1099            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
1100              stroke-linecap="round" stroke-linejoin="round">
1101              <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
1102              <polyline points="22 4 12 14.01 9 11.01" />
1103            </svg>
1104            Verify & Log In
1105          </button>
1106          <div style="margin-top:14px;display:flex;justify-content:space-between;align-items:center;">
1107            <a href="javascript:void(0)" onclick="goBackToEmail()"
1108              style="font-size:12px;color:var(--bl-text-secondary);text-decoration:none;font-weight:500;">
1109              ← Back to Email
1110            </a>
1111            <a href="javascript:void(0)" id="resendCodeLink" onclick="resendCode()"
1112              style="font-size:12px;color:var(--bl-primary);text-decoration:none;font-weight:600;">
1113              Resend code
1114            </a>
1115          </div>
1116        </div>
1117
1118        <!-- Step 3: Signup — Service Selection -->
1119        <div id="loginStep3" style="display:none;">
1120          
1121          
1122          
1123          
1124          
1125<style>
1126    .sp-head {
1127        position: relative;
1128        margin-bottom: 16px;
1129        text-align: center;
1130        font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
1131    }
1132
1133    /* Out of the flow, so the heading below stays centred on the panel rather
1134       than on what is left beside the button. */
1135    .sp-close {
1136        position: absolute;
1137        top: 0;
1138        right: 0;
1139        background: none;
1140        border: none;
1141        padding: 4px;
1142        cursor: pointer;
1143        line-height: 0;
1144        color: var(--bl-text-muted, #6D7185);
1145        transition: color 0.18s;
1146    }
1147
1148    .sp-close:hover {
1149        color: var(--bl-text-primary, #181C32);
1150    }
1151
1152    .sp-logo {
1153        height: 36px;
1154        margin: 0 auto 16px;
1155        display: block;
1156    }
1157
1158    .sp-title {
1159        font-size: 18px;
1160        font-weight: 700;
1161        color: var(--bl-text-primary, #181C32);
1162        margin: 0 0 10px;
1163        line-height: 1.3;
1164    }
1165
1166    .sp-sub {
1167        font-size: 13px;
1168        font-weight: 400;
1169        color: var(--bl-text-secondary, #5E6278);
1170        line-height: 1.38;
1171        margin: 0;
1172    }
1173
1174    /* Under the subtitle: nobody should feel forced to pick blind. */
1175    .sp-consult {
1176        display: inline-block;
1177        margin-top: 10px;
1178        padding: 7px 14px;
1179        border: 1px solid rgba(16, 185, 129, 0.35);
1180        border-radius: 999px;
1181        background: rgba(16, 185, 129, 0.07);
1182        font-size: 12.5px;
1183        font-weight: 500;
1184        color: var(--bl-text-primary, #181C32);
1185        line-height: 1.35;
1186    }
1187
1188    .sp-consult strong {
1189        color: #059669;
1190        font-weight: 700;
1191    }
1192
1193    .sp-grid {
1194        display: grid;
1195        grid-template-columns: 1fr 1fr;
1196        gap: 12px;
1197        text-align: left;
1198        margin-bottom: 16px;
1199        font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
1200    }
1201
1202    .sp-card {
1203        border: 1.5px solid var(--bl-border, #E4E6EF);
1204        border-radius: 12px;
1205        padding: 16px 16px 14px;
1206        cursor: pointer;
1207        transition: all 0.2s ease;
1208        background: var(--bl-bg, #F5F8FA);
1209        text-align: left;
1210        text-decoration: none;
1211        display: block;
1212    }
1213
1214    .sp-card:hover {
1215        border-color: var(--bl-primary, #1B84FF);
1216        background: rgba(59, 130, 246, 0.04);
1217        transform: translateY(-1px);
1218        box-shadow: 0 4px 12px rgba(59, 130, 246, 0.08);
1219    }
1220
1221    .sp-card.selected {
1222        border-color: var(--bl-primary, #1B84FF);
1223        background: rgba(59, 130, 246, 0.06);
1224        box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
1225    }
1226
1227    .sp-card-title {
1228        font-size: 14px;
1229        font-weight: 700;
1230        color: var(--bl-text-primary, #181C32);
1231        /* Away from its own description: at 6px the two read as one block. */
1232        margin-bottom: 6px;
1233    }
1234
1235    .sp-card-desc {
1236        font-size: 12.4px;
1237        font-weight: 400;
1238        color: var(--bl-text-secondary, #5E6278);
1239        /* And the description's own lines pulled closer, so the gap that means
1240           "new thought" is bigger than the gap that means "same sentence". */
1241        line-height: 1.32;
1242    }
1243</style>
1244
1245<div class="sp-head">
1246    
1247    <button type="button" class="sp-close" aria-label="Close"
1248        
1249        onclick="goBackToLogin()">
1250        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"
1251             stroke-linecap="round" stroke-linejoin="round">
1252            <line x1="18" y1="6" x2="6" y2="18" />
1253            <line x1="6" y1="6" x2="18" y2="18" />
1254        </svg>
1255    </button>
1256    
1257    
1258    
1259    <img class="sp-logo" src="https://w-cdn.b-cdn.net/vw_bureaucracy/img/bureaucracy_logo.png?width=180"
1260         alt="Bureaucracy.es">
1261    <div class="sp-title">What are you looking for?</div>
1262    <div class="sp-sub">Select the service that best fits your situation.</div>
1263    <div class="sp-consult">Not sure which fits? You can book a <strong>free consultation</strong> once you've created your account.</div>
1264</div>
1265
1266
1267          <div id="signupErrorMsg"
1268            style="display:none;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.2);border-radius:10px;padding:10px 14px;margin-bottom:14px;text-align:left;">
1269            <span style="font-size:12px;font-weight:600;color:#ef4444;" id="signupErrorText">Please select a
1270              service.</span>
1271          </div>
1272          <input type="hidden" id="signupService" value="">
1273          
1274
1275<div class="sp-grid" id="signupServiceGrid">
1276    
1277    
1278    <div class="sp-card" onclick="selectService(this,'Non Lucrative Visa')">
1279        <div class="sp-card-title">Non-Lucrative Visa</div>
1280        <div class="sp-card-desc">Retire or live in Spain on savings or passive income</div>
1281    </div>
1282    
1283    
1284    
1285    <div class="sp-card" onclick="selectService(this,'Digital Nomad Visa')">
1286        <div class="sp-card-title">Digital Nomad Visa</div>
1287        <div class="sp-card-desc">Work remotely for foreign companies from Spain</div>
1288    </div>
1289    
1290    
1291    
1292    <div class="sp-card" onclick="selectService(this,'Student Visa')">
1293        <div class="sp-card-title">Student Visa</div>
1294        <div class="sp-card-desc">Study, research or train at a Spanish institution</div>
1295    </div>
1296    
1297    
1298    
1299    <div class="sp-card" onclick="selectService(this,'Non Lucrative Visa Renewal')">
1300        <div class="sp-card-title">NLV Renewal</div>
1301        <div class="sp-card-desc">Extend your Non-Lucrative Visa for 2 more years</div>
1302    </div>
1303    
1304    
1305    
1306    <div class="sp-card" onclick="selectService(this,'Digital Nomad Visa Renewal')">
1307        <div class="sp-card-title">DNV Renewal</div>
1308        <div class="sp-card-desc">Renew your Digital Nomad Visa and keep working from Spain</div>
1309    </div>
1310    
1311    
1312    
1313    <div class="sp-card" onclick="selectService(this,'Citizenship')">
1314        <div class="sp-card-title">Citizenship</div>
1315        <div class="sp-card-desc">Apply for Spanish nationality after 10 years of residency</div>
1316    </div>
1317    
1318    
1319    
1320    <div class="sp-card" onclick="selectService(this,'Highly Skilled Worker Visa')">
1321        <div class="sp-card-title">Highly Skilled Worker Visa</div>
1322        <div class="sp-card-desc">Work in Spain under a job offer for a specialized, qualified role</div>
1323    </div>
1324    
1325    
1326    
1327    <div class="sp-card" onclick="selectService(this,'Long-Term Residence')">
1328        <div class="sp-card-title">Long-Term Residence</div>
1329        <div class="sp-card-desc">Make your residency in Spain permanent after 5 years</div>
1330    </div>
1331    
1332    
1333</div>
1334
1335          <div style="margin-top:12px;display:flex;gap:10px;">
1336            
1337            <a href="javascript:void(0)" onclick="if(history.length>1){history.back()}else{location.href='/'}"
1338              style="flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:10px 14px;border-radius:10px;border:1px solid var(--bl-border);background:var(--bl-bg);font-size:13px;font-weight:600;color:var(--bl-text-secondary);text-decoration:none;transition:border-color 0.2s,background 0.2s;">
1339              ← Go Back
1340            </a>
1341            <a href="javascript:void(0)" onclick="goBackToLogin()"
1342              style="flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:10px 14px;border-radius:10px;border:1px solid var(--bl-border);background:var(--bl-bg);font-size:13px;font-weight:600;color:var(--bl-text-secondary);text-decoration:none;transition:border-color 0.2s,background 0.2s;">
1343              Go to Login
1344            </a>
1345          </div>
1346        </div>
1347
1348        <!-- Step 4: Signup — Details Form -->
1349        <div id="loginStep4" style="display:none;">
1350          <div style="margin-bottom:20px;">
1351            <img src="https://w-cdn.b-cdn.net/vw_bureaucracy/img/bureaucracy_logo.png" alt="Bureaucracy.es"
1352              style="height:36px;margin-bottom:16px;">
1353            <div style="font-size:15px;color:var(--bl-text-secondary);line-height:1.5;">Selected: <b
1354                id="displaySelectedService" style="color:var(--bl-text-primary);"></b></div>
1355          </div>
1356
1357          <!-- Error message -->
1358          <div id="detailsErrorMsg"
1359            style="display:none;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.2);border-radius:10px;padding:10px 14px;margin-bottom:14px;text-align:left;">
1360            <span style="font-size:12px;font-weight:600;color:#ef4444;" id="detailsErrorText">Please fill in all
1361              required fields.</span>
1362          </div>
1363
1364          <div style="text-align:left;display:flex;flex-direction:column;gap:14px;">
1365            <div class="dn-modal-field">
1366              <label for="signupName">Full Name *</label>
1367              <input type="text" id="signupName" placeholder="John Doe"
1368                style="width:100%;padding:12px 14px;border-radius:10px;border:1px solid var(--bl-border);background:var(--bl-bg);font-size:14px;color:var(--bl-text-primary);outline:none;transition:border-color 0.2s;font-family:inherit;box-sizing:border-box;">
1369            </div>
1370            <div class="dn-modal-field">
1371              <label for="signupEmail">Email Address *</label>
1372              <input type="email" id="signupEmail" placeholder="[email protected]"
1373                style="width:100%;padding:12px 14px;border-radius:10px;border:1px solid var(--bl-border);background:var(--bl-bg);font-size:14px;color:var(--bl-text-primary);outline:none;transition:border-color 0.2s;font-family:inherit;box-sizing:border-box;">
1374            </div>
1375            <div class="dn-modal-field">
1376              <label for="signupPhone">Phone Number <span
1377                  style="font-weight:400;color:var(--bl-text-muted);text-transform:none;letter-spacing:0;">(optional)</span></label>
1378              <input type="tel" id="signupPhone" placeholder="+34 612 345 678"
1379                style="width:100%;padding:12px 14px;border-radius:10px;border:1px solid var(--bl-border);background:var(--bl-bg);font-size:14px;color:var(--bl-text-primary);outline:none;transition:border-color 0.2s;font-family:inherit;box-sizing:border-box;">
1380            </div>
1381
1382            <div class="dn-modal-field">
1383              <label for="signupReferral">
1383How did you find us?</label>
1384              
1385              <select id="signupReferral" onchange="calcSignupPrice()"
1386                style="width:100%;padding:12px 14px;border-radius:10px;border:1px solid var(--bl-border);background:var(--bl-bg);font-size:14px;color:var(--bl-text-primary);outline:none;transition:border-color 0.2s;font-family:inherit;box-sizing:border-box;appearance:auto;">
1387                <option value="no_discount" selected>Other/Don't want to say</option>
1388                <option value="Bing">Bing</option>
1389                <option value="Facebook">Facebook</option>
1390                <option value="Google">Google</option>
1391                <option value="Instagram">Instagram</option>
1392                <option value="Linkedin">Linkedin</option>
1393                <option value="Reddit">Reddit</option>
1394                <option value="Youtube">Youtube</option>
1395              </select>
1396              
1397            </div>
1398
1399            <!-- Applicant counts -->
1400            <div style="display:flex;gap:12px;">
1401              <div class="dn-modal-field" style="flex:1;">
1402                <label for="signupAdults">Adults Applying *</label>
1403                <select id="signupAdults" onchange="calcSignupPrice()"
1404                  style="width:100%;padding:12px 14px;border-radius:10px;border:1px solid var(--bl-border);background:var(--bl-bg);font-size:14px;color:var(--bl-text-primary);outline:none;transition:border-color 0.2s;font-family:inherit;box-sizing:border-box;appearance:auto;">
1405                  <option value="1" selected>1</option>
1406                  <option value="2">2</option>
1407                  <option value="3">3</option>
1408                  <option value="4">4</option>
1409                  <option value="5">5</option>
1410                  <option value="6">6</option>
1411                </select>
1412              </div>
1413              <div class="dn-modal-field" style="flex:1;">
1414                <label for="signupKids">Children Applying</label>
1415                <select id="signupKids" onchange="calcSignupPrice()"
1416                  style="width:100%;padding:12px 14px;border-radius:10px;border:1px solid var(--bl-border);background:var(--bl-bg);font-size:14px;color:var(--bl-text-primary);outline:none;transition:border-color 0.2s;font-family:inherit;box-sizing:border-box;appearance:auto;">
1417                  <option value="0" selected>0</option>
1418                  <option value="1">1</option>
1419                  <option value="2">2</option>
1420                  <option value="3">3</option>
1421                  <option value="4">4</option>
1422                  <option value="5">5</option>
1423                  <option value="6">6</option>
1424                </select>
1425              </div>
1426            </div>
1427
1428            <!-- Pricing breakdown card -->
1429            <div id="signupPricingCard"
1430              style="background:linear-gradient(135deg, rgba(16,185,129,0.06), rgba(59,130,246,0.04));border:1px solid rgba(16,185,129,0.18);border-radius:12px;padding:16px 18px;">
1431              <div id="signupPriceRows" style="display:flex;flex-direction:column;gap:4px;font-size:13px;"></div>
1432              <div
1433                style="border-top:1px solid rgba(0,0,0,0.08);margin:10px 0 6px;padding-top:8px;display:flex;justify-content:space-between;align-items:center;">
1434                <span style="font-weight:700;font-size:14px;color:var(--bl-text-primary);">Total</span>
1435                <span style="font-weight:800;font-size:20px;color:#10b981;" id="signupPriceTotal">€0</span>
1436              </div>
1437              <div id="signupDiscountBadge"
1438                style="display:none;margin-top:8px;padding:6px 10px;background:rgba(16,185,129,0.1);border:1px solid rgba(16,185,129,0.25);border-radius:8px;font-size:12px;font-weight:600;color:#059669;text-align:center;">
1439              </div>
1440
1441              <!-- How the fee is split, from defs/def_payment_plans.py -->
1442              <div style="border-top:1px solid rgba(0,0,0,0.08);margin-top:12px;padding-top:10px;">
1443                <div style="font-size:11px;text-transform:uppercase;letter-spacing:0.6px;font-weight:600;color:var(--bl-text-muted);margin-bottom:8px;">
1444                  Paid in steps — never all at once</div>
1445                <div id="signupPaymentSteps" style="display:flex;flex-direction:column;gap:7px;"></div>
1446              </div>
1447            </div>
1448
1449            <!-- What the fee buys -->
1450            <div style="margin-top:14px;border:1px solid var(--bl-border);border-radius:12px;padding:14px 16px;">
1451              <div style="font-size:11px;text-transform:uppercase;letter-spacing:0.6px;font-weight:600;color:var(--bl-text-muted);margin-bottom:8px;">
1452                What's included</div>
1453              <ul id="signupIncludedList" style="list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px;">
1454                
1455                <li style="display:flex;gap:8px;align-items:flex-start;font-size:12.5px;color:var(--bl-text-secondary);line-height:1.4;">
1456                  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="3"
1457                    stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;margin-top:2px;">
1458                    <polyline points="20 6 9 17 4 12" />
1459                  </svg>
1460                  <span data-included="translations">Official sworn translations of your documents</span>
1461                </li>
1462                
1463                <li style="display:flex;gap:8px;align-items:flex-start;font-size:12.5px;color:var(--bl-text-secondary);line-height:1.4;">
1464                  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="3"
1465                    stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;margin-top:2px;">
1466                    <polyline points="20 6 9 17 4 12" />
1467                  </svg>
1468                  <span>A dedicated case manager who knows your case</span>
1469                </li>
1470                
1471                <li style="display:flex;gap:8px;align-items:flex-start;font-size:12.5px;color:var(--bl-text-secondary);line-height:1.4;">
1472                  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="3"
1473                    stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;margin-top:2px;">
1474                    <polyline points="20 6 9 17 4 12" />
1475                  </svg>
1476                  <span data-included="appointments">Government appointment booking</span>
1477                </li>
1478                
1479                <li style="display:flex;gap:8px;align-items:flex-start;font-size:12.5px;color:var(--bl-text-secondary);line-height:1.4;">
1480                  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="3"
1481                    stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;margin-top:2px;">
1482                    <polyline points="20 6 9 17 4 12" />
1483                  </svg>
1484                  <span>Full document review and a checklist built for you</span>
1485                </li>
1486                
1487                <li style="display:flex;gap:8px;align-items:flex-start;font-size:12.5px;color:var(--bl-text-secondary);line-height:1.4;">
1488                  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="3"
1489                    stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;margin-top:2px;">
1490                    <polyline points="20 6 9 17 4 12" />
1491                  </svg>
1492                  <span>Application submission and follow-up</span>
1493                </li>
1494                
1495                <li style="display:flex;gap:8px;align-items:flex-start;font-size:12.5px;color:var(--bl-text-secondary);line-height:1.4;">
1496                  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="3"
1497                    stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;margin-top:2px;">
1498                    <polyline points="20 6 9 17 4 12" />
1499                  </svg>
1500                  <span>Your own dashboard, with Carlos our AI assistant</span>
1501                </li>
1502                
1503                <li style="display:flex;gap:8px;align-items:flex-start;font-size:12.5px;color:var(--bl-text-secondary);line-height:1.4;">
1504                  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="3"
1505                    stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;margin-top:2px;">
1506                    <polyline points="20 6 9 17 4 12" />
1507                  </svg>
1508                  <span data-included="dhl">DHL document delivery where it applies</span>
1509                </li>
1510                
1511                <li style="display:flex;gap:8px;align-items:flex-start;font-size:12.5px;color:var(--bl-text-secondary);line-height:1.4;">
1512                  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="3"
1513                    stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;margin-top:2px;">
1514                    <polyline points="20 6 9 17 4 12" />
1515                  </svg>
1516                  <span>Appeals at no extra cost — full support against unfounded refusals or administrative silence.</span>
1517                </li>
1518                
1519              </ul>
1520              <a href="javascript:void(0)" id="signupIncludedToggle" onclick="toggleIncluded()"
1521                style="display:none;margin-top:10px;text-align:center;font-size:12px;font-weight:600;color:var(--bl-primary);text-decoration:none;"></a>
1522            </div>
1523
1524            <!-- Consent -->
1525            <div style="margin-top:14px;display:flex;flex-direction:column;gap:10px;">
1526              <label id="signupTermsRow" style="display:flex;gap:9px;align-items:flex-start;font-size:12.5px;color:var(--bl-text-secondary);line-height:1.45;cursor:pointer;">
1527                <input type="checkbox" id="signupTerms" onchange="validateSignupConsent()"
1528                  style="margin-top:2px;flex-shrink:0;accent-color:var(--bl-primary);width:15px;height:15px;">
1529                <span>I accept the <a href="/policy/terms-conditions" target="_blank" rel="noopener"
1530                    style="color:var(--bl-primary);font-weight:600;">Terms &amp; Conditions</a> and the
1531                  <a href="/policy/privacy-cookies" target="_blank" rel="noopener"
1532                    style="color:var(--bl-primary);font-weight:600;">Privacy Policy</a>. *</span>
1533              </label>
1534              <label style="display:flex;gap:9px;align-items:flex-start;font-size:12.5px;color:var(--bl-text-secondary);line-height:1.45;cursor:pointer;">
1535                <input type="checkbox" id="signupNewsletter"
1536                  style="margin-top:2px;flex-shrink:0;accent-color:var(--bl-primary);width:15px;height:15px;">
1537                <span>Keep me posted by email about Spanish immigration news and updates. You can unsubscribe
1538                  at any time.</span>
1539              </label>
1540              <div id="signupConsentError"
1541                style="display:none;font-size:12px;color:#f06548;font-weight:600;">
1542                Please accept the Terms &amp; Conditions to continue.</div>
1543            </div>
1544          </div>
1545          <button type="button" id="btnSignup" onclick="submitSignup()" class="dn-modal-btn-primary"
1546            style="width:100%;padding:12px;font-size:14px;border-radius:10px;display:flex;align-items:center;justify-content:center;gap:8px;margin-top:18px;">
1547            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
1548              stroke-linecap="round" stroke-linejoin="round">
1549              <path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
1550              <circle cx="8.5" cy="7" r="4" />
1551              <line x1="20" y1="8" x2="20" y2="14" />
1552              <line x1="23" y1="11" x2="17" y2="11" />
1553            </svg>
1554            Create Free Account
1555          </button>
1556          <div style="margin-top:14px;">
1557            <a href="javascript:void(0)" onclick="goBackToServiceSelect()"
1558              style="font-size:12px;color:var(--bl-text-secondary);text-decoration:none;font-weight:500;">
1559              ← Change Service
1560            </a>
1561          </div>
1562        </div>
1563
1564      </div>
1565    </div>
1566  </div>
1567  <!-- Toast for login-code feedback (e.g. "Code sent", "Code resent") -->
1568  <div id="loginToast" class="dn-login-toast" style="display:none;">
1569    <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"
1570      stroke-linecap="round" stroke-linejoin="round">
1571      <polyline points="20 6 9 17 4 12" />
1572    </svg>
1573    <span id="loginToastText"></span>
1574  </div>
1575  
1575<script>
1576    let loginEmail = '';
1577    let loginToastTimer = null;
1578    let resendCooldownTimer = null;
1579
1580    function showLoginToast(message) {
1581      const toast = document.getElementById('loginToast');
1582      const text = document.getElementById('loginToastText');
1583      if (!toast || !text) return;
1584      text.textContent = message;
1585      toast.style.display = 'flex';
1586      toast.classList.remove('dn-login-toast--hide');
1587      clearTimeout(loginToastTimer);
1588      loginToastTimer = setTimeout(() => {
1589        toast.classList.add('dn-login-toast--hide');
1590        setTimeout(() => { toast.style.display = 'none'; }, 250);
1591      }, 3500);
1592    }
1593
1594    function startResendCooldown(seconds) {
1595      const link = document.getElementById('resendCodeLink');
1596      if (!link) return;
1597      clearInterval(resendCooldownTimer);
1598      let remaining = seconds;
1599      link.style.pointerEvents = 'none';
1600      link.style.opacity = '0.55';
1601      link.textContent = 'Resend code (' + remaining + 's)';
1602      resendCooldownTimer = setInterval(() => {
1603        remaining--;
1604        if (remaining <= 0) {
1605          clearInterval(resendCooldownTimer);
1606          link.style.pointerEvents = 'auto';
1607          link.style.opacity = '1';
1608          link.textContent = 'Resend code';
1609        } else {
1610          link.textContent = 'Resend code (' + remaining + 's)';
1611        }
1612      }, 1000);
1613    }
1614
1615    function resendCode() {
1616      if (!loginEmail) return;
1617      const errorBox = document.getElementById('codeErrorMsg');
1618      const errorText = document.getElementById('codeErrorText');
1619      const link = document.getElementById('resendCodeLink');
1620
1621      errorBox.style.display = 'none';
1622      link.style.pointerEvents = 'none';
1623      link.style.opacity = '0.55';
1624      link.textContent = 'Sending...';
1625
1626      fetch('/api/dashboard-login/send-code', {
1627        method: 'POST',
1628        headers: { 'Content-Type': 'application/json' },
1629        body: JSON.stringify({ email: loginEmail })
1630      })
1631        .then(r => r.json())
1632        .then(data => {
1633          if (data.success) {
1634            document.getElementById('loginCode').value = '';
1635            document.getElementById('loginCode').focus();
1636            showLoginToast('A new code was sent to ' + loginEmail);
1637            startResendCooldown(30);
1638          } else {
1639            link.style.pointerEvents = 'auto';
1640            link.style.opacity = '1';
1641            link.textContent = 'Resend code';
1642            errorText.textContent = data.error || 'Failed to resend code.';
1643            errorBox.style.display = 'block';
1644          }
1645        })
1646        .catch(() => {
1647          link.style.pointerEvents = 'auto';
1648          link.style.opacity = '1';
1649          link.textContent = 'Resend code';
1650          errorText.textContent = 'A network error occurred.';
1651          errorBox.style.display = 'block';
1652        });
1653    }
1654
1655    // Prevent closing login modal with Escape key
1656    document.addEventListener('keydown', function (e) {
1657      if (e.key === 'Escape' && document.getElementById('loginModal')) {
1658        e.preventDefault();
1659        e.stopPropagation();
1660      }
1661    });
1662
1663    // Enter key triggers send code on Step 1
1664    document.getElementById('loginEmail').addEventListener('keypress', function (e) {
1665      if (e.key === 'Enter') { e.preventDefault(); sendLoginCode(); }
1666    });
1667
1668    // Deep-link straight to the Create Account step (e.g. /dashboard?signup=1)
1669    if (new URLSearchParams(window.location.search).get('signup') === '1') {
1670      showSignup();
1671    }
1672
1673    function sendLoginCode() {
1674      const emailInput = document.getElementById('loginEmail');
1675      const email = emailInput.value.trim().toLowerCase();
1676      const errorBox = document.getElementById('loginErrorMsg');
1677      const errorText = document.getElementById('loginErrorText');
1678      const btn = document.getElementById('btnSendCode');
1679
1680      if (!email || !email.includes('@')) {
1681        errorText.textContent = 'Please enter a valid email address.';
1682        errorBox.style.display = 'block';
1683        return;
1684      }
1685      errorBox.style.display = 'none';
1686
1687      btn.disabled = true;
1688      btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" style="animation:spin 1s linear infinite;" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg> Sending...';
1689
1690      fetch('/api/dashboard-login/send-code', {
1691        method: 'POST',
1692        headers: { 'Content-Type': 'application/json' },
1693        body: JSON.stringify({ email: email })
1694      })
1695        .then(r => r.json())
1696        .then(data => {
1697          btn.disabled = false;
1698          btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg> Send Login Code';
1699          if (data.success) {
1700            loginEmail = email;
1701            document.getElementById('displayLoginEmail').textContent = email;
1702            document.getElementById('loginStep1').style.display = 'none';
1703            document.getElementById('loginStep2').style.display = 'block';
1704            showLoginToast('A login code was sent to ' + email);
1705            startResendCooldown(30);
1706            setTimeout(() => document.getElementById('loginCode').focus(), 100);
1707          } else {
1708            errorText.textContent = data.error || 'Failed to send code.';
1709            errorBox.style.display = 'block';
1710          }
1711        })
1712        .catch(() => {
1713          btn.disabled = false;
1714          btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg> Send Login Code';
1715          errorText.textContent = 'A network error occurred.';
1716          errorBox.style.display = 'block';
1717        });
1718    }
1719
1720    function verifyLoginCode() {
1721      const codeInput = document.getElementById('loginCode');
1722      const code = codeInput.value.trim();
1723      const errorBox = document.getElementById('codeErrorMsg');
1724      const errorText = document.getElementById('codeErrorText');
1725      const btn = document.getElementById('btnVerifyCode');
1726
1727      if (code.length !== 6) {
1728        errorText.textContent = 'Please enter the 6-digit code.';
1729        errorBox.style.display = 'block';
1730        return;
1731      }
1732      errorBox.style.display = 'none';
1733
1734      btn.disabled = true;
1735      btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" style="animation:spin 1s linear infinite;" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg> Verifying...';
1736
1737      fetch('/api/dashboard-login/verify-code', {
1738        method: 'POST',
1739        headers: { 'Content-Type': 'application/json' },
1740        body: JSON.stringify({ email: loginEmail, code: code })
1741      })
1742        .then(r => r.json())
1743        .then(data => {
1744          if (data.success) {
1745            window.location.href = '/dashboard';
1746          } else {
1747            btn.disabled = false;
1748            btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg> Verify & Log In';
1749            errorText.textContent = data.error || 'Invalid code.';
1750            errorBox.style.display = 'block';
1751          }
1752        })
1753        .catch(() => {
1754          btn.disabled = false;
1755          btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg> Verify & Log In';
1756          errorText.textContent = 'A network error occurred.';
1757          errorBox.style.display = 'block';
1758        });
1759    }
1760
1761    function goBackToEmail() {
1762      document.getElementById('loginStep2').style.display = 'none';
1763      document.getElementById('loginStep1').style.display = 'block';
1764      document.getElementById('loginCode').value = '';
1765      document.getElementById('codeErrorMsg').style.display = 'none';
1766      document.getElementById('loginErrorMsg').style.display = 'none';
1767      clearInterval(resendCooldownTimer);
1768      const link = document.getElementById('resendCodeLink');
1769      link.style.pointerEvents = 'auto';
1770      link.style.opacity = '1';
1771      link.textContent = 'Resend code';
1772    }
1773
1774    function showSignup() {
1775      document.getElementById('loginStep1').style.display = 'none';
1776      document.getElementById('loginStep2').style.display = 'none';
1777      document.getElementById('loginStep3').style.display = 'block';
1778      document.getElementById('loginStep4').style.display = 'none';
1779      document.getElementById('signupErrorMsg').style.display = 'none';
1780      document.getElementById('signupService').value = '';
1781      // Widen modal for cards
1782      document.getElementById('loginModalBox').style.maxWidth = '560px';
1783      // Clear any previous selection
1784      document.querySelectorAll('.sp-card').forEach(c => c.classList.remove('selected'));
1785    }
1786
1787    function selectService(cardEl, serviceName) {
1788      document.getElementById('signupService').value = serviceName;
1789      document.querySelectorAll('.sp-card').forEach(c => c.classList.remove('selected'));
1790      cardEl.classList.add('selected');
1791      document.getElementById('signupErrorMsg').style.display = 'none';
1792      // Reset applicant counts
1793      document.getElementById('signupAdults').value = '1';
1794      document.getElementById('signupKids').value = '0';
1795      // After a brief moment, transition to details form
1796      setTimeout(() => {
1797        document.getElementById('displaySelectedService').textContent = serviceName;
1798        document.getElementById('loginStep3').style.display = 'none';
1799        document.getElementById('loginStep4').style.display = 'block';
1800        document.getElementById('loginModalBox').style.maxWidth = '480px';
1801        calcSignupPrice();
1802        setTimeout(() => document.getElementById('signupName').focus(), 100);
1803      }, 200);
1804    }
1805
1806    // Preselect a service (from landing-page visa modal) and jump straight to the details form,
1807    // skipping the Step 3 picker. Returns false if the service is unknown.
1808    function preselectService(serviceName) {
1809      if (!SERVICE_PRICING[serviceName]) return false;
1810      showSignup();
1811      document.getElementById('signupService').value = serviceName;
1812      document.getElementById('signupAdults').value = '1';
1813      document.getElementById('signupKids').value = '0';
1814      document.getElementById('displaySelectedService').textContent = serviceName;
1815      document.getElementById('loginStep3').style.display = 'none';
1816      document.getElementById('loginStep4').style.display = 'block';
1817      document.getElementById('loginModalBox').style.maxWidth = '480px';
1818      calcSignupPrice();
1819      setTimeout(() => document.getElementById('signupName').focus(), 100);
1820      return true;
1821    }
1822
1823    function goBackToServiceSelect() {
1824      document.getElementById('loginStep4').style.display = 'none';
1825      document.getElementById('loginStep3').style.display = 'block';
1826      document.getElementById('loginModalBox').style.maxWidth = '560px';
1827      document.getElementById('detailsErrorMsg').style.display = 'none';
1828    }
1829
1830    function goBackToLogin() {
1831      document.getElementById('loginStep3').style.display = 'none';
1832      document.getElementById('loginStep4').style.display = 'none';
1833      document.getElementById('loginStep1').style.display = 'block';
1834      document.getElementById('loginModalBox').style.maxWidth = '420px';
1835      document.getElementById('signupErrorMsg').style.display = 'none';
1836    }
1837
1838    // @formatter:off
1839    // Pricing config per service — injected from server (already includes partner discounts)
1840    const PARTNER_KEY = null;
1841    const PARTNER_LABEL = null;
1842    // Self-reported channel → discount %, previewed only when nothing is
1843    // already tracked (PARTNER_KEY set) — a real link always wins over a guess.
1844    const REFERRAL_CHANNEL_DISCOUNTS = {
1845      "Bing": 10,
1846      "Facebook": 10,
1847      "Google": 10,
1848      "Instagram": 10,
1849      "Linkedin": 10,
1850      "Reddit": 10,
1851      "Telegram": 10,
1852      "Twitter (X)": 10,
1853      "Twitter (X)": 10,
1854      "Youtube": 10,
1855      "TikTok": 10,
1856      
1857    };
1858    const SERVICE_PRICING = {
1859      'Non Lucrative Visa': { adult: 999, child: 500, has_discount: false  },
1860    'Digital Nomad Visa': { adult: 1399, child: 500, has_discount: false  },
1861    'Student Visa': { adult: 499, child: 249, has_discount: false  },
1862    'Citizenship': { adult: 1999, child: 1000, has_discount: false  },
1863    'Non Lucrative Visa Renewal': { adult: 499, child: 350, has_discount: false  },
1864    'Digital Nomad Visa Renewal': { adult: 799, child: 500, has_discount: false  },
1865    'Highly Skilled Worker Visa': { adult: 1399, child: 500, has_discount: false  },
1866    'Long-Term Residence': { adult: 499, child: 350, has_discount: false  }
1867    };
1868    // @formatter:on
1869
1870    // Same rules the server writes onto the client record — defs/def_payment_plans.py.
1871    const PAYMENT_PLANS = {"Citizenship": [{"adult": 500, "child": 300, "label": "First payment", "when": "when engaging service"}, {"adult": 500, "child": 300, "label": "Second payment", "when": "when your file is submitted to the authorities"}, {"adult": null, "child": null, "label": "Final payment", "when": "once we receive a positive resolution"}], "Digital Nomad Visa": [{"adult": 300, "child": 100, "label": "First payment", "when": "when engaging service"}, {"adult": 500, "child": 100, "label": "Second payment", "when": "before case processing"}, {"adult": null, "child": null, "label": "Final payment", "when": "once your visa is approved"}], "Digital Nomad Visa Renewal": [{"adult": 400, "child": 250, "label": "First payment", "when": "when engaging service"}, {"adult": 399, "child": 250, "label": "Second payment", "when": "after the visa is granted"}], "Golden Visa": [{"adult": 200, "child": 100, "label": "First payment", "when": "when engaging service"}, {"adult": 400, "child": 100, "label": "Second payment", "when": "before case processing"}, {"adult": null, "child": null, "label": "Final payment", "when": "once your visa is approved"}], "Highly Skilled Worker Visa": [{"adult": 300, "child": 100, "label": "First payment", "when": "when you sign up"}, {"adult": 500, "child": 100, "label": "Second payment", "when": "before case processing"}, {"adult": null, "child": null, "label": "Final payment", "when": "once your visa is approved"}
1871], "Long-Term Residence": [{"adult": 200, "child": 150, "label": "First payment", "when": "when you sign up"}, {"adult": 299, "child": 200, "label": "Second payment", "when": "when your application is submitted"}], "Non Lucrative Visa": [{"adult": 300, "child": 100, "label": "First payment", "when": "when engaging service"}, {"adult": 400, "child": 100, "label": "Second payment", "when": "before case processing"}, {"adult": null, "child": null, "label": "Final payment", "when": "once your visa is approved"}], "Non Lucrative Visa Renewal": [{"adult": 200, "child": 150, "label": "First payment", "when": "when engaging service"}, {"adult": 299, "child": 200, "label": "Second payment", "when": "after approval of the visa renewal"}], "Student Visa": [{"adult": 200, "child": 100, "label": "First payment", "when": "when engaging service"}, {"adult": 299, "child": 149, "label": "Second payment", "when": "once your visa is approved"}]};
1872    // How much sworn translation each service covers, per adult.
1873    const TRANSLATION_ALLOWANCE = {"Citizenship": 200, "Digital Nomad Visa": 150, "Digital Nomad Visa Renewal": 75, "Non Lucrative Visa": 80, "Student Visa": 50};
1874    // Each service's finished "what's included" lines, already resolved on the
1875    // server (defs/def_payment_plans.included_map): the covered translation
1876    // amount filled in, lines the service doesn't cover dropped, and the order
1877    // the service actually wants — the renewal's list is not the standard one.
1878    const INCLUDED_LINES = {"Citizenship": ["Official sworn translations, up to \u20ac200 per adult", "A dedicated immigration lawyer from Spain who knows your case", "Government appointment booking when applies", "Full document review and checklist built for you", "Application submission and follow up", "Your own dashboard, with Carlos our AI assistant"], "Digital Nomad Visa": ["Official sworn translations, up to \u20ac150 per adult", "UGE visa fees included", "A dedicated case manager who knows your case", "Full document review and a checklist built for you", "Application submission and follow-up", "Your own dashboard, with Carlos our AI assistant", "Appeals at no extra cost \u2014 full support against unfounded refusals or administrative silence."], "Digital Nomad Visa Renewal": ["Official sworn translations, up to \u20ac75 per adult", "UGE visa fees included", "A dedicated case manager who knows your case", "Application submission and follow-up", "Your own dashboard, with Carlos our AI assistant", "Appeals at no extra cost \u2014 full support against unfounded refusals or administrative silence."], "Golden Visa": ["A dedicated case manager who knows your case", "Government appointment booking", "Full document review and a checklist built for you", "Application submission and follow-up", "Your own dashboard, with Carlos our AI assistant", "DHL document delivery where it applies", "Appeals at no extra cost \u2014 full support against unfounded refusals or administrative silence."], "Highly Skilled Worker Visa": ["A dedicated case manager who knows your case", "Full document review and a checklist built for you", "Application submission and follow-up", "Your own dashboard, with Carlos our AI assistant", "DHL document delivery where it applies", "Appeals at no extra cost \u2014 full support against unfounded refusals or administrative silence."], "Long-Term Residence": ["A dedicated case manager who knows your case", "Full document review and a checklist built for you", "Application submission and follow-up", "Your own dashboard, with Carlos our AI assistant", "DHL document delivery where it applies", "Appeals at no extra cost \u2014 full support against unfounded refusals or administrative silence."], "Non Lucrative Visa": ["Official sworn translations, up to \u20ac80 per adult", "A dedicated case manager who knows your case", "Full document review and a checklist built for you", "Application submission and follow-up", "Your own dashboard, with Carlos our AI assistant", "DHL document delivery where it applies", "Appeals at no extra cost \u2014 full support against unfounded refusals or administrative silence."], "Non Lucrative Visa Renewal": ["A dedicated case manager who knows your case", "Government appointment booking", "Full document review and a checklist built for you", "Application submission and follow-up", "Your own dashboard, with Carlos our AI assistant", "DHL document delivery where it applies", "Appeals at no extra cost \u2014 full support against unfounded refusals or administrative silence."], "Student Visa": ["Official sworn translations, up to \u20ac50 per adult", "A dedicated case manager who knows your case", "Full document review and a checklist built for you", "Application submission and follow-up", "Your own dashboard, with Carlos our AI assistant", "DHL document delivery where it applies", "Appeals at no extra cost \u2014 full support against unfounded refusals or administrative silence."]};
1879
1880    // The list shows this many lines; the rest wait behind "Show more".
1881    const INCLUDED_PREVIEW = 3;
1882    let includedExpanded = false;
1883
1884    function escapeIncluded(text) {
1885      const d = document.createElement('div');
1886      d.textContent = text;
1887      return d.innerHTML;
1888    }
1889
1890    function renderIncludedList(service) {
1891      const list = document.getElementById('signupIncludedList');
1892      const toggle = document.getElementById('signupIncludedToggle');
1893      if (!list) return;
1894
1895      const lines = INCLUDED_LINES[service];
1896      // No service chosen yet: leave the server-rendered starting list alone.
1897      if (lines) {
1898        list.innerHTML = lines.map(function (line) {
1899          // The covered amount is the point of the translations line, so it
1900          // carries the weight. Everything is escaped first — these are our own
1901          // strings, but the bolding is the only markup they are allowed.
1902          const text = escapeIncluded(line).replace(
1903            /(€\d+)/, '<b style="color:#059669;font-weight:700;">$1</b>');
1904          return '<li style="display:flex;gap:8px;align-items:flex-start;font-size:12.5px;' +
1905            'color:var(--bl-text-secondary);line-height:1.4;">' +
1906            '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#10b981" ' +
1907            'stroke-width="3" stroke-linecap="round" stroke-linejoin="round" ' +
1908            'style="flex-shrink:0;margin-top:2px;">' +
1909            '<polyline points="20 6 9 17 4 12"/></svg><span>' + text + '</span></li>';
1910        }).join('');
1911      }
1912
1913      let shown = 0;
1914      list.querySelectorAll('li').forEach(function (row) {
1915        shown++;
1916        const visible = includedExpanded || shown <= INCLUDED_PREVIEW;
1917        row.style.display = visible ? 'flex' : 'none';
1918      });
1919
1920      if (!toggle) return;
1921      const hidden = Math.max(0, shown - INCLUDED_PREVIEW);
1922      toggle.style.display = hidden ? 'block' : 'none';
1923      toggle.textContent = includedExpanded ? 'Show less' : 'Show ' + hidden + ' more';
1924    }
1925
1926    function toggleIncluded() {
1927      includedExpanded = !includedExpanded;
1928      renderIncludedList(document.getElementById('signupService').value);
1929    }
1930
1931    function renderPaymentSteps(service, adults, kids, total) {
1932      const el = document.getElementById('signupPaymentSteps');
1933      if (!el) return;
1934      const plan = PAYMENT_PLANS[service];
1935      if (!plan || !total) { el.innerHTML = ''; return; }
1936
1937      let running = 0;
1938      el.innerHTML = plan.map(function (step, i) {
1939        // A step with no per-person amount takes whatever is left, so the parts
1940        // always add up to the total shown above.
1941        const last = i === plan.length - 1;
1942        let amount = (step.adult === null || last)
1943          ? total - running
1944          : Math.round((adults * step.adult) + (kids * step.child));
1945        if (amount < 0) amount = 0;
1946        running += amount;
1947        return '<div style="display:flex;justify-content:space-between;align-items:baseline;gap:10px;">' +
1948          '<span style="font-size:12.5px;color:var(--bl-text-secondary);">' +
1949          '<span style="display:inline-flex;align-items:center;justify-content:center;width:17px;height:17px;' +
1950          'border-radius:50%;background:rgba(59,130,246,0.12);color:var(--bl-primary);font-size:10px;' +
1951          'font-weight:700;margin-right:7px;">' + (i + 1) + '</span>' +
1952          step.label + ' <span style="color:var(--bl-text-muted);">· ' + step.when + '</span></span>' +
1953          '<span style="font-weight:700;font-size:13px;color:var(--bl-text-primary);white-space:nowrap;">€' +
1954          amount + '</span></div>';
1955      }).join('');
1956    }
1957
1958    function validateSignupConsent() {
1959      const terms = document.getElementById('signupTerms');
1960      const err = document.getElementById('signupConsentError');
1961      const row = document.getElementById('signupTermsRow');
1962      const accepted = !!(terms && terms.checked);
1963      // Clear the callout as soon as they tick it — leaving a box red after the
1964      // problem is fixed reads as a second, unexplained error.
1965      if (accepted) {
1966        if (err) err.style.display = 'none';
1967        if (row) row.classList.remove('is--missing');
1968      }
1969      return accepted;
1970    }
1971
1972    function highlightMissingConsent() {
1973      const row = document.getElementById('signupTermsRow');
1974      const terms = document.getElementById('signupTerms');
1975      if (!row) return;
1976      // Restart the nudge even when the class is already on, so a second attempt
1977      // is not silent.
1978      row.classList.remove('is--missing');
1979      void row.offsetWidth;
1980      row.classList.add('is--missing');
1981      // The consent sits at the bottom of a long scrolling form and may be out
1982      // of view when the button is pressed.
1983      if (row.scrollIntoView) row.scrollIntoView({ block: 'center', behavior: 'smooth' });
1984      if (terms) terms.focus({ preventScroll: true });
1985    }
1986
1987    function calcSignupPrice() {
1988      const service = document.getElementById('signupService').value;
1989      const adults = parseInt(document.getElementById('signupAdults').value, 10) || 1;
1990      const kids = parseInt(document.getElementById('signupKids').value, 10) || 0;
1991      const pricing = SERVICE_PRICING[service];
1992      const rowsEl = document.getElementById('signupPriceRows');
1993      const totalEl = document.getElementById('signupPriceTotal');
1994      const discountEl = document.getElementById('signupDiscountBadge');
1995
1996      if (!pricing) {
1997        rowsEl.innerHTML = '';
1998        totalEl.textContent = '€0';
1999        if (discountEl) discountEl.style.display = 'none';
2000        return;
2001      }
2002
2003      // A self-reported channel previews the same discount the server will
2004      // actually apply on submit — but only when nothing is already tracked;
2005      // a real link's own discount (has_discount) always takes priority.
2006      const referralEl = document.getElementById('signupReferral');
2007      const referralValue = referralEl ? referralEl.value : '';
2008      const selfReportPct = (!pricing.has_discount && REFERRAL_CHANNEL_DISCOUNTS[referralValue]) || 0;
2009      const effectiveAdult = selfReportPct
2010        ? Math.round(pricing.adult * (1 - selfReportPct / 100))
2011        : pricing.adult;
2012
2013      let total = (adults * effectiveAdult) + (kids * pricing.child);
2014      let rows = '';
2015
2016      if (pricing.has_discount && pricing.adult_original) {
2017        rows += '<div style="display:flex;justify-content:space-between;"><span style="color:var(--bl-text-secondary);">' + adults + ' Adult' + (adults > 1 ? 's' : '') + ' × <s style="color:var(--bl-text-muted);">€' + pricing.adult_original + '</s> €' + pricing.adult + '</span><span style="font-weight:600;color:var(--bl-text-primary);">€' + (adults * pricing.adult) + '</span></div>';
2018      } else if (selfReportPct) {
2019        rows += '<div style="display:flex;justify-content:space-between;"><span style="color:var(--bl-text-secondary);">' + adults + ' Adult' + (adults > 1 ? 's' : '') + ' × <s style="color:var(--bl-text-muted);">€' + pricing.adult + '</s> €' + effectiveAdult + '</span><span style="font-weight:600;color:var(--bl-text-primary);">€' + (adults * effectiveAdult) + '</span></div>';
2020      } else {
2021        rows += '<div style="display:flex;justify-content:space-between;"><span style="color:var(--bl-text-secondary);">' + adults + ' Adult' + (adults > 1 ? 's' : '') + ' × €' + pricing.adult + '</span><span style="font-weight:600;color:var(--bl-text-primary);">€' + (adults * pricing.adult) + '</span></div>';
2022      }
2023      // A child's fee is the same with or without a partner link, so this row
2024      // never carries a struck-through price.
2025      if (kids > 0) {
2026        rows += '<div style="display:flex;justify-content:space-between;"><span style="color:var(--bl-text-secondary);">' + kids + ' Child' + (kids > 1 ? 'ren' : '') + ' × €' + pricing.child + '</span><span style="font-weight:600;color:var(--bl-text-primary);">€' + (kids * pricing.child) + '</span></div>';
2027      }
2028      rowsEl.innerHTML = rows;
2029
2030      renderPaymentSteps(service, adults, kids, total);
2031      renderIncludedList(service);
2032      totalEl.textContent = '€' + total;
2033
2034      // Show/hide discount badge
2035      if (discountEl) {
2036        if (pricing.has_discount && PARTNER_LABEL) {
2037          discountEl.textContent = '🎉 ' + PARTNER_LABEL + ' Applied';
2038          discountEl.style.display = 'block';
2039        } else if (selfReportPct) {
2040          discountEl.textContent = '🎉 ' + referralValue + ' Discount Applied';
2041          discountEl.style.display = 'block';
2042        } else {
2043          discountEl.style.display = 'none';
2044        }
2045      }
2046    }
2047
2048    // ── GA4 signup conversions ────────────────────────────────────────────
2049    //
2050    // One event per visa, wired to a conversion action of the same name in
2051    // Google Ads, so the exact strings below matter: a typo here is a
2052    // conversion action that never records anything.
2053    //
2054    // There is one signup form on the site, not six — the visa is a field on it
2055    // — so the "which form" the ads side asks about is this map.
2056    var SIGNUP_CONVERSIONS = {
2057      'Digital Nomad Visa':         { event: 'ads_conversion_signup_DNV',         label: 'DNV Form Submission' },
2058      'Non Lucrative Visa':         { event: 'ads_conversion_signup_NLV',         label: 'NLV Form Submission' },
2059      'Student Visa':               { event: 'ads_conversion_signup_SV',          label: 'SV Form Submission' },
2060      'Citizenship':                { event: 'ads_conversion_signup_Citizenship', label: 'Citizenship Form Submission' },
2061      'Digital Nomad Visa Renewal': { event: 'ads_conversion_signup_DNVRenewal',  label: 'DNV Renewal Form Submission' },
2062      'Non Lucrative Visa Renewal': { event: 'ads_conversion_signup_NLVRenewal',  label: 'NLV Renewal Form Submission' }
2063    };
2064
2065    // Enhanced conversions. Ads matches a conversion back to the person who
2066    // clicked the ad using the address they signed up with — hashed here, in the
2067    // browser, so the address itself never leaves it and Google only ever
2068    // receives the digest. Normalisation is what their spec asks for (trimmed,
2069    // lowercased); submitSignup has already done both.
2070    function hashedEmail(email) {
2071      if (!email || !window.crypto || !window.crypto.subtle || !window.TextEncoder) {
2072        return Promise.resolve(null);
2073      }
2074      return crypto.subtle
2075        .digest('SHA-256', new TextEncoder().encode(email.trim().toLowerCase()))
2076        .then(function (buf) {
2077          return Array.prototype.map.call(new Uint8Array(buf), function (b) {
2078            return ('0' + b.toString(16)).slice(-2);
2079          }).join('');
2080        })
2081        .catch(function () { return null; });
2082    }
2083
2084    // Called only from the success branch below, and only when a new account was
2085    // actually created. `next` is whatever should happen afterwards — usually a
2086    // redirect, which is the reason for the callback: an event sent as the page
2087    // is being replaced is routinely lost, so navigation waits for GA to
2088    // acknowledge it, with a timer in case it never does.
2089    //
2090    // Nothing here waits on anything indefinitely. Hashing is given 250ms before
2091    // the event goes without it, and the redirect happens within 900ms of the
2092    // event whatever GA does — measurement never costs the visitor their signup.
2093    function trackSignupConversion(service, email, next) {
2094      var proceed = next || function () {};
2095      var mapping = SIGNUP_CONVERSIONS[service];
2096
2097      // With consent mode the tag is always present, so this is a genuine
2098      // absence — a blocker, or the script failing to load. Carry on unmeasured.
2099      if (!mapping || typeof gtag !== 'function') { proceed(); return; }
2100
2101      // One per session, matching the "Count: One" setting on the Ads side. A
2102      // browser with storage blocked reports nothing rather than throwing.
2103      var key = 'conv_fired_' + mapping.event;
2104      try {
2105        if (sessionStorage.getItem(key) === '1') { proceed(); return; }
2106        sessionStorage.setItem(key, '1');
2107      } catch (e) { /* private mode, blocked storage — fire anyway */ }
2108
2109      var moved = false;
2110      function go() { if (moved) { return; } moved = true; proceed(); }
2111
2112      var sent = false;
2113      function send(hash) {
2114        if (sent) { return; }
2115        sent = true;
2116
2117        if (hash) {
2118          gtag('set', 'user_data', { sha256_email_address: hash });
2119        }
2120
2121        gtag('event', mapping.event, {
2122          event_category: 'signup',
2123          event_label: mapping.label,
2124          value: 1,
2125          event_callback: go,
2126          event_timeout: 800
2127        });
2128        setTimeout(go, 900);
2129      }
2130
2131      hashedEmail(email).then(send);
2132      setTimeout(function () { send(null); }, 250);
2133    }
2134
2135    function submitSignup() {
2136      if (!validateSignupConsent()) {
2137        document.getElementById('signupConsentError').style.display = 'block';
2138        highlightMissingConsent();
2139        return;
2140      }
2141      const service = document.getElementById('signupService').value;
2142      const name = document.getElementById('signupName').value.trim();
2143      const email = document.getElementById('signupEmail').value.trim().toLowerCase();
2144      const phone = document.getElementById('signupPhone').value.trim();
2145      const adults = parseInt(document.getElementById('signupAdults').value, 10) || 1;
2146      const kids = parseInt(document.getElementById('signupKids').value, 10) || 0;
2147      const errorBox = document.getElementById('detailsErrorMsg');
2148      const errorText = document.getElementById('detailsErrorText');
2149      const btn = document.getElementById('btnSignup');
2150
2151      if (!service) {
2152        errorText.textContent = 'Please select a service.';
2153        errorBox.style.display = 'block';
2154        return;
2155      }
2156      if (!name) {
2157        errorText.textContent = 'Please enter your full name.';
2158        errorBox.style.display = 'block';
2159        return;
2160      }
2161      if (!email || !email.includes('@')) {
2162        errorText.textContent = 'Please enter a valid email address.';
2163        errorBox.style.display = 'block';
2164        return;
2165      }
2166      errorBox.style.display = 'none';
2167
2168      btn.disabled = true;
2169      btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" style="animation:spin 1s linear infinite;" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg> Creating your account...';
2170
2171      fetch('/api/dashboard-signup', {
2172        method: 'POST',
2173        headers: { 'Content-Type': 'application/json' },
2174        body: JSON.stringify({ service: service, name: name, email: email, phone: phone,
2175                               applicants_adults: adults, applicants_kids: kids,
2176                               referral_channel: document.getElementById('signupReferral').value,
2177                               accepted_terms: document.getElementById('signupTerms').checked,
2178                               newsletter_opt_in: document.getElementById('signupNewsletter').checked })
2179      })
2180        .then(r => r.json())
2181        .then(data => {
2182          btn.disabled = false;
2183          btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/></svg> Create Free Account';
2184          if (data.success) {
2185            // A signup that opened an existing file is not a new lead: no
2186            // account was created, we recognised the address and sent a code to
2187            // the file we already hold. Counting it would inflate exactly the
2188            // number the ads side is bidding on.
2189            var isNewAccount = !data.existing_file;
2190
2191            if (data.auto_login) {
2192              // Auto-logged in — go straight to dashboard, once the conversion
2193              // has been sent (see trackSignupConversion).
2194              var goToDashboard = function () { window.location.href = '/dashboard'; };
2195              if (isNewAccount) { trackSignupConversion(service, email, goToDashboard); }
2196              else { goToDashboard(); }
2197            } else {
2198              if (isNewAccount) { trackSignupConversion(service, email); }
2199              // Fallback: go to verification step. Also the path when we already
2200              // hold a file on this email — the code opens that one instead of
2201              // starting a second account.
2202              loginEmail = email;
2203              document.getElementById('displayLoginEmail').textContent = email;
2204              document.getElementById('loginStep4').style.display = 'none';
2205              document.getElementById('loginStep2').style.display = 'block';
2206              document.getElementById('loginModalBox').style.maxWidth = '420px';
2207              showLoginToast(data.existing_file
2208                ? 'We already have your details — we sent a code to ' + email
2209                : 'A login code was sent to ' + email);
2210              startResendCooldown(30);
2211              setTimeout(() => document.getElementById('loginCode').focus(), 100);
2212            }
2213          } else {
2214            errorText.textContent = data.error || 'Failed to create account.';
2215            errorBox.style.display = 'block';
2216          }
2217        })
2218        .catch(() => {
2219          btn.disabled = false;
2220          btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/></svg> Create Free Account';
2221          errorText.textContent = 'A network error occurred.';
2222          errorBox.style.display = 'block';
2223        });
2224    }
2225
2226    // Enter key triggers verify on Step 2 + auto-submit when 6 digits entered
2227    document.getElementById('loginCode').addEventListener('keypress', function (e) {
2228      if (e.key === 'Enter') { e.preventDefault(); verifyLoginCode(); }
2229    });
2230    document.getElementById('loginCode').addEventListener('input', function () {
2231      if (this.value.trim().length === 6) { verifyLoginCode(); }
2232    });
2233
2234    // Auto-open signup if redirected from main site with ?action=signup
2235    (function () {
2236      var params = new URLSearchParams(window.location.search);
2237      if (params.get('action') === 'signup') {
2238        var service = params.get('service');
2239        // If a visa was already chosen on the landing page, skip the picker (Step 3)
2240        // and go straight to the details form; otherwise show the picker.
2241        if (!(service && preselectService(service))) {
2242          showSignup();
2243        }
2244      }
2245    })();
2246
2247    // Hold the page still behind the modal.
2248    //
2249    // The dashboard underneath is several screens tall and this overlay is
2250    // position:fixed, so on a phone a swipe dragged the page behind rather than
2251    // the form: it slid for a second or two, hit the bottom, and from then on
2252    // nothing appeared to move at all. Nothing back there is reachable until
2253    // this modal is answered, so it should not be taking the gesture.
2254    //
2255    // overflow:hidden alone does not hold Safari on iOS — the body has to be
2256    // taken out of flow. Safe here because the modal is up before the visitor
2257    // has scrolled anywhere.
2258    (function () {
2259      var body = document.body;
2260      body.style.overflow = 'hidden';
2261      body.style.position = 'fixed';
2262      body.style.width = '100%';
2263    })();
2264  </script>
2264
2265  <style>
2266    @keyframes spin {
2267      to {
2268        transform: rotate(360deg);
2269      }
2270    }
2271
2272    .dn-login-toast {
2273      position: fixed;
2274      top: 24px;
2275      left: 50%;
2276      transform: translateX(-50%);
2277      z-index: 100000;
2278      display: flex;
2279      align-items: center;
2280      gap: 8px;
2281      background: #10b981;
2282      color: #fff;
2283      font-size: 13px;
2284      font-weight: 600;
2285      padding: 12px 18px;
2286      border-radius: 10px;
2287      box-shadow: 0 8px 24px rgba(16, 185, 129, 0.35);
2288      animation: dnLoginToastIn 0.25s ease;
2289    }
2290
2291    .dn-login-toast--hide {
2292      animation: dnLoginToastOut 0.25s ease forwards;
2293    }
2294
2295    @keyframes dnLoginToastIn {
2296      from { opacity: 0; transform: translate(-50%, -10px); }
2297      to   { opacity: 1; transform: translate(-50%, 0); }
2298    }
2299
2300    @keyframes dnLoginToastOut {
2301      from { opacity: 1; transform: translate(-50%, 0); }
2302      to   { opacity: 0; transform: translate(-50%, -10px); }
2303    }
2304
2305    @media (prefers-reduced-motion: reduce) {
2306      .dn-login-toast, .dn-login-toast--hide {
2307        animation: none;
2308      }
2309    }
2310
2311    /* Trying to sign up without accepting the terms. The red note under the box
2312       was easy to miss, so the consent row itself is called out and nudged. */
2313    #signupTermsRow {
2314      border: 1.5px solid transparent;
2315      border-radius: 10px;
2316      padding: 8px 10px;
2317      margin: -8px -10px;
2318      transition: border-color 0.2s ease, background 0.2s ease;
2319    }
2320
2321    #signupTermsRow.is--missing {
2322      border-color: #f06548;
2323      background: rgba(240, 101, 72, 0.07);
2324      animation: signupTermsNudge 0.4s ease;
2325    }
2326
2327    /* Draws the eye without moving the layout. */
2328    @keyframes signupTermsNudge {
2329      0%, 100% { transform: translateX(0); }
2330      25%      { transform: translateX(-5px); }
2331      75%      { transform: translateX(5px); }
2332    }
2333
2334    @media (prefers-reduced-motion: reduce) {
2335      #signupTermsRow.is--missing {
2336        animation: none;
2337      }
2338    }
2339
2340    @media (max-width: 480px) {
2341      #loginModalBox {
2342        max-width: 95vw !important;
2343      }
2344    }
2345  </style>
2346  
2347
2348  <div class="dn-wrapper" style="flex-direction: column;">
2349
2350    <!-- ============ Header ============ -->
2351    <header class="dn-header" style="position:sticky;top:0;z-index:90;">
2352      <div class="dn-header-left">
2353        <a href="/dashboard" style="display:flex;align-items:center;">
2354          
2355          <picture>
2356            <source media="(max-width: 600px)"
2357              srcset="https://w-cdn.b-cdn.net/img/custom/bureaucracy-round%20Background%20Removed.png?width=104">
2358            <img src="https://w-cdn.b-cdn.net/vw_bureaucracy/img/bureaucracy_logo.png" alt="Bureaucracy.es"
2359              class="dn-header-logo">
2360          </picture>
2361        </a>
2362      </div>
2363      <div class="dn-header-right">
2364        
2365        <div class="dn-lang notranslate" id="bulang" style="position:relative;">
2366          <button type="button" id="bulangBtn" onclick="bulangToggle()" aria-haspopup="true" aria-expanded="false"
2367            title="Change language"
2368            style="display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:8px;border:1px solid var(--bl-border);background:var(--bl-bg-card);color:var(--bl-text-secondary);font-size:13px;font-weight:600;cursor:pointer;transition:all 0.15s;white-space:nowrap;">
2369            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
2370            <span id="bulangCur">EN</span>
2371          </button>
2372          <div class="dn-lang-menu" id="bulangMenu" role="menu">
2373            <button type="button" role="menuitem" onclick="buSetLang('en')">English</button>
2374            <button type="button" role="menuitem" onclick="buSetLang('es')">Español</button>
2375            <button type="button" role="menuitem" onclick="buSetLang('pt')">Português</button>
2376          </div>
2377        </div>
2378        
2379        <button id="tourBtn" type="button" onclick="startDashboardTour()" title="Take a quick tour of your dashboard"
2380          style="display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:8px;border:1px solid var(--bl-border);background:var(--bl-bg-card);color:var(--bl-text-secondary);font-size:13px;font-weight:600;cursor:pointer;transition:all 0.15s;white-space:nowrap;">
2381          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2382            <circle cx="12" cy="12" r="10" />
2383            <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
2384            <line x1="12" y1="17" x2="12.01" y2="17" />
2385          </svg>
2386          <span class="dn-tour-btn-label">Take a tour</span>
2387        </button>
2388        <button id="notesBtn" type="button" onclick="openNotesModal()" title="Your private notes"
2389          style="display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:8px;border:1px solid var(--bl-border);background:var(--bl-bg-card);color:var(--bl-text-secondary);font-size:13px;font-weight:600;cursor:pointer;transition:all 0.15s;white-space:nowrap;position:relative;">
2390          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2391            <path d="M12 20h9" />
2392            <path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" />
2393          </svg>
2394          <span class="dn-tour-btn-label">My notes</span>
2395          
2396          <span id="notesDot" style="display:none;position:absolute;top:4px;right:5px;width:7px;height:7px;border-radius:50%;background:var(--bl-primary);"></span>
2397        </button>
2398        
2399        
2400        
2401        <div class="dn-notif-wrap" style="position:relative;">
2402          <button id="notifBtn" type="button" onclick="toggleNotifications(event)"
2403            title="Notifications" aria-haspopup="true" aria-expanded="false"
2404            style="display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:8px;border:1px solid var(--bl-border);background:var(--bl-bg-card);color:var(--bl-text-secondary);font-size:13px;font-weight:600;cursor:pointer;transition:all 0.15s;position:relative;">
2405            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2406              <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
2407              <path d="M13.73 21a2 2 0 0 1-3.46 0" />
2408            </svg>
2409            
2410            <span id="notifCount" style="display:none;position:absolute;top:-6px;right:-6px;min-width:17px;height:17px;padding:0 5px;border-radius:999px;background:var(--bl-danger);color:#fff;font-size:10px;font-weight:700;line-height:17px;text-align:center;"></span>
2411          </button>
2412
2413          <div class="dn-avatar-menu" id="notifMenu" style="min-width:320px;max-width:min(360px,calc(100vw - 24px));">
2414            <div class="dn-avatar-menu-header" style="display:flex;align-items:center;gap:8px;">
2415              <div style="font-weight:600;font-size:13px;color:var(--bl-text-primary);">Notifications</div>
2416              <span id="notifNewPill" style="display:none;font-size:10px;font-weight:700;padding:2px 7px;border-radius:999px;background:var(--bl-primary-bg);color:var(--bl-primary);">new</span>
2417            </div>
2418            <div class="dn-avatar-menu-divider"></div>
2419
2420            
2421            
2422            <div id="notifList" style="max-height:340px;overflow-y:auto;">
2423              
2424              <div style="padding:22px 14px;text-align:center;font-size:12.5px;color:var(--bl-text-muted);">
2425                Nothing yet. Updates from your  show up here.
2426              </div>
2427              
2428            </div>
2429
2430            
2431          </div>
2432        </div>
2433        <div class="dn-avatar-dropdown" style="position:relative;">
2434          
2435          <div class="dn-header-avatar-fallback" onclick="toggleAvatarDropdown(event)" style="cursor:pointer;"
2436            title="Account menu">?</div>
2437          
2438          <div class="dn-avatar-menu" id="avatarMenu">
2439            <div class="dn-avatar-menu-header">
2440              <div style="font-weight:600;font-size:13px;color:var(--bl-text-primary);">User</div>
2441              <div style="font-size:11px;color:var(--bl-text-muted);"></div>
2442            </div>
2443            <div class="dn-avatar-menu-divider"></div>
2444            
2445            <div id="avatarNavLinks"></div>
2446            <div class="dn-avatar-menu-divider"></div>
2447            <a class="dn-avatar-menu-item" href="javascript:void(0)"
2448              onclick="openProfileModal(); closeAvatarDropdown();">
2449              <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2450                <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
2451                <circle cx="12" cy="7" r="4" />
2452              </svg>
2453              Edit Profile
2454            </a>
2455            <div class="dn-avatar-menu-divider"></div>
2456            <a class="dn-avatar-menu-item dn-avatar-menu-logout" href="/logout-dashboard">
2457              <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2458                <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
2459                <polyline points="16 17 21 12 16 7" />
2460                <line x1="21" y1="12" x2="9" y2="12" />
2461              </svg>
2462              Log out
2463            </a>
2464          </div>
2465        </div>
2466      </div>
2467    </header>
2468
2469    <!-- ============ Section navigation (pills) ============ -->
2470    <nav class="dnv-nav" id="dnvNav" aria-label="Dashboard sections">
2471      <div class="dnv-nav-in" id="dnvNavIn">
2472        <button type="button" class="dnv-pill on" data-dnv="overview">Overview</button>
2473        <button type="button" class="dnv-pill" data-dnv="tasks">Tasks</button>
2474        <button type="button" class="dnv-pill" data-dnv="documents">Documents</button>
2475        <button type="button" class="dnv-pill" data-dnv="included">My Service</button>
2476        <button type="button" class="dnv-pill" data-dnv="help">Help</button>
2477        <button type="button" class="dnv-pill" data-dnv="perks">Partners</button>
2478        
2479        
2480        <button type="button" class="dnv-pill" data-dnv="socials">Socials</button>
2481        
2482      </div>
2483    </nav>
2484
2485    <!-- ============ Content ============ -->
2486    <div class="dn-content" style="max-width:1200px;margin:0 auto;width:100%;">
2487      
2488      <!-- ===== Stats Row ===== -->
2489      <div class="dn-stats-row">
2490        <!-- Service Type -->
2491        <div class="dn-stat-card blue dn-animate dn-animate-delay-1">
2492          <div class="dn-stat-header">
2493            <div class="dn-stat-icon blue">
2494              <svg viewBox="0 0 24 24">
2495                <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
2496                <polyline points="14 2 14 8 20 8" />
2497              </svg>
2498            </div>
2499          </div>
2500          <div class="dn-stat-value" style="font-size:20px;">Pending</div>
2501          <div class="dn-stat-label">Service Type</div>
2502          
2503          
2504        </div>
2505
2506        <!-- Progress (merged) -->
2507        <div class="dn-stat-card green dn-animate dn-animate-delay-2">
2508          <div class="dn-stat-header">
2509            <div class="dn-stat-icon green">
2510              <svg viewBox="0 0 24 24">
2511                <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
2512                <polyline points="22 4 12 14.01 9 11.01" />
2513              </svg>
2514            </div>
2515          </div>
2516          
2517          <div class="dn-stat-value">0</div>
2518          <div class="dn-stat-label">Tasks Completed</div>
2519          
2520          
2521          <div class="dn-stat-actions">
2522            <button type="button" class="dn-stat-btn"
2523              onclick="if(window.dnNav){dnNav.show('tasks')}else{location.hash='tasks'}"
2524              style="border:1px solid rgba(34,197,94,0.25);background:rgba(34,197,94,0.07);color:#15803d;">
2525              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
2526                stroke-linecap="round" stroke-linejoin="round">
2527                <polyline points="9 11 12 14 22 4" />
2528                <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
2529              </svg>
2530              See all tasks
2531            </button>
2532          </div>
2533        </div>
2534
2535        <!-- Client Profile -->
2536        <div class="dn-stat-card purple dn-animate dn-animate-delay-3" style="text-align:center;">
2537          <div style="display:inline-block;margin-bottom:8px;">
2538            
2539            <div id="profileCardImg"
2540              style="width:52px;height:52px;border-radius:50%;background:rgba(183,82,255,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto;">
2541              <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="rgba(183,82,255,0.5)"
2542                stroke-width="1.5">
2543                <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
2544                <circle cx="12" cy="7" r="4" />
2545              </svg>
2546            </div>
2547            
2548          </div>
2549          <div class="dn-stat-value" style="font-size:16px;"></div>
2550          <div class="dn-stat-label">—</div>
2551          <div class="dn-stat-actions">
2552            <button onclick="openProfileModal()" class="dn-stat-btn"
2553              style="border:1px solid rgba(183,82,255,0.2);background:rgba(183,82,255,0.06);color:var(--bl-primary);">
2554              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
2555                <path d="M12 20h9" />
2556                <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
2557              </svg>
2558              Edit Profile
2559            </button>
2560          </div>
2561        </div>
2562
2563        <!-- Case Manager -->
2564        <div class="dn-stat-card orange dn-animate dn-animate-delay-4" style="text-align:center;">
2565          <div style="display:inline-block;margin-bottom:8px;">
2566            
2567            <div class="dn-stat-icon orange" style="margin:0 auto;">
2568              <svg viewBox="0 0 24 24">
2569                <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
2570                <circle cx="12" cy="7" r="4" />
2571              </svg>
2572            </div>
2573            
2574          </div>
2575          <div class="dn-stat-label" style="margin-bottom:4px;font-weight:600;color:var(--bl-text-primary);">Your </div>
2576          
2577          <div class="dn-stat-value" style="font-size:17px;"></div>
2578          
2579          
2580          <div class="dn-stat-actions">
2581            <button type="button" class="dn-stat-btn"
2582              onclick="if(window.dnNav){dnNav.show('help')}else{location.hash='help'}"
2583              style="border:1px solid rgba(255,168,0,0.28);background:rgba(255,168,0,0.08);color:var(--bl-primary);">
2584              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"
2585                stroke-linecap="round" stroke-linejoin="round">
2586                <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
2587              </svg>
2588              Contact
2589            </button>
2590          </div>
2591        </div>
2592      </div>
2593
2594      
2595      <!-- ===== Payment Options ===== -->
2596      
2597      
2598      
2599      
2600      
2601
2602      
2603      
2604      
2605      
2606
2607      
2608      
2609      
2610      <button type="button" id="dnPaymentsShowBtn" onclick="showPaymentsCard()" class="dn-show-btn">
2611        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
2612          stroke-linecap="round" stroke-linejoin="round">
2613          <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
2614          <circle cx="12" cy="12" r="3" />
2615        </svg>
2616        Show payments
2617      </button>
2618      <div class="dn-card dn-animate dn-animate-delay-2" id="dnPaymentsCard" style="margin-bottom:20px;">
2619        <div class="dn-card-header">
2620          <div>
2621            <div class="dn-card-title" style="display:flex;align-items:center;gap:8px;">
2622              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--bl-primary)" stroke-width="2">
2623                <rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
2624                <line x1="1" y1="10" x2="23" y2="10" />
2625              </svg>
2626              Payments
2627            </div>
2628            
2629            
2630            
2631            <div class="dn-pay-total">€ 0</div>
2632            <div class="dn-card-subtitle">
2633              Total for  adults, 
2634              
2635            </div>
2636          </div>
2637          <button type="button" id="dnPaymentsHideBtn" onclick="hidePaymentsCard()" title="Hide payments"
2638            aria-label="Hide payments" class="dn-hide-btn">
2639            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
2640              stroke-linecap="round" stroke-linejoin="round">
2641              <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
2642              <line x1="1" y1="1" x2="23" y2="23" />
2643            </svg>
2644          </button>
2645        </div>
2646        <div class="dn-card-body">
2647
2648          
2649          
2650          
2651          
2652
2653          
2654          
2655
2656          <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;">
2657
2658            
2659            <div class="dn-pay-card ">
2660              
2661      <div class="dn-pay-header">
2662        <div class="dn-pay-head-left">
2663          <div class="dn-pay-num">1</div>
2664          
2665        </div>
2666        <div class="dn-pay-amount">€ </div>
2667      </div>
2668      
2669              <div class="dn-pay-desc">
2670                Make first payment to get started.
2671              </div>
2672              <div class="dn-pay-action">
2673                
2674                
2675      <span class="dn-badge dn-badge-green" style="font-size:12px;padding:6px 14px;">
2676        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
2677          style="margin-right:4px;vertical-align:-2px;">
2678          <polyline points="20 6 9 17 4 12" />
2679        </svg>
2680        Paid
2681      </span>
2682      
2683                
2684              </div>
2685            </div>
2686
2687            
2688            
2689            <div class="dn-pay-card ">
2690              
2691      <div class="dn-pay-header">
2692        <div class="dn-pay-head-left">
2693          <div class="dn-pay-num">2</div>
2694          
2695        </div>
2696        <div class="dn-pay-amount">€ </div>
2697      </div>
2698      
2699              <div class="dn-pay-desc">
2700                
2701                Due <strong>before case processing</strong>.
2702                
2703              </div>
2704              <div class="dn-pay-action">
2705                
2706                
2707      <span class="dn-badge dn-badge-green" style="font-size:12px;padding:6px 14px;">
2708        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
2709          style="margin-right:4px;vertical-align:-2px;">
2710          <polyline points="20 6 9 17 4 12" />
2711        </svg>
2712        Paid
2713      </span>
2714      
2715                
2716              </div>
2717            </div>
2718            
2719
2720            
2721            
2722            <div class="dn-pay-card ">
2723              
2724      <div class="dn-pay-header">
2725        <div class="dn-pay-head-left">
2726          <div class="dn-pay-num">3</div>
2727          
2728        </div>
2729        <div class="dn-pay-amount">€ </div>
2730      </div>
2731      
2732              <div class="dn-pay-desc">
2733                Due <strong>once your visa has been approved</strong>.
2734              </div>
2735              <div class="dn-pay-action">
2736                
2737                
2738      <span class="dn-badge dn-badge-green" style="font-size:12px;padding:6px 14px;">
2739        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
2740          style="margin-right:4px;vertical-align:-2px;">
2741          <polyline points="20 6 9 17 4 12" />
2742        </svg>
2743        Paid
2744      </span>
2745      
2746                
2747              </div>
2748            </div>
2749            
2750
2751            
2752            
2753
2754          </div>
2755
2756          
2757          
2758
2759          
2760          
2761          
2762
2763          
2764          
2765          <button type="button"
2766            onclick="var r=document.querySelector('[data-hideable=referral]');if(!r){return;}var b=r.previousElementSibling;if(getComputedStyle(r).display==='none'&&b&&b.classList.contains('dn-show-btn')){b.click();}r.scrollIntoView({behavior:'smooth',block:'start'});"
2767            style="margin-top:16px;width:100%;display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:11px 16px;border-radius:10px;border:1px solid rgba(34,197,94,0.25);background:rgba(34,197,94,0.06);color:#16a34a;font-size:12.5px;font-weight:600;cursor:pointer;transition:all 0.15s;font-family:inherit;">
2768            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
2769              stroke-linecap="round" stroke-linejoin="round">
2770              <polyline points="20 12 20 22 4 22 4 12" />
2771              <rect x="2" y="7" width="20" height="5" />
2772              <line x1="12" y1="22" x2="12" y2="7" />
2773              <path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z" />
2774              <path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z" />
2775            </svg>
2776            Refer a friend — 10% off your next renewal
2777            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
2778              stroke-linecap="round" stroke-linejoin="round">
2779              <line x1="5" y1="12" x2="19" y2="12" />
2780              <polyline points="12 5 19 12 12 19" />
2781            </svg>
2782          </button>
2783          
2784        </div>
2785      </div>
2786
2787      
2787<script>
2788        (function () {
2789          var KEY = 'dn_hide_payments_';
2790          var card = document.getElementById('dnPaymentsCard');
2791          var showBtn = document.getElementById('dnPaymentsShowBtn');
2792          window.hidePaymentsCard = function () {
2793            if (card) card.style.display = 'none';
2794            if (showBtn) showBtn.style.display = 'flex';
2795            try { localStorage.setItem(KEY, '1'); } catch (e) {}
2796          };
2797          window.showPaymentsCard = function () {
2798            if (card) card.style.display = '';
2799            if (showBtn) showBtn.style.display = 'none';
2800            try { localStorage.removeItem(KEY); } catch (e) {}
2801          };
2802          try {
2803            if (localStorage.getItem(KEY) === '1') window.hidePaymentsCard();
2804          } catch (e) {}
2805        })();
2806      </script>
2806
2807      
2808
2809      <!-- Mollie Checkout Modal -->
2810      <div id="mollieCheckoutModal" class="dn-modal-overlay" style="display:none;">
2811        <div class="dn-modal" style="max-width:440px;">
2812          <div class="dn-modal-header">
2813            <div>
2814              <div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--bl-primary);margin-bottom:2px">Secure Payment</div>
2815              <div id="mollieModalTitle" style="font-size:16px;font-weight:700;color:var(--bl-text-primary);">Payment</div>
2816            </div>
2817            <button onclick="closeMollieModal()" class="dn-modal-close">&times;</button>
2818          </div>
2819          <div class="dn-modal-body" style="text-align:center;padding:30px;">
2820
2821            <!-- Loading state -->
2822            <div id="mollieLoadingState">
2823              <div style="width:40px;height:40px;border:3px solid var(--bl-border,#e5e7eb);border-top-color:var(--bl-primary);border-radius:50%;animation:spin 0.7s linear infinite;margin:0 auto 16px;"></div>
2824              <p style="color:var(--bl-text-secondary);margin:0;font-size:13px;">Setting up your secure payment…</p>
2825            </div>
2826
2827            <!-- Ready state. The client clicks through themselves: this used to
2828                 redirect on its own the moment the checkout URL came back, so
2829                 the modal only ever flashed — nobody read that card is the main
2830                 way to pay, and nobody saw the Stripe alternative underneath. -->
2831            <div id="mollieOpenedState" style="display:none;">
2832              <div style="width:56px;height:56px;border-radius:50%;background:rgba(34,197,94,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 14px;">
2833                <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2"><rect x="1" y="4" width="22" height="16" rx="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg>
2834              </div>
2835              <div style="font-size:16px;font-weight:700;margin-bottom:4px;">Pay by credit card</div>
2836              <p style="font-size:13px;color:var(--bl-text-secondary);margin-bottom:18px;">You'll be taken to our secure payment page and brought straight back here as soon as it's done.</p>
2837              <a id="mollie_reopen_link" href="#" class="dn-pay-btn" style="display:inline-flex;padding:11px 26px;font-size:14px;border-radius:10px;text-decoration:none;margin-bottom:10px;">
2838                <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="4" width="22" height="16" rx="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg>
2839                Pay <span id="mollie_pay_amount"></span> by card
2840              </a>
2841              <div style="font-size:11px;color:var(--bl-text-muted);line-height:1.4;">Visa, Mastercard and American Express &middot; secured by Mollie</div>
2842            </div>
2843
2844            <!-- Error state -->
2845            <div id="mollieErrorState" style="display:none;">
2846              <div style="width:56px;height:56px;border-radius:50%;background:rgba(240,101,72,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 14px;">
2847                <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f06548" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
2848              </div>
2849              <div style="font-size:16px;font-weight:700;margin-bottom:4px;">Something went wrong</div>
2850              <p style="font-size:13px;color:var(--bl-text-secondary);margin-bottom:18px;">We couldn't start the payment. Please try again.</p>
2851            </div>
2852          </div>
2853          <!-- Column, so the caption sits under the whole row: with it stacked beside
2854               one button that column grew taller and centred Close out of line. -->
2855          <div class="dn-modal-footer" style="flex-direction:column;align-items:stretch;gap:8px;">
2856            <div style="display:flex;justify-content:space-between;align-items:center;gap:12px;">
2857              <a id="dn_stripe_link" href="" class="dn-modal-btn-secondary"
2858                style="display:inline-flex;align-items:center;gap:7px;text-decoration:none;">
2859                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink:0;">
2860                  <path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/><path d="M18 12a2 2 0 0 0 0 4h4v-4Z"/>
2861                </svg>
2862                Other payment options
2863              </a>
2864              <button onclick="closeMollieModal()" class="dn-modal-btn-secondary">Close</button>
2865            </div>
2866            <small style="color:var(--bl-text-muted);font-size:11px;line-height:1.35;">
2867              Rather not use a card? Pay another way or spread the cost — handled by <b>Stripe</b>.
2868            </small>
2869          </div>
2870        </div>
2871      </div>
2872      <style>@keyframes spin{to{transform:rotate(360deg)}}</style>
2873      
2874      
2875
2876
2877      <!-- Payment Success Modal -->
2878      
2879
2880      
2881      
2882
2883      <!-- ===== Your Case Manager (Messages + Meetings tabs) ===== -->
2884      
2885
2886      <!-- ===== Timeline ===== -->
2887
2888      
2889      
2890
2891
2892      <!-- ===== Express Service Deadlines ===== -->
2893
2894      
2895
2896
2897      <!-- ===== AI + Tasks Grid ===== -->
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910      <div class="dn-grid-2 dn-help-grid" style="margin-bottom:20px;align-items:start;">
2911
2912        <!-- ===== Carlos (AI assistant) Card ===== -->
2913        <div class="dn-card dn-ai-card" data-hideable="ai_tasks" data-hide-label="Carlos" id="aiCard" style="margin-bottom:0;">
2914          
2915          <div class="dn-card-header">
2916            <div>
2917              <div class="dn-card-title" style="display:flex;align-items:center;gap:8px;">
2918                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#b752ff" stroke-width="2"
2919                  stroke-linecap="round" stroke-linejoin="round">
2920                  <path d="M12 2L2 7l10 5 10-5-10-5z" />
2921                  <path d="M2 17l10 5 10-5" />
2922                  <path d="M2 12l10 5 10-5" />
2923                </svg>
2924                Carlos
2925              </div>
2926              <div class="dn-card-subtitle">Ask anything about your visa process</div>
2927            </div>
2928            <span class="dn-badge" style="margin-left:auto;font-size:10px;background:rgba(183,82,255,0.1);color:#b752ff;">Carlos AI</span>
2929          </div>
2930          <div class="dn-card-body" style="padding-top:12px;">
2931
2932            
2933            <div id="aiChatInterface">
2934              <!-- Messages -->
2935              <div id="aiMessages"
2936                style="min-height:200px;max-height:600px;overflow-y:auto;display:flex;flex-direction:column;gap:12px;margin-bottom:16px;padding-right:4px;">
2937                <!-- Messages injected here -->
2938              </div>
2939
2940              <!-- Input -->
2941              <div
2942                style="background:var(--bl-bg);border-radius:12px;padding:12px 14px;border:1px solid var(--bl-border);transition:box-shadow 0.2s ease;"
2943                id="aiInputCard">
2944                <form id="aiChatForm" style="display:flex;flex-direction:column;gap:8px;">
2945                  <textarea id="aiChatInput" placeholder="Ask a question about your visa process..." rows="2"
2946                    style="width:100%;border:none;resize:none;outline:none;background:transparent;font-size:13px;color:var(--bl-text-primary);line-height:1.5;min-height:48px;max-height:160px;font-family:inherit;"></textarea>
2947                  <div style="display:flex;justify-content:space-between;align-items:center;">
2948                    <div style="font-size:10px;color:var(--bl-text-muted);">AI may not always be 100% accurate</div>
2949                    <button type="submit" id="aiSendBtn" disabled
2950                      style="width:32px;height:32px;border-radius:50%;border:none;background:linear-gradient(135deg,#2c3136,#1a1d20);color:white;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all 0.2s ease;">
2951                      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
2952                        stroke-width="2">
2953                        <line x1="12" y1="19" x2="12" y2="5" />
2954                        <polyline points="5 12 12 5 19 12" />
2955                      </svg>
2956                    </button>
2957                  </div>
2958                </form>
2959              </div>
2960            </div>
2961
2962          </div>
2963        </div>
2964
2965        <!-- ===== Referral Program (top of the right column) ===== -->
2966        
2967        
2968        
2969        
2970        <div class="dn-card" data-hideable="referral" data-hide-label="Referral program"
2971          style="margin-bottom:20px;" data-tour="referral">
2972          <div class="dn-card-header">
2973            <div>
2974              <div class="dn-card-title" style="display:flex;align-items:center;gap:8px;">
2975                <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="var(--bl-primary)"
2976                  stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2977                  <path d="M20 21v-2a4 4 0 0 0-3-3.87" />
2978                  <path d="M4 21v-2a4 4 0 0 1 3-3.87" />
2979                  <circle cx="12" cy="7" r="4" />
2980                </svg>
2981                Referral Program
2982              </div>
2983              <div class="dn-card-subtitle">Every friend you refer takes <strong style="color:#16a34a;">10% off your next visa renewal</strong> — and they get 10% off too</div>
2984            </div>
2985          </div>
2986          <div class="dn-card-body">
2987
2988            <div class="dn-ref-steps">
2989              
2990              <div class="dn-ref-step is-done"
2991                title="Your welcome 10%">
2992                
2993                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"
2994                  stroke-linecap="round" stroke-linejoin="round">
2995                  <polyline points="20 6 9 17 4 12" />
2996                </svg>
2997                
2998              </div>
2999              
3000              <div class="dn-ref-step"
3001                title="Referral 1">
3002                2
3003              </div>
3004              
3005              <div class="dn-ref-step"
3006                title="Referral 2">
3007                3
3008              </div>
3009              
3010              <div class="dn-ref-step"
3011                title="Referral 3">
3012                4
3013              </div>
3014              
3015              <div class="dn-ref-step"
3016                title="Referral 4">
3017                5
3018              </div>
3019              
3020              <div class="dn-ref-step"
3021                title="Referral 5">
3022                6
3023              </div>
3024              
3025              <div class="dn-ref-step"
3026                title="Referral 6">
3027                7
3028              </div>
3029              
3030              <div class="dn-ref-step"
3031                title="Referral 7">
3032                8
3033              </div>
3034              
3035              <div class="dn-ref-step"
3036                title="Referral 8">
3037                9
3038              </div>
3039              
3040              <div class="dn-ref-step"
3041                title="Referral 9">
3042                10
3043              </div>
3044              
3045            </div>
3046
3047            <div class="dn-ref-status">
3048              <span class="dn-badge dn-badge-green" style="font-size:12px;padding:6px 14px;">
3049                Renewal Discount: 10% Active
3050              </span>
3051              
3052              <span class="dn-ref-next">9 more for a free renewal</span>
3053              
3054            </div>
3055
3056            
3057            <div style="font-size:12px;color:var(--bl-text-secondary);margin-bottom:14px;line-height:1.5;">
3058              This discount comes off <strong style="color:var(--bl-text-primary);">your next visa renewal</strong>, applied automatically — it doesn't change the price of your current application.
3059            </div>
3060
3061            
3062            <div class="dn-ref-link">
3063              <input type="text" id="dnRefLink" readonly
3064                value=""
3065                placeholder="Your personal link is on its way">
3066              
3067              <button type="button" id="dnRefCopyBtn" onclick="dnGenerateReferralLink()">Get my link</button>
3068              
3069            </div>
3070            <div id="dnRefHint" class="dn-ref-hint"></div>
3071
3072            <button type="button" class="dn-ref-rules-btn" onclick="openReferralRules()">
3073              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
3074                stroke-linecap="round" stroke-linejoin="round">
3075                <circle cx="12" cy="12" r="10" />
3076                <line x1="12" y1="16" x2="12" y2="12" />
3077                <line x1="12" y1="8" x2="12.01" y2="8" />
3078              </svg>
3079              How it works
3080            </button>
3081          </div>
3082        </div>
3083        
3084
3085        <!-- ===== Current Task Detail (right column) ===== -->
3086        <div id="current_task_card">
3087          
3088          <div class="ctc ctc--empty">
3089            <div class="ctc-empty-state">
3090              <div class="ctc-empty-icon">
3091                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
3092                  stroke-linejoin="round">
3093                  <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
3094                  <polyline points="22 4 12 14.01 9 11.01" />
3095                </svg>
3096              </div>
3097              <div class="ctc-empty-title">All tasks completed!</div>
3098              <div class="ctc-empty-sub">Check back for updates from your .</div>
3099            </div>
3100          </div>
3101          
3102
3103        </div>
3104
3105      </div> <!-- close dn-grid-2 -->
3106
3107
3108      <!-- ===== Task List (full width) ===== -->
3109
3110      <div class="dn-card" data-hideable="tasks" data-hide-label="Task list" data-tour="tasks">
3111        <div class="dn-card-header" style="flex-wrap:wrap;">
3112          <div>
3113            <div class="dn-card-title">All Tasks </div>
3114            <div class="dn-card-subtitle">Your visa process checklist</div>
3115          </div>
3116          
3117          
3118        </div>
3119
3120        <div class="dn-card-body">
3121
3122          
3123          
3124          
3125
3126          
3127          
3128
3129  </div>
3130  </div>
3131
3132
3133  <!-- ===== Documents Card ===== -->
3134
3135  <div class="dn-card" id="dnDocumentsCard" data-hideable="documents" data-hide-label="Documents"
3136    style="margin-bottom:20px;" data-tour="documents">
3137    <div class="dn-card-header">
3138      <div>
3139        <div class="dn-card-title">Documents</div>
3140        <div class="dn-card-subtitle">Upload your required documents and view files from your </div>
3141      </div>
3142    </div>
3143    <div class="dn-card-body" style="padding:0;">
3144      <!-- Tab headers -->
3145      <div style="display:flex;border-bottom:1px solid var(--bl-border);">
3146        <button class="dn-doc-tab active" onclick="switchDocTab('required', this)"
3147          style="flex:1;padding:10px 16px;border:none;background:none;font-size:12px;font-weight:600;color:var(--bl-primary);cursor:pointer;border-bottom:2px solid var(--bl-primary);transition:all 0.15s;">
3148          Required Documents 
3149        </button>
3150        <button class="dn-doc-tab" onclick="switchDocTab('uploads', this)"
3151          style="flex:1;padding:10px 16px;border:none;background:none;font-size:12px;font-weight:600;color:var(--bl-text-muted);cursor:pointer;border-bottom:2px solid transparent;transition:all 0.15s;">
3152          My Uploads <span style="font-weight:400;opacity:0.7;">(0)</span>
3153        </button>
3154        <button class="dn-doc-tab" onclick="switchDocTab('cm', this)"
3155          style="flex:1;padding:10px 16px;border:none;background:none;font-size:12px;font-weight:600;color:var(--bl-text-muted);cursor:pointer;border-bottom:2px solid transparent;transition:all 0.15s;">
3156          From  <span style="font-weight:400;opacity:0.7;">(0)</span>
3157        </button>
3158      </div>
3159
3160      <!-- Required Documents tab -->
3161      
3162      <div id="docTabRequired" class="dn-doc-panel" style="padding:16px;">
3163        
3164        <div style="text-align:center;padding:24px;color:var(--bl-text-muted);font-size:13px;">No documents requested
3165          yet.</div>
3166        
3167      </div>
3168
3169      <!-- My Uploads tab -->
3170      <div id="docTabUploads" class="dn-doc-panel" style="padding:16px;display:none;">
3171        
3172        <div style="text-align:center;padding:24px;color:var(--bl-text-muted);font-size:13px;">No documents uploaded
3173          yet.</div>
3174        
3175      </div>
3176
3177      <!-- CM documents tab -->
3178      <div id="docTabCm" class="dn-doc-panel" style="padding:16px;display:none;">
3179        
3180        <div style="text-align:center;padding:24px;color:var(--bl-text-muted);font-size:13px;">No files from your case
3181          manager yet.</div>
3182        
3183      </div>
3184    </div>
3185  </div>
3186
3187
3188  
3189  
3190  <div class="dn-card dn-animate dn-animate-delay-4" id="dnCaseManagerCard" style="margin-bottom:20px;">
3191        <div class="dn-card-header">
3192          <div>
3193            <div class="dn-card-title" style="display:flex;align-items:center;gap:8px;">
3194              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--bl-primary)" stroke-width="2"
3195                stroke-linecap="round" stroke-linejoin="round">
3196                <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
3197                <circle cx="9" cy="7" r="4" />
3198                <path d="M23 21v-2a4 4 0 0 0-3-3.87" />
3199                <path d="M16 3.13a4 4 0 0 1 0 7.75" />
3200              </svg>
3201              Your 
3202            </div>
3203            <div class="dn-card-subtitle">Message <strong></strong> directly</div>
3204          </div>
3205          
3206        </div>
3207
3208
3209        <div class="dn-card-body">
3210
3211          <!-- Tabs -->
3212          <div class="dn-cmt-tabs">
3213            <button type="button" class="dn-cmt-tab active" id="dnCmtTabMessages"
3214              onclick="dnCmTab('messages')">
3215              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
3216                stroke-linejoin="round">
3217                <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
3218              </svg>
3219              Messages
3220            </button>
3221            
3222          </div>
3223
3224          <!-- ── Messages pane (default) ── -->
3225          <div class="dn-cmt-pane active" id="dnCmtPaneMessages">
3226
3227            <div class="dn-msg-ai" id="dnMsgAiBanner">
3228              <div class="dn-msg-ai-ic">
3229                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2"
3230                  stroke-linecap="round" stroke-linejoin="round">
3231                  <path d="M12 2l1.9 4.6L18.5 8l-4.6 1.9L12 14l-1.9-4.1L5.5 8l4.6-1.4L12 2z" />
3232                  <path d="M5 16l.9 2.1L8 19l-2.1.9L5 22l-.9-2.1L2 19l2.1-.9L5 16z" />
3233                </svg>
3234              </div>
3235              <div class="dn-msg-ai-txt"><strong>Looking for a quick answer?</strong> Most questions are answered
3236                instantly by <strong>Carlos</strong>, your AI assistant. Use the messages below
3237                to reach your  for anything that needs a personal reply.</div>
3238              <button type="button" class="dn-msg-ai-hide" onclick="dnHideMsgAiBanner()" title="Hide this message">
3239                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
3240                  stroke-linecap="round" stroke-linejoin="round">
3241                  <line x1="18" y1="6" x2="6" y2="18" />
3242                  <line x1="6" y1="6" x2="18" y2="18" />
3243                </svg>
3244                Hide this message
3245              </button>
3246            </div>
3247            
3248            
3248<script>
3249              (function () {
3250                var KEY = 'dn_hide_msg_ai_banner_';
3251                var banner = document.getElementById('dnMsgAiBanner');
3252                try { if (banner && localStorage.getItem(KEY) === '1') banner.style.display = 'none'; } catch (e) {}
3253                window.dnHideMsgAiBanner = function () {
3254                  if (banner) banner.style.display = 'none';
3255                  try { localStorage.setItem(KEY, '1'); } catch (e) {}
3256                };
3257              })();
3258            </script>
3258
3259
3260            
3261            <div class="dn-msg-note">
3262              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
3263                stroke-linecap="round" stroke-linejoin="round">
3264                <circle cx="12" cy="12" r="10" />
3265                <polyline points="12 6 12 12 16 14" />
3266              </svg>
3267              <div>This is your direct line to your  &mdash; message them any time, and they can message
3268                you back here too. It isn't a live chat, so replies usually take <strong>2&ndash;3 working
3269                days</strong>.</div>
3270            </div>
3271
3272            <div class="dn-msg-ok" id="dnMsgOk">
3273              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
3274                stroke-linecap="round" stroke-linejoin="round">
3275                <polyline points="20 6 9 17 4 12" />
3276              </svg>
3277              <span>Your message has been sent. Your  will reply within 2&ndash;3 working days.</span>
3278            </div>
3279
3280            <form class="dn-msg-form" id="dnMsgForm" onsubmit="return false;">
3281              <div class="dn-msg-field">
3282                <label for="dnMsgSubject">What's it about? <span style="font-weight:400;opacity:.7;">(optional)</span></label>
3283                <input type="text" id="dnMsgSubject" maxlength="200"
3284                  placeholder="e.g. My documents, my appointment, a payment">
3285              </div>
3286              <div class="dn-msg-field">
3287                <label for="dnMsgInput">Your message to your </label>
3288                <textarea id="dnMsgInput" placeholder="Type your message here..."></textarea>
3289              </div>
3290              <div class="dn-msg-foot">
3291                <span class="dn-msg-hint" id="dnMsgHint">Please write at least 10 characters.</span>
3292                <button type="button" class="dn-cm-btn dn-cm-btn--primary" id="dnMsgSend" onclick="dnSendMessage()">
3293                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
3294                    stroke-linejoin="round">
3295                    <line x1="22" y1="2" x2="11" y2="13" />
3296                    <polygon points="22 2 15 22 11 13 2 9 22 2" />
3297                  </svg>
3298                  Send message
3299                </button>
3300              </div>
3301            </form>
3302
3303            <div class="dn-msg-sephead">
3304              <span class="dn-msg-sep">Conversation</span>
3305              
3306            </div>
3307            <div class="dn-msg-thread" id="dnMsgThread">
3308              
3309              <div class="dn-msg-empty">No messages yet &mdash; send your first message above.</div>
3310              
3311            </div>
3312            
3313
3314          </div><!-- /messages pane -->
3315
3316          
3317
3318        </div>
3319      </div>
3320
3321
3322  
3322<script>
3323    /* Jump from a task (card or modal) down to the Documents card. Kept outside
3324       the hideable cards' own script blocks so the task buttons keep working
3325       whichever cards this client has switched off. */
3326    function scrollToDocuments(tab) {
3327      var card = document.getElementById('dnDocumentsCard');
3328      if (!card) return;
3329
3330      // The card can be hidden by the client's own Hide control; bring it back
3331      // first, otherwise the scroll lands on an empty spot.
3332      if (card.style.display === 'none') {
3333        var showBtn = card.previousElementSibling;
3334        if (showBtn && showBtn.classList.contains('dn-show-btn')) showBtn.click();
3335      }
3336
3337      // The upload buttons live on the "Required Documents" tab, so select that
3338      // tab first — the client may have left the card on another one.
3339      var wanted = tab || 'required';
3340      var tabBtn = card.querySelectorAll('.dn-doc-tab')[{ required: 0, uploads: 1, cm: 2 }[wanted]];
3341      if (tabBtn && typeof switchDocTab === 'function') switchDocTab(wanted, tabBtn);
3342
3343      card.scrollIntoView({ behavior: 'smooth', block: 'start' });
3344      card.classList.add('dn-doc-flash');
3345      setTimeout(function () { card.classList.remove('dn-doc-flash'); }, 1800);
3346    }
3347
3348    // "Go to current task" in the Service Type card at the top of the page.
3349    function scrollToCurrentTask() {
3350      var wrap = document.getElementById('current_task_card');
3351      if (!wrap) return;
3352
3353      // The card itself is the .ctc inside, and the client can hide it — same
3354      // as the documents card. Bring it back first or the scroll lands on a
3355      // dashed "Show current task" button with nothing under it.
3356      var card = wrap.querySelector('.ctc') || wrap;
3357      if (card.style.display === 'none') {
3358        var showBtn = card.previousElementSibling;
3359        if (showBtn && showBtn.classList.contains('dn-show-btn')) showBtn.click();
3360      }
3361
3362      // Ask for smooth only when the visitor has not turned animation off. With
3363      // prefers-reduced-motion set, a smooth scrollIntoView can be dropped and
3364      // the page simply never moves — jumping straight there is the fallback.
3365      var still = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
3366      wrap.scrollIntoView({ behavior: still ? 'auto' : 'smooth', block: 'center' });
3367      card.classList.add('dn-doc-flash');
3368      setTimeout(function () { card.classList.remove('dn-doc-flash'); }, 1800);
3369    }
3370  </script>
3370
3371
3372  
3373  
3374  
3375
3376  
3377  
3377<script>
3378    (function () {
3379      var ai = document.getElementById('aiCard');
3380      var cm = document.getElementById('dnCaseManagerCard');
3381      if (ai && cm && ai.parentNode && ai.parentNode.classList.contains('dn-grid-2')) {
3382        cm.style.marginBottom = '0';   // grid gap handles spacing between columns
3383        ai.after(cm);
3384      }
3385    })();
3386  </script>
3386
3387
3388  <!-- ===== Activity + Services Row ===== -->
3389
3390  <div style="margin-bottom:20px;">
3391
3392    <!-- Activity Feed -->
3393    
3394
3395    <!-- Recommended Services -->
3396    <div>
3397      <div class="dn-card" style="margin-bottom:0;">
3398        <div class="dn-card-header">
3399          <div>
3400            <div class="dn-card-title">Recommended Services</div>
3401            <div class="dn-card-subtitle">Trusted partners for your move to Spain</div>
3402          </div>
3403        </div>
3404        <div class="dn-card-body">
3405
3406          
3407          <div class="dn-svc-filters">
3408            <button type="button" class="dn-svc-chip is-active" data-cat="all"
3409              onclick="filterServices('all', this)">All services</button>
3410            
3411          </div>
3412
3413          
3414          <div id="svcSubsetNote" style="display:none;align-items:center;gap:10px;flex-wrap:wrap;
3415                      margin:0 0 14px;padding:9px 13px;border-radius:9px;
3416                      background:var(--bl-primary-bg);border:1px solid rgba(27,132,255,0.22);
3417                      font-size:12.5px;color:var(--bl-text-primary);">
3418            <span>Showing the partners for this step.</span>
3419            <button type="button" onclick="filterServices('all', document.querySelector('.dn-svc-chip[data-cat=&quot;all&quot;]'))"
3420              style="background:none;border:none;padding:0;font:inherit;font-weight:600;
3421                     color:var(--bl-primary);cursor:pointer;text-decoration:underline;">
3422              Show all services
3423            </button>
3424          </div>
3425
3426          
3427
3428          <div class="dn-svc-disclaimer">
3429            <p>A few of the links above are affiliate links. This means that if you click on one and make a purchase,
3430              we may receive a commission at no additional cost to you. But more likely we have an agreements with the
3431              services where they refer clients needing help with their visa to us, and we recommend their services in
3432              return.</p>
3433            <p>We only recommend products and services that we have experience with and believe will add value to our
3434              clients. Our opinions and recommendations are based on personal experience and research, and we are
3435              committed to maintaining an unbiased perspective.</p>
3436          </div>
3437        </div>
3438      </div>
3439    </div>
3440
3441    <!-- Social Media -->
3442
3443    <div>
3444      <div class="dn-card" data-hideable="social" data-hide-label="Social media" style="margin-top:20px;margin-bottom:0;">
3445        <div class="dn-card-header">
3446          <div>
3447            <div class="dn-card-title">Follow us</div>
3448            <div class="dn-card-subtitle">Visa tips, client stories and life in Spain</div>
3449          </div>
3450        </div>
3451        <div class="dn-card-body">
3452          <div class="dn-social-grid">
3453            <div class="dn-social">
3454              <div class="dn-social-head">
3455                <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#e1306c" stroke-width="2"
3456                  stroke-linecap="round" stroke-linejoin="round">
3457                  <rect x="2" y="2" width="20" height="20" rx="5" ry="5" />
3458                  <path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" />
3459                  <line x1="17.5" y1="6.5" x2="17.51" y2="6.5" />
3460                </svg>
3461                Instagram
3462                <a href="https://www.instagram.com/bureaucracy.es/" target="_blank" rel="noopener"
3463                  class="dn-social-link">Visit ↗</a>
3464              </div>
3465              
3466              <div class="elfsight-app-db3db7cf-746f-4436-b839-16c28cedd8e0" data-elfsight-app-lazy
3467                style="min-height:320px;"></div>
3468            </div>
3469
3470            <div class="dn-social">
3471              <div class="dn-social-head">
3472                <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#ff0000" stroke-width="2"
3473                  stroke-linecap="round" stroke-linejoin="round">
3474                  <path d="M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 2A29 29 0 0 0 1 11.75a29 29 0 0 0 .46 5.33A2.78 2.78 0 0 0 3.4 19c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-2 29 29 0 0 0 .46-5.25 29 29 0 0 0-.46-5.33z" />
3475                  <polygon points="9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02" />
3476                </svg>
3477                YouTube
3478                <a href="https://www.youtube.com/@Bureaucracyes" target="_blank" rel="noopener"
3479                  class="dn-social-link">Visit ↗</a>
3480              </div>
3481              <div id="dnSocialMount" class="elfsight-app-c23c3219-14e6-4881-9f9a-6bea124b05e1"
3482                data-elfsight-app-lazy style="min-height:320px;"></div>
3483            </div>
3484          </div>
3485        </div>
3486      </div>
3487    </div>
3488    
3488<script>
3489      /* One platform.js serves both widgets, fetched the first time the card
3490         comes near the viewport rather than on every dashboard load. */
3491      (function () {
3492        var mount = document.getElementById('dnSocialMount');
3493        if (!mount) return;
3494
3495        var loaded = false;
3496        function loadElfsight() {
3497          if (loaded) return;
3498          loaded = true;
3499          var s = document.createElement('script');
3500          s.src = 'https://static.elfsight.com/platform/platform.js';
3501          s.async = true;
3502          document.body.appendChild(s);
3503        }
3504
3505        // Browsers without IntersectionObserver just get it straight away.
3506        if (!('IntersectionObserver' in window)) { loadElfsight(); return; }
3507
3508        var observer = new IntersectionObserver(function (entries) {
3509          entries.forEach(function (entry) {
3510            if (!entry.isIntersecting) return;
3511            observer.disconnect();
3512            loadElfsight();
3513          });
3514        }, { rootMargin: '400px' });
3515        observer.observe(mount);
3516      })();
3517    </script>
3517
3518
3519
3520  </div>
3521
3522  </div>
3523
3524  <!-- Footer -->
3525  <footer class="dn-footer">
3526    © 2026 Bureaucracy.es. All rights reserved. &nbsp;·&nbsp;
3527    <a href="javascript:void(0)" onclick="openBugModal()"
3528      style="color:var(--bl-primary);cursor:pointer;text-decoration:none;">
3529      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
3530        style="vertical-align:-1px;margin-right:3px;">
3531        <circle cx="12" cy="12" r="10" />
3532        <line x1="12" y1="8" x2="12" y2="12" />
3533        <line x1="12" y1="16" x2="12.01" y2="16" />
3534      </svg>Report a Bug
3535    </a>
3536    &nbsp;·&nbsp;
3537    <a href="javascript:void(0)" onclick="openTermsModal()"
3538      style="color:var(--bl-primary);cursor:pointer;text-decoration:none;">
3539      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
3540        style="vertical-align:-1px;margin-right:3px;">
3541        <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
3542        <polyline points="14 2 14 8 20 8" />
3543        <line x1="16" y1="13" x2="8" y2="13" />
3544        <line x1="16" y1="17" x2="8" y2="17" />
3545      </svg>Terms &amp; Conditions
3546    </a>
3547  </footer>
3548
3549  </div>
3550
3551  
3551<script>
3552    // ===== Avatar dropdown =====
3553    /* ===== Notifications =====
3554       The server renders only entries newer than the client's
3555       notifications_seen_at, so everything in the panel is unread by
3556       definition. Opening the bell tells the server (which also clears the
3557       badge on their other devices at the next load); on the next page load
3558       these entries no longer appear here — the Recent Activity card keeps
3559       the full history. Seen state used to live in localStorage, which
3560       followed the browser rather than the client. */
3561    function refreshNotifBadge() {
3562      var unread = 0;
3563      document.querySelectorAll('.dn-notif-item').forEach(function (item) {
3564        var isNew = item.dataset.seen !== '1';
3565        if (isNew) unread++;
3566        // A left accent and a faint wash rather than a dot in its own column:
3567        // the dot pushed every row's text out of line with the panel heading.
3568        item.style.borderLeftColor = isNew ? 'var(--bl-primary)' : 'transparent';
3569        item.style.background = isNew ? 'var(--bl-primary-bg)' : '';
3570      });
3571
3572      var badge = document.getElementById('notifCount');
3573      var pill = document.getElementById('notifNewPill');
3574      if (badge) {
3575        badge.textContent = unread > 9 ? '9+' : String(unread);
3576        badge.style.display = unread ? 'block' : 'none';
3577      }
3578      if (pill) pill.style.display = unread ? 'inline-block' : 'none';
3579    }
3580
3581    /* Opening the panel records the visit but deliberately does NOT re-colour
3582       the rows yet: greying them the instant they come into view takes away the
3583       one thing the client opened the panel to see. The count clears straight
3584       away, the dots settle on close. */
3585    var notifSeenPending = false;
3586
3587    function markNotificationsSeen() {
3588      document.querySelectorAll('.dn-notif-item').forEach(function (item) {
3589        item.dataset.seen = '1';
3590      });
3591      // Fire-and-forget: a failed write just means these show once more on the
3592      // next load, which is the harmless direction to fail in.
3593      fetch(`${window.origin}/dashboard/api/notifications-seen`, {
3594        method: 'POST', credentials: 'include',
3595        headers: { 'Content-Type': 'application/json' }, body: '{}'
3596      }).catch(function () {});
3597    }
3598
3599    function setNotifOpen(open) {
3600      var menu = document.getElementById('notifMenu');
3601      var btn = document.getElementById('notifBtn');
3602      if (!menu) return;
3603      menu.classList.toggle('open', open);
3604      if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
3605
3606      // On a phone the panel is position:fixed and anchored to the screen, so
3607      // it needs a real `top` — measured off the bell rather than hard-coded,
3608      // since the header is 65px tall at one breakpoint and 50px at another.
3609      // Cleared above that width so the desktop rule keeps its own position.
3610      if (btn) {
3611        var anchored = window.matchMedia('(max-width: 600px)').matches;
3612        menu.style.top = anchored ? (Math.round(btn.getBoundingClientRect().bottom) + 8) + 'px' : '';
3613      }
3614
3615      if (open) {
3616        var badge = document.getElementById('notifCount');
3617        if (badge) badge.style.display = 'none';
3618        markNotificationsSeen();
3619        notifSeenPending = true;
3620      } else if (notifSeenPending) {
3621        notifSeenPending = false;
3622        refreshNotifBadge();       // now the dots catch up
3623      }
3624    }
3625
3626    function toggleNotifications(e) {
3627      e.stopPropagation();
3628      var menu = document.getElementById('notifMenu');
3629      var opening = !menu.classList.contains('open');
3630      // Two panels open in the same corner is never what was wanted.
3631      closeAvatarDropdown();
3632      setNotifOpen(opening);
3633    }
3634
3635    function openActivityFromNotifications() {
3636      setNotifOpen(false);
3637      var card = document.querySelector('[data-hideable="activity"]');
3638      if (!card) return;
3639      // scrollIntoView is patched further down to open whichever section owns
3640      // the card first, so this works from any section.
3641      card.scrollIntoView({ behavior: 'smooth', block: 'center' });
3642    }
3643
3644    // Closing by clicking away or pressing Escape has to settle the dots too,
3645    // or they stay blue until the next full page load.
3646    document.addEventListener('click', function (e) {
3647      var menu = document.getElementById('notifMenu');
3648      if (menu && menu.classList.contains('open') && !e.target.closest('.dn-notif-wrap')) {
3649        setNotifOpen(false);
3650      }
3651    });
3652
3653    document.addEventListener('keydown', function (e) {
3654      if (e.key !== 'Escape') return;
3655      var menu = document.getElementById('notifMenu');
3656      if (menu && menu.classList.contains('open')) setNotifOpen(false);
3657    });
3658
3659    refreshNotifBadge();
3660
3661    function toggleAvatarDropdown(e) {
3662      e.stopPropagation();
3663      document.getElementById('avatarMenu').classList.toggle('open');
3664      // Same reasoning in reverse: opening this one dismisses notifications.
3665      var notif = document.getElementById('notifMenu');
3666      if (notif) notif.classList.remove('open');
3667    }
3668    function closeAvatarDropdown() {
3669      document.getElementById('avatarMenu').classList.remove('open');
3670    }
3671    document.addEventListener('click', function (e) {
3672      var menu = document.getElementById('avatarMenu');
3673      if (menu && !e.target.closest('.dn-avatar-dropdown')) {
3674        menu.classList.remove('open');
3675      }
3676    });
3677
3678    // ===== Document tabs =====
3679    function switchDocTab(tab, btn) {
3680      document.getElementById('docTabRequired').style.display = tab === 'required' ? 'block' : 'none';
3681      document.getElementById('docTabUploads').style.display = tab === 'uploads' ? 'block' : 'none';
3682      document.getElementById('docTabCm').style.display = tab === 'cm' ? 'block' : 'none';
3683      document.querySelectorAll('.dn-doc-tab').forEach(function (t) {
3684        t.style.color = 'var(--bl-text-muted)';
3685        t.style.borderBottomColor = 'transparent';
3686      });
3687      btn.style.color = 'var(--bl-primary)';
3688      btn.style.borderBottomColor = 'var(--bl-primary)';
3689    }
3690
3691    // ===== Upload document by type =====
3692    function uploadDocByType(input, docType) {
3693      if (!input.files.length) return;
3694      var label = input.closest('.dn-upload-btn');
3695      var origHTML = label.innerHTML;
3696      label.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="animation:spin 1s linear infinite"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> Uploading...';
3697      label.style.pointerEvents = 'none';
3698      label.style.opacity = '0.6';
3699
3700      var formData = new FormData();
3701      for (var i = 0; i < input.files.length; i++) {
3702        formData.append('files', input.files[i]);
3703      }
3704      var safeType = encodeURIComponent(docType);
3705      fetch('/dashboard/uploads/private-upload-tagged//' + safeType, {
3706        method: 'POST',
3707        credentials: 'include',
3708        body: formData
3709      }).then(function (resp) {
3710        if (resp.ok) {
3711          label.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg> Uploaded!';
3712          label.style.background = 'rgba(34,197,94,0.1)';
3713          label.style.color = '#22c55e';
3714          label.style.borderColor = '#22c55e';
3715          setTimeout(function () { location.reload(); }, 1200);
3716        } else {
3717          label.innerHTML = origHTML;
3718          label.style.pointerEvents = '';
3719          label.style.opacity = '';
3720          alert('Upload failed. Please try again.');
3721        }
3722      }).catch(function () {
3723        label.innerHTML = origHTML;
3724        label.style.pointerEvents = '';
3725        label.style.opacity = '';
3726        alert('Upload failed. Please try again.');
3727      });
3728    }
3729
3730    // ===== Document comment toggle =====
3731    function toggleDocComments(id) {
3732      var el = document.getElementById(id);
3733      if (!el) return;
3734      el.style.display = el.style.display === 'none' ? 'block' : 'none';
3735    }
3736
3737    // ===== Post document comment =====
3738    function postDocComment(clientUid, fileName, inputId, triggerEl) {
3739      var input = document.getElementById(inputId);
3740      if (!input || !input.value.trim()) return;
3741      var btn = triggerEl.tagName === 'BUTTON' ? triggerEl : triggerEl.closest('div').querySelector('button');
3742      var origText = btn.textContent;
3743      btn.textContent = '...';
3744      btn.disabled = true;
3745      fetch('/api/dashboard/add-doc-client-comment', {
3746        method: 'POST',
3747        credentials: 'include',
3748        headers: { 'Content-Type': 'application/json' },
3749        body: JSON.stringify({
3750          client_uid: clientUid,
3751          comment_text: input.value.trim(),
3752          file_name: fileName
3753        })
3754      }).then(function (resp) {
3755        if (resp.ok) {
3756          input.value = '';
3757          btn.textContent = 'Sent!';
3758          setTimeout(function () { location.reload(); }, 800);
3759        } else {
3760          btn.textContent = origText;
3761          btn.disabled = false;
3762        }
3763      }).catch(function () {
3764        btn.textContent = origText;
3765        btn.disabled = false;
3766      });
3767    }
3768
3769    // ===== Send message to case manager (async, not a chat) =====
3770    function dnSendMessage() {
3771      var subjectEl = document.getElementById('dnMsgSubject');
3772      var input = document.getElementById('dnMsgInput');
3773      var hint = document.getElementById('dnMsgHint');
3774      var btn = document.getElementById('dnMsgSend');
3775      var body = (input.value || '').trim();
3776      hint.style.display = 'none';
3777      if (body.length < 10) {
3778        hint.textContent = 'Please write at least 10 characters.';
3779        hint.style.display = 'inline';
3780        return;
3781      }
3782      var origHTML = btn.innerHTML;
3783      btn.disabled = true;
3784      btn.textContent = 'Sending...';
3785      fetch('/api/dashboard/messages/send', {
3786        method: 'POST',
3787        credentials: 'include',
3788        headers: { 'Content-Type': 'application/json' },
3789        body: JSON.stringify({ subject: subjectEl.value.trim(), body: body })
3790      }).then(function (resp) {
3791        if (resp.ok) {
3792          subjectEl.value = '';
3793          input.value = '';
3794          document.getElementById('dnMsgForm').style.display = 'none';
3795          document.getElementById('dnMsgOk').style.display = 'flex';
3796          setTimeout(function () { location.reload(); }, 1100);
3797        } else {
3798          btn.disabled = false;
3799          btn.innerHTML = origHTML;
3800          hint.textContent = 'Could not send. Please try again.';
3801          hint.style.display = 'inline';
3802        }
3803      }).catch(function () {
3804        btn.disabled = false;
3805        btn.innerHTML = origHTML;
3806        hint.textContent = 'Could not send. Please try again.';
3807        hint.style.display = 'inline';
3808      });
3809    }
3810
3811    // ===== Upload untagged (Other) document =====
3812    // The "Other" zone takes files from the picker and from a drag, so the
3813    // upload itself is separate from where the files came from.
3814    function uploadDocUntagged(input) {
3815      uploadOtherFiles(input.files);
3816    }
3817
3818    function uploadOtherFiles(files) {
3819      var zone = document.getElementById('dnOtherDrop');
3820      var sub = document.getElementById('dnOtherSub');
3821      if (!files || !files.length || !zone || !sub) return;
3822
3823      var origHTML = sub.innerHTML;
3824      zone.classList.remove('is--over');
3825      zone.style.pointerEvents = 'none';
3826      zone.style.opacity = '0.7';
3827      sub.textContent = 'Uploading ' + files.length +
3828        (files.length === 1 ? ' file…' : ' files…');
3829
3830      var formData = new FormData();
3831      for (var i = 0; i < files.length; i++) {
3832        formData.append('files', files[i]);
3833      }
3834      fetch('/dashboard/uploads/private-upload/', {
3835        method: 'POST',
3836        credentials: 'include',
3837        body: formData
3838      }).then(function (resp) {
3839        if (resp.ok) {
3840          sub.textContent = 'Uploaded — thank you!';
3841          zone.style.borderColor = '#22c55e';
3842          setTimeout(function () { location.reload(); }, 1200);
3843        } else {
3844          restoreOtherZone(zone, sub, origHTML);
3845          alert('Upload failed. Please try again.');
3846        }
3847      }).catch(function () {
3848        restoreOtherZone(zone, sub, origHTML);
3849        alert('Upload failed. Please try again.');
3850      });
3851    }
3852
3853    function restoreOtherZone(zone, sub, html) {
3854      sub.innerHTML = html;
3855      zone.style.pointerEvents = '';
3856      zone.style.opacity = '';
3857      zone.style.borderColor = '';
3858    }
3859
3860    // Dropping onto the zone. Dragging anywhere else on the page keeps the
3861    // browser's own behaviour, which is to do nothing.
3862    (function () {
3863      var zone = document.getElementById('dnOtherDrop');
3864      if (!zone) return;
3865      ['dragenter', 'dragover'].forEach(function (evt) {
3866        zone.addEventListener(evt, function (e) {
3867          e.preventDefault();
3868          e.stopPropagation();
3869          zone.classList.add('is--over');
3870        });
3871      });
3872      // dragleave fires when the pointer crosses onto a child element too, so
3873      // the highlight is only dropped once the cursor has really left.
3874      zone.addEventListener('dragleave', function (e) {
3875        if (!zone.contains(e.relatedTarget)) zone.classList.remove('is--over');
3876      });
3877      zone.addEventListener('drop', function (e) {
3878        e.preventDefault();
3879        e.stopPropagation();
3880        zone.classList.remove('is--over');
3881        uploadOtherFiles(e.dataTransfer && e.dataTransfer.files);
3882      });
3883    })();
3884
3885    // ===== Inline delete confirmation =====
3886    function showDeleteConfirm(rowId) {
3887      var row = document.getElementById(rowId);
3888      if (!row) return;
3889      row.querySelector('.dn-upload-content').style.display = 'none';
3890      row.querySelector('.dn-delete-confirm').style.display = 'flex';
3891    }
3892
3893    function hideDeleteConfirm(rowId) {
3894      var row = document.getElementById(rowId);
3895      if (!row) return;
3896      row.querySelector('.dn-upload-content').style.display = '';
3897      row.querySelector('.dn-delete-confirm').style.display = 'none';
3898    }
3899
3900    function deleteUploadedDoc(fileName, rowId) {
3901      var row = document.getElementById(rowId);
3902      var confirmBar = row ? row.querySelector('.dn-delete-confirm') : null;
3903      if (confirmBar) {
3904        confirmBar.innerHTML = '<span style="font-size:11px;color:var(--bl-text-muted);">Deleting...</span>';
3905      }
3906      fetch('/dashboard/api/obj_delete_user_file', {
3907        method: 'POST',
3908        credentials: 'include',
3909        headers: { 'Content-Type': 'application/json' },
3910        body: JSON.stringify({ file_name: fileName })
3911      }).then(function (resp) {
3912        if (resp.ok) {
3913          if (row) {
3914            row.style.transition = 'all 0.3s';
3915            row.style.maxHeight = '0';
3916            row.style.opacity = '0';
3917            row.style.overflow = 'hidden';
3918            row.style.padding = '0';
3919            row.style.margin = '0';
3920            setTimeout(function () { row.remove(); }, 350);
3921          }
3922        } else {
3923          hideDeleteConfirm(rowId);
3924          alert('Delete failed. Please try again.');
3925        }
3926      }).catch(function () {
3927        hideDeleteConfirm(rowId);
3928        alert('Delete failed. Please try again.');
3929      });
3930    }
3931
3932    // ===== Timeline actions =====
3933    function tlMarkInsurancePaid() {
3934      fetch(window.origin + '/workspace/api/timeline', {
3935        method: 'POST',
3936        credentials: 'include',
3937        headers: { 'content-type': 'application/json' },
3938        body: JSON.stringify({
3939          to_update: 'time_line_health_insurance_paid',
3940          to_update_value: true,
3941          uid: ''
3942        })
3943      }).then(function (r) { if (r.ok) location.reload(); });
3944    }
3945
3946    // ===== Translation quote: "I have not paid this" =====
3947    // The question replaces the button inside the quote it is about, rather
3948    // than opening a dialog that covers it.
3949    function tqAskUnpaid(btn) {
3950      var box = btn.closest('.dn-tq-unpaid');
3951      var owed = box.dataset.owed;
3952      box.innerHTML =
3953        '<div style="font-size:11.5px;line-height:1.6;color:var(--bl-text-secondary);">' +
3954        '<div style="margin-bottom:6px;">Mark this translation as not paid? ' +
3955        'It will show €' + owed + ' to pay again and your case manager will be told.</div>' +
3956        '<button type="button" class="tq-yes" style="background:none;border:none;padding:0;cursor:pointer;' +
3957        'font-size:11.5px;font-weight:700;color:#c2410c;text-decoration:underline;">Yes, it is not paid</button>' +
3958        ' &middot; <button type="button" class="tq-no" style="background:none;border:none;padding:0;cursor:pointer;' +
3959        'font-size:11.5px;font-weight:600;color:var(--bl-text-secondary);text-decoration:underline;">Cancel</button>' +
3960        '</div>';
3961      box.querySelector('.tq-no').onclick = function () { tqResetUnpaid(box); };
3962      box.querySelector('.tq-yes').onclick = function () { tqSendUnpaid(box); };
3963    }
3964
3965    function tqResetUnpaid(box) {
3966      box.innerHTML =
3967        '<button type="button" onclick="tqAskUnpaid(this)" style="background:none;border:none;padding:0;' +
3968        'cursor:pointer;font-size:11.5px;font-weight:600;color:var(--bl-text-secondary);' +
3969        'text-decoration:underline;">Mark as unpaid</button>';
3970    }
3971
3972    function tqSendUnpaid(box) {
3973      box.innerHTML = '<span style="font-size:11.5px;color:var(--bl-text-secondary);">Saving…</span>';
3974      fetch(window.origin + '/dashboard/api/translation-unpaid/' + box.dataset.quote, {
3975        method: 'POST',
3976        credentials: 'include',
3977        headers: { 'content-type': 'application/json' },
3978        body: JSON.stringify({})
3979      }).then(function (r) { return r.json().catch(function () { return {}; }); })
3980        .then(function (d) {
3981          // Reload, so the badge, the amount and the payment button all agree.
3982          if (d && d.ok) { location.reload(); return; }
3983          box.innerHTML = '<span style="font-size:11.5px;color:#c2410c;">' +
3984            ((d && d.error) || 'That did not work. Please message your case manager.') + '</span>';
3985          setTimeout(function () { tqResetUnpaid(box); }, 6000);
3986        }).catch(function () {
3987          box.innerHTML = '<span style="font-size:11.5px;color:#c2410c;">
3987' +
3988            'Could not reach the server.</span>';
3989          setTimeout(function () { tqResetUnpaid(box); }, 6000);
3990        });
3991    }
3992
3993    function tlSaveApptDate() {
3994      var date = document.getElementById('tl_appt_date').value;
3995      if (!date) { alert('Please enter your appointment date.'); return; }
3996      fetch(window.origin + '/workspace/api/timeline', {
3997        method: 'POST',
3998        credentials: 'include',
3999        headers: { 'content-type': 'application/json' },
4000        body: JSON.stringify({
4001          to_update: 'time_line_selected_appointment_date',
4002          to_update_value: date,
4003          uid: ''
4004        })
4005      }).then(function (r) { if (r.ok) location.reload(); });
4006    }
4007
4008    // ===== Task section toggles =====
4009    function toggleSection(header) {
4010      var section = header.closest('.dn-task-section');
4011      section.classList.toggle('open');
4012    }
4013    function toggleTask(item) {
4014      item.classList.toggle('expanded');
4015    }
4016
4017    // ===== Carlos (AI assistant) =====
4018    const aiMessages = document.getElementById('aiMessages');
4019    const aiChatForm = document.getElementById('aiChatForm');
4020    const aiChatInput = document.getElementById('aiChatInput');
4021    const aiSendBtn = document.getElementById('aiSendBtn');
4022
4023    // Carlos works from the client's case, full stop — that is covered by the
4024    // Privacy Policy they accept at signup, so there is no per-client consent
4025    // flag to read, no screen to answer and no switch to flip.
4026    const aiContextData = null;
4027    let aiMsgHistory = [];
4028    let aiInitDone = false;
4029
4030    // Shared by initAiChat (which sends it) and the history replay (which must
4031    // recognise it to avoid drawing it back as a client-typed message).
4032    const AI_KICKOFF_OPENER = `Hi there! I'm looking for help with my Spain visa and relocation process.`;
4033
4034    function buildAiSystemPrompt() {
4035      if (aiContextData) {
4036        return `You are Carlos, the AI assistant for Bureaucracy. You are assisting a client named ${aiContextData.name}. The client's context data is as follows: ${JSON.stringify(aiContextData)}. Use this context dynamically when answering questions.`;
4037      }
4038      return `You are Carlos, the AI assistant for Bureaucracy. You assist clients with general Spain visa and relocation questions. You do NOT have access to their personal data, so kindly ask for details if they ask something specific to their case.`;
4039    }
4040
4041    function initAiChat() {
4042      if (aiInitDone) return;
4043      aiInitDone = true;
4044
4045      aiMsgHistory = [{ role: 'system', content: buildAiSystemPrompt() }];
4046
4047      let initMsg = AI_KICKOFF_OPENER;
4048      if (aiContextData) {
4049        initMsg += ` I believe you have my case details, so feel free to check what I need to work on next.`;
4050      }
4051
4052      aiMsgHistory.push({ role: 'user', content: initMsg });
4053
4054      // Show loader
4055      const loaderId = addAiLoader(aiContextData ? 'Loading your case data...' : 'Starting assistant...');
4056
4057      (async function () {
4058        try {
4059          const response = await fetch('/dashboard/api/carlos/chat', {
4060            method: 'POST',
4061            headers: { 'Content-Type': 'application/json' },
4062            // is_init keeps this scripted opener out of the stored transcript
4063            // and out of the client's daily question allowance.
4064            body: JSON.stringify({ messages: aiMsgHistory, is_init: true })
4065          });
4066          const reader = response.body.getReader();
4067          const decoder = new TextDecoder();
4068          let buffer = '';
4069          let fullReply = '';
4070
4071          while (true) {
4072            const { done, value } = await reader.read();
4073            if (done) break;
4074            buffer += decoder.decode(value, { stream: true });
4075            const lines = buffer.split('\n');
4076            buffer = lines.pop();
4077            for (const line of lines) {
4078              if (!line.startsWith('data: ')) continue;
4079              try {
4080                const chunk = JSON.parse(line.slice(6));
4081                if (chunk.content) fullReply += chunk.content;
4082              } catch (e) { }
4083            }
4084          }
4085
4086          removeAiLoader(loaderId);
4087          if (fullReply) {
4088            aiMsgHistory.push({ role: 'assistant', content: fullReply });
4089            addAiMessageUI('bot', fullReply);
4090          }
4091        } catch (e) {
4092          removeAiLoader(loaderId);
4093          addAiMessageUI('bot', 'Could not connect. Please try refreshing.');
4094        }
4095      })();
4096    }
4097
4098    // Send messages
4099    if (aiChatForm) {
4100      aiChatInput.addEventListener('input', function () {
4101        this.style.height = 'auto';
4102        this.style.height = this.scrollHeight + 'px';
4103        aiSendBtn.disabled = !this.value.trim();
4104      });
4105
4106      aiChatInput.addEventListener('keydown', function (e) {
4107        if (e.key === 'Enter' && !e.shiftKey) {
4108          e.preventDefault();
4109          if (this.value.trim()) aiChatForm.dispatchEvent(new Event('submit'));
4110        }
4111      });
4112
4113      aiChatForm.addEventListener('submit', async function (e) {
4114        e.preventDefault();
4115        const text = aiChatInput.value.trim();
4116        if (!text) return;
4117
4118        addAiMessageUI('user', text);
4119        aiMsgHistory.push({ role: 'user', content: text });
4120
4121        aiChatInput.value = '';
4122        aiChatInput.style.height = 'auto';
4123        aiChatInput.disabled = true;
4124        aiSendBtn.disabled = true;
4125
4126        // Create bot bubble with typing indicator
4127        const botDiv = document.createElement('div');
4128        botDiv.className = 'dn-ai-msg dn-ai-msg-bot';
4129        botDiv.innerHTML = `<div class="dn-ai-msg-avatar">
4129<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#b752ff" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg></div><div class="dn-ai-msg-bubble"><div class="dn-ai-typing"><span></span><span></span><span></span></div></div>`;
4130        aiMessages.appendChild(botDiv);
4131        aiMessages.scrollTop = aiMessages.scrollHeight;
4132        const bubble = botDiv.querySelector('.dn-ai-msg-bubble');
4133
4134        let fullReply = '';
4135        let streamStarted = false;
4136
4137        try {
4138          const response = await fetch('/dashboard/api/carlos/chat', {
4139            method: 'POST',
4140            headers: { 'Content-Type': 'application/json' },
4141            body: JSON.stringify({ messages: aiMsgHistory })
4142          });
4143
4144          const reader = response.body.getReader();
4145          const decoder = new TextDecoder();
4146          let buffer = '';
4147
4148          while (true) {
4149            const { done, value } = await reader.read();
4150            if (done) break;
4151
4152            buffer += decoder.decode(value, { stream: true });
4153            const lines = buffer.split('\n');
4154            buffer = lines.pop();
4155
4156            for (const line of lines) {
4157              if (!line.startsWith('data: ')) continue;
4158              try {
4159                const evt = JSON.parse(line.slice(6));
4160
4161                if (evt.error) {
4162                  bubble.innerHTML = 'Sorry, an error occurred. Please try again.';
4163                  aiMsgHistory.pop();
4164                  return;
4165                }
4166
4167                if (evt.limit_reached) {
4168                  bubble.style.background = '#fff7ed';
4169                  bubble.style.border = '1px solid #fdba74';
4170                  bubble.style.color = '#9a3412';
4171                }
4172
4173                if (evt.content) {
4174                  if (!streamStarted) {
4175                    bubble.innerHTML = '';
4176                    streamStarted = true;
4177                  }
4178                  fullReply += evt.content;
4179                  bubble.innerHTML = carlosMd(fullReply);
4180                  aiMessages.scrollTop = aiMessages.scrollHeight;
4181                }
4182
4183                if (evt.done && fullReply) {
4184                  bubble.innerHTML = carlosMd(fullReply);
4185                }
4186              } catch (e) { }
4187            }
4188          }
4189
4190          if (fullReply) {
4191            aiMsgHistory.push({ role: 'assistant', content: fullReply });
4192          } else {
4193            bubble.innerHTML = 'Sorry, an error occurred. Please try again.';
4194            aiMsgHistory.pop();
4195          }
4196        } catch (err) {
4197          bubble.innerHTML = 'Network error. Please check your connection.';
4198          aiMsgHistory.pop();
4199        } finally {
4200          aiChatInput.disabled = false;
4201          aiSendBtn.disabled = false;
4202          aiChatInput.focus();
4203        }
4204      });
4205    }
4206
4207    function addAiMessageUI(type, text) {
4208      const div = document.createElement('div');
4209      div.className = 'dn-ai-msg dn-ai-msg-' + type;
4210      const formatted = carlosMd(text);
4211      if (type === 'bot') {
4212        div.innerHTML = `<div class="dn-ai-msg-avatar"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#b752ff" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg></div><div class="dn-ai-msg-bubble">${formatted}</div>`;
4213      } else {
4214        div.innerHTML = `<div class="dn-ai-msg-bubble">${escapeAiHTML(text)}</div>`;
4215      }
4216      aiMessages.appendChild(div);
4217      aiMessages.scrollTop = aiMessages.scrollHeight;
4218    }
4219
4220    function addAiLoader(label) {
4221      const id = 'ai-loader-' + Date.now();
4222      const div = document.createElement('div');
4223      div.className = 'dn-ai-msg dn-ai-msg-bot';
4224      div.id = id;
4225      div.innerHTML = `<div class="dn-ai-msg-avatar"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#b752ff" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg></div><div class="dn-ai-msg-bubble" style="display:flex;align-items:center;gap:10px;"><div class="dn-ai-typing"><span></span><span></span><span></span></div><span style="font-size:12px;color:var(--bl-text-muted);">${label || 'Thinking...'}</span></div>`;
4226      aiMessages.appendChild(div);
4227      aiMessages.scrollTop = aiMessages.scrollHeight;
4228      return id;
4229    }
4230
4231    function removeAiLoader(id) {
4232      const el = document.getElementById(id);
4233      if (el) el.remove();
4234    }
4235
4236    function escapeAiHTML(str) {
4237      return str.replace(/[&<>'"]/g, t => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": "&#39;", '"': '&quot;' }[t]));
4238    }
4239
4240    // Replay a stored turn. carlosMd escapes its input before rebuilding the
4241    // markdown subset, so database round-tripped text is never trusted just
4242    // because the agent originally produced it.
4243    function addAiHistoryMessageUI(type, text) {
4244      const div = document.createElement('div');
4245      div.className = 'dn-ai-msg dn-ai-msg-' + type;
4246      const safe = carlosMd(text);
4247      if (type === 'bot') {
4248        div.innerHTML = `<div class="dn-ai-msg-avatar">
4248<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#b752ff" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg></div><div class="dn-ai-msg-bubble">${safe}</div>`;
4249      } else {
4250        div.innerHTML = `<div class="dn-ai-msg-bubble">${escapeAiHTML(text)}</div>`;
4251      }
4252      aiMessages.appendChild(div);
4253      aiMessages.scrollTop = aiMessages.scrollHeight;
4254    }
4255
4256    // Every turn is already persisted server-side and shown to staff, but the
4257    // client used to get a blank chat on each load — and a re-billed greeting
4258    // round-trip with it. Restore the conversation instead.
4259    async function bootstrapAiChat() {
4260      if (aiInitDone) return;
4261
4262      let past = [];
4263      try {
4264        const response = await fetch('/dashboard/api/carlos/history');
4265        if (response.ok) {
4266          const data = await response.json();
4267          past = (data && data.messages) || [];
4268        }
4269      } catch (err) {
4270        past = [];  // a history failure must never leave the client without a chat
4271      }
4272
4273      // The fetch is async, so the client may have typed in the meantime. Any
4274      // bubble on screen means the normal flow already owns the conversation.
4275      if (!past.length || aiMessages.children.length || aiInitDone) {
4276        initAiChat();
4277        return;
4278      }
4279
4280      aiInitDone = true;
4281      aiMsgHistory = [{ role: 'system', content: buildAiSystemPrompt() }];
4282
4283      for (let i = 0; i < past.length; i++) {
4284        const msg = past[i];
4285        if (!msg || typeof msg.content !== 'string' || !msg.content) continue;
4286        const role = msg.role === 'assistant' ? 'assistant' : 'user';
4287        // The kick-off line is generated by initAiChat, not typed by the client,
4288        // and was never drawn as a bubble. It is persisted though, so replaying
4289        // it verbatim would show the client a message they never wrote. Keep it
4290        // in the model's history for continuity, just don't render it.
4291        const isKickOff = role === 'user' && msg.content.indexOf(AI_KICKOFF_OPENER) === 0;
4292        if (!isKickOff) {
4293          addAiHistoryMessageUI(role === 'assistant' ? 'bot' : 'user', msg.content);
4294        }
4295        aiMsgHistory.push({ role: role, content: msg.content });
4296      }
4297    }
4298
4299    // Auto-trigger on page load
4300    document.addEventListener('DOMContentLoaded', function () {
4301      bootstrapAiChat();
4302    });
4303  </script>
4303
4304
4305
4306      
4306<script>
4307        /* Hide / show a card, the behaviour the Payments card has always had: the
4308           client's choice lives in their own browser, not on their record. Any card
4309           marked data-hideable gets a Hide control in its header and a dashed
4310           "Show …" button in its place. */
4311        (function () {
4312          var UID = '';
4313          var EYE_OFF = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" ' +
4314            'stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
4315            '<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 ' +
4316            '9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>' +
4317            '<line x1="1" y1="1" x2="23" y2="23"/></svg>';
4318          var EYE = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" ' +
4319            'stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
4320            '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
4321
4322          /* Every card on the page, not a hand-maintained list — a card added later
4323             gets its Hide control for free. Cards inside modals are skipped, as are
4324             any marked data-no-hide. */
4325          function keyFor(card, title) {
4326            var explicit = card.getAttribute('data-hideable');
4327            if (explicit) return explicit;
4328            // Titles can carry a live count ("All Tasks 85%"), so strip anything
4329            // that is not a letter — otherwise the key would change and the
4330            // client's choice would be forgotten.
4331            return title.toLowerCase().replace(/[^a-z ]/g, '').trim().replace(/\s+/g, '_');
4332          }
4333
4334          document.querySelectorAll('.dn-card, .ctc').forEach(function (card) {
4335            if (card.hasAttribute('data-no-hide')) return;
4336            if (card.closest('.dn-modal, .dn-modal-overlay')) return;
4337            if (card.id === 'dnPaymentsCard') return;   // has its own control already
4338
4339            var titleEl = card.querySelector('.dn-card-title, .ctc-title');
4340            if (!titleEl) return;
4341            var title = titleEl.textContent.replace(/\s+/g, ' ').trim();
4342            var key = keyFor(card, title);
4343            var label = (card.getAttribute('data-hide-label') ||
4344                         title.replace(/[^A-Za-z &]/g, '').trim() || 'this card').toLowerCase();
4345            var storageKey = 'dn_hide_' + key + '_' + UID;
4346            var header = card.querySelector('.dn-card-header, .ctc-header');
4347            if (!header) return;
4348
4349            var hideBtn = document.createElement('button');
4350            hideBtn.type = 'button';
4351            hideBtn.title = 'Hide ' + label;
4352            hideBtn.className = 'dn-hide-btn';
4353            hideBtn.setAttribute('aria-label', 'Hide ' + label);
4354            hideBtn.innerHTML = EYE_OFF;
4355
4356            /* Everything after the title — badges, counts — joins the icon in one
4357               right-hand group, so the header reads title | (badge, hide). */
4358            if (header.classList.contains('dn-card-header')) {
4359              var actions = header.querySelector('.dn-card-actions');
4360              if (!actions) {
4361                actions = document.createElement('div');
4362                actions.className = 'dn-card-actions';
4363                while (header.children.length > 1) actions.appendChild(header.children[1]);
4364                header.appendChild(actions);
4365              }
4366              actions.appendChild(hideBtn);
4367            } else {
4368              hideBtn.style.marginLeft = 'auto';
4369              header.appendChild(hideBtn);
4370            }
4371
4372            var showBtn = document.createElement('button');
4373            showBtn.type = 'button';
4374            showBtn.className = 'dn-show-btn';
4375            showBtn.innerHTML = EYE + ' Show ' + label;
4376            card.parentNode.insertBefore(showBtn, card);
4377
4378            function hide(remember) {
4379              card.style.display = 'none';
4380              showBtn.style.display = 'flex';
4381              if (remember) { try { localStorage.setItem(storageKey, '1'); } catch (e) {} }
4382            }
4383            function show() {
4384              card.style.display = '';
4385              showBtn.style.display = 'none';
4386              try { localStorage.removeItem(storageKey); } catch (e) {}
4387            }
4388            hideBtn.addEventListener('click', function () { hide(true); });
4389            showBtn.addEventListener('click', show);
4390            try { if (localStorage.getItem(storageKey) === '1') hide(false); } catch (e) {}
4391          });
4392        })();
4393      </script>
4393
4394
4395      <style>
4396        /* Hiding a card is a secondary action: an icon that stays out of the way
4397           until the header is hovered, not a labelled button competing with the
4398           card's own title. */
4399        .dn-hide-btn {
4400          background: none; border: none; cursor: pointer; padding: 5px;
4401          border-radius: 6px; line-height: 0;
4402          color: var(--bl-text-muted); opacity: .35;
4403          transition: opacity .15s ease, background .15s ease, color .15s ease;
4404        }
4405        .dn-card-header:hover .dn-hide-btn { opacity: .75; }
4406        .dn-hide-btn:hover { opacity: 1; color: var(--bl-text-secondary); background: var(--bl-bg); }
4407        .dn-hide-btn:focus-visible { opacity: 1; outline: 2px solid var(--bl-primary); outline-offset: 1px; }
4408
4409        .dn-card-actions { display: flex; align-items: center; gap: 10px; margin-left: auto; }
4410
4411        /* The payments header runs three lines deep (title, total, subtitle).
4412           The card header centres its children, which left the hide control
4413           floating halfway down — it belongs in the corner. */
4414        #dnPaymentsHideBtn { align-self: flex-start; }
4415
4416        .dn-show-btn {
4417          display: none; width: 100%; margin-bottom: 20px; padding: 9px 14px;
4418          background: none; border: 1px dashed var(--bl-border); border-radius: var(--bl-radius-lg);
4419          cursor: pointer; color: var(--bl-text-muted); font-size: 12px; font-weight: 600;
4420          align-items: center; justify-content: center; gap: 7px;
4421          transition: color .15s ease, border-color .15s ease, background .15s ease;
4422        }
4423        .dn-show-btn:hover { color: var(--bl-text-secondary); border-color: var(--bl-text-muted);
4424                             background: var(--bl-bg); }
4425      </style>
4426
4427  <!-- ===== Profile Edit Modal ===== -->
4428  <style>
4429    .dn-modal-overlay {
4430      position: fixed;
4431      top: 0;
4432      left: 0;
4433      width: 100%;
4434      height: 100%;
4435      background: rgba(0, 0, 0, 0.4);
4436      backdrop-filter: blur(4px);
4437      z-index: 9999;
4438      display: flex;
4439      align-items: center;
4440      justify-content: center;
4441      padding: 20px;
4442      /* A dialog taller than the screen has to go somewhere. Without this the
4443         overflow was simply unreachable — nothing scrolled on a phone. */
4444      overflow-y: auto;
4445      -webkit-overflow-scrolling: touch;
4446      overscroll-behavior: contain;
4447    }
4448
4449    .dn-modal {
4450      background: var(--bl-bg-card);
4451      border-radius: 16px;
4452      width: 100%;
4453      max-width: 480px;
4454      box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
4455      animation: modalIn 0.25s ease;
4456      /* Never taller than the viewport; the body scrolls inside instead.
4457         dvh follows the mobile address bar, vh is the fallback. */
4458      max-height: calc(100vh - 40px);
4459      max-height: calc(100dvh - 40px);
4460      display: flex;
4461      flex-direction: column;
4462      /* When it does exceed the overlay, centring would clip the top off. */
4463      margin: auto;
4464    }
4465
4466    /* min-height:0 so this can shrink below its content. Without it a flex item
4467       will not give way, and the body ends up clipped at the dialog's edge with
4468       no scroll of its own — which is what iOS was showing. */
4469    .dn-modal > .dn-modal-body {
4470      overflow-y: auto;
4471      -webkit-overflow-scrolling: touch;
4472      min-height: 0;
4473    }
4474    .dn-modal > .dn-modal-header,
4475    .dn-modal > .dn-modal-footer { flex-shrink: 0; }
4476
4477    @media (max-width: 575px) {
4478      .dn-modal-overlay { padding: 12px; align-items: flex-start; }
4479      /* On a phone the dialog is not capped at all and the overlay does the
4480         scrolling. Capping it meant a scroller inside a scroller, which reads
4481         fine in a desktop browser but is exactly the arrangement iOS Safari
4482         would clip without letting a touch reach the inner one. One scroll
4483         container, the outer one, has nothing to go wrong. */
4484      .dn-modal { max-height: none; }
4485      .dn-modal > .dn-modal-body { overflow-y: visible; }
4486    }
4487
4488    @keyframes modalIn {
4489      from {
4490        opacity: 0;
4491        transform: scale(0.95) translateY(10px);
4492      }
4493
4494      to {
4495        opacity: 1;
4496        transform: scale(1) translateY(0);
4497      }
4498    }
4499
4500    .dn-modal-header {
4501      display: flex;
4502      align-items: center;
4503      justify-content: space-between;
4504      padding: 20px 24px;
4505      border-bottom: 1px solid var(--bl-border);
4506    }
4507
4508    .dn-modal-close {
4509      width: 32px;
4510      height: 32px;
4511      border-radius: 8px;
4512      border: none;
4513      background: var(--bl-bg);
4514      cursor: pointer;
4515      font-size: 20px;
4516      color: var(--bl-text-secondary);
4517      display: flex;
4518      align-items: center;
4519      justify-content: center;
4520      transition: all 0.15s;
4521    }
4522
4523    .dn-modal-close:hover {
4524      background: var(--bl-border);
4525      color: var(--bl-text-primary);
4526    }
4527
4528    .dn-modal-body {
4529      padding: 24px;
4530    }
4531
4532    .dn-modal-footer {
4533      display: flex;
4534      justify-content: flex-end;
4535      gap: 10px;
4536      padding: 16px 24px;
4537      border-top: 1px solid var(--bl-border);
4538    }
4539
4540    .dn-modal-form {
4541      display: flex;
4542      flex-direction: column;
4543      gap: 16px;
4544    }
4545
4546    .dn-modal-field label {
4547      display: block;
4548      font-size: 12px;
4549      font-weight: 600;
4550      color: var(--bl-text-secondary);
4551      margin-bottom: 6px;
4552      text-transform: uppercase;
4553      letter-spacing: 0.5px;
4554    }
4555
4556    .dn-modal-field input {
4557      width: 100%;
4558      padding: 10px 14px;
4559      border-radius: 10px;
4560      border: 1px solid var(--bl-border);
4561      background: var(--bl-bg);
4562      font-size: 14px;
4563      color: var(--bl-text-primary);
4564      outline: none;
4565      transition: border-color 0.2s;
4566      font-family: inherit;
4567      box-sizing: border-box;
4568    }
4569
4570    .dn-modal-field input:focus {
4571      border-color: var(--bl-primary);
4572      box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
4573    }
4574
4575    /* Opt-in sitting under its field. Its own rules for the box and the text,
4576       since the field defaults above are written for full-width text inputs. */
4577    .dn-modal-field label.dn-profile-check {
4578      display: flex;
4579      align-items: center;
4580      gap: 8px;
4581      margin: 8px 0 0;
4582      font-size: 12.5px;
4583      font-weight: 500;
4584      text-transform: none;
4585      letter-spacing: 0;
4586      color: var(--bl-text-secondary);
4587      cursor: pointer;
4588    }
4589
4590    .dn-modal-field label.dn-profile-check input[type="checkbox"] {
4591      width: 15px;
4592      height: 15px;
4593      padding: 0;
4594      margin: 0;
4595      flex-shrink: 0;
4596      accent-color: var(--bl-primary);
4597      cursor: pointer;
4598    }
4599
4600    .dn-profile-error {
4601      margin-top: 16px;
4602      padding: 10px 14px;
4603      border-radius: 10px;
4604      border: 1px solid rgba(239, 68, 68, 0.25);
4605      background: rgba(239, 68, 68, 0.08);
4606      color: #b91c1c;
4607      font-size: 12.5px;
4608      line-height: 1.5;
4609    }
4610
4611    .dn-modal-btn-secondary {
4612      padding: 9px 18px;
4613      border-radius: 10px;
4614      border: 1px solid var(--bl-border);
4615      background: var(--bl-bg-card);
4616      color: var(--bl-text-secondary);
4617      font-size: 13px;
4618      font-weight: 600;
4619      cursor: pointer;
4620      transition: all 0.15s;
4621    }
4622
4623    .dn-modal-btn-secondary:hover {
4624      background: var(--bl-bg);
4625    }
4626
4627    .dn-modal-btn-primary {
4628      padding: 9px 22px;
4629      border-radius: 10px;
4630      border: none;
4631      background: var(--bl-primary);
4632      color: white;
4633      font-size: 13px;
4634      font-weight: 600;
4635      cursor: pointer;
4636      transition: all 0.15s;
4637    }
4638
4639    .dn-modal-btn-primary:hover {
4640      opacity: 0.9;
4641    }
4642
4643    .dn-modal-btn-primary:disabled {
4644      opacity: 0.6;
4645      cursor: not-allowed;
4646    }
4647  </style>
4648
4649  <!-- ===== Private Notes Modal ===== -->
4650  <style>
4651    .dn-notes-toolbar {
4652      display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
4653      padding: 6px; margin-bottom: 10px;
4654      border: 1px solid var(--bl-border); border-radius: 10px; background: var(--bl-bg);
4655    }
4656    .dn-notes-tool {
4657      min-width: 32px; height: 32px; padding: 0 8px;
4658      display: inline-flex; align-items: center; justify-content: center;
4659      border: none; border-radius: 7px; background: transparent; cursor: pointer;
4660      color: var(--bl-text-secondary); font-size: 14px; transition: background 0.15s, color 0.15s;
4661    }
4662    .dn-notes-tool:hover { background: var(--bl-border); color: var(--bl-text-primary); }
4663    .dn-notes-sep { width: 1px; height: 20px; background: var(--bl-border); margin: 0 4px; }
4664
4665    .dn-notes-editor {
4666      min-height: 240px; max-height: 45vh; overflow-y: auto;
4667      padding: 14px 16px;
4668      border: 1px solid var(--bl-border); border-radius: 10px; background: var(--bl-bg);
4669      font-size: 14px; line-height: 1.6; color: var(--bl-text-primary);
4670      outline: none; transition: border-color 0.2s;
4671    }
4672    .dn-notes-editor:focus { border-color: var(--bl-primary); }
4673    /* contenteditable leaves stray markup behind, so emptiness is decided in JS
4674       and flagged with a class rather than with :empty. */
4675    .dn-notes-editor.is-empty::before {
4676      content: attr(data-placeholder); color: var(--bl-text-muted); pointer-events: none;
4677    }
4678    /* The dashboard strips list markers globally; a list the client made with
4679       the toolbar has to show them or the button looks broken. */
4680    .dn-notes-editor ul, .dn-notes-editor ol { padding-left: 22px; margin: 6px 0; }
4681    .dn-notes-editor ul { list-style: disc outside; }
4682    .dn-notes-editor ol { list-style: decimal outside; }
4683    .dn-notes-editor li { display: list-item; }
4684    .dn-notes-editor p { margin: 0 0 8px; }
4685  </style>
4686
4687  <div id="notesModal" class="dn-modal-overlay" style="display:none;">
4688    <div class="dn-modal" style="max-width:640px;">
4689      <div class="dn-modal-header">
4690        <div>
4691          <div class="dn-card-title">My notes</div>
4692          <div style="font-size:12px;color:var(--bl-text-muted);margin-top:3px;">
4693            Just for you &mdash; your case manager doesn't see this.
4694          </div>
4695        </div>
4696        <button onclick="closeNotesModal()" class="dn-modal-close" aria-label="Close notes">&times;</button>
4697      </div>
4698      <div class="dn-modal-body">
4699        <div class="dn-notes-toolbar" role="toolbar" aria-label="Formatting">
4700          <button type="button" class="dn-notes-tool" data-cmd="bold" title="Bold (Ctrl+B)"
4701            style="font-weight:800;">B</button>
4702          <button type="button" class="dn-notes-tool" data-cmd="italic" title="Italic (Ctrl+I)"
4703            style="font-style:italic;font-family:Georgia,serif;">I</button>
4704          <button type="button" class="dn-notes-tool" data-cmd="underline" title="Underline (Ctrl+U)"
4705            style="text-decoration:underline;">U</button>
4706          <span class="dn-notes-sep"></span>
4707          <button type="button" class="dn-notes-tool" data-cmd="insertUnorderedList" title="Bulleted list">
4708            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
4709              stroke-linecap="round"><line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/>
4710              <line x1="9" y1="18" x2="20" y2="18"/><circle cx="4.5" cy="6" r="1.4" fill="currentColor" stroke="none"/>
4711              <circle cx="4.5" cy="12" r="1.4" fill="currentColor" stroke="none"/>
4712              <circle cx="4.5" cy="18" r="1.4" fill="currentColor" stroke="none"/></svg>
4713          </button>
4714          <button type="button" class="dn-notes-tool" data-cmd="insertOrderedList" title="Numbered list">
4715            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
4716              stroke-linecap="round"><line x1="10" y1="6" x2="20" y2="6"/><line x1="10" y1="12" x2="20" y2="12"/>
4717              <line x1="10" y1="18" x2="20" y2="18"/><text x="2" y="8" font-size="7" fill="currentColor"
4718              stroke="none">1</text><text x="2" y="14.5" font-size="7" fill="currentColor" stroke="none">2</text>
4719              <text x="2" y="21" font-size="7" fill="currentColor" stroke="none">3</text></svg>
4720          </button>
4721          <span class="dn-notes-sep"></span>
4722          <button type="button" class="dn-notes-tool" data-cmd="removeFormat" title="Clear formatting">
4723            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
4724              stroke-linecap="round"><path d="M6 4h14M9 20h7M15 4 9 20"/><line x1="4" y1="4" x2="20" y2="20"/></svg>
4725          </button>
4726        </div>
4727
4728        
4729        <div id="notesEditor" class="dn-notes-editor" contenteditable="true" spellcheck="true"
4730          autocorrect="on" autocapitalize="sentences" role="textbox" aria-multiline="true"
4731          aria-label="Your private notes"
4732          data-placeholder="Questions for your case manager, reference numbers, reminders…"></div>
4733      </div>
4734      <div class="dn-modal-footer" style="justify-content:space-between;align-items:center;">
4735        <span id="notesStatus" style="font-size:12px;color:var(--bl-text-muted);"></span>
4736        <div style="display:flex;gap:10px;">
4737          <button onclick="closeNotesModal()" class="dn-modal-btn-secondary">Close</button>
4738          <button onclick="saveNote(true)" id="notesSaveBtn" class="dn-modal-btn-primary">Save</button>
4739        </div>
4740      </div>
4741    </div>
4742  </div>
4743
4744  
4744<script>
4745    (function () {
4746      var modal   = document.getElementById('notesModal');
4747      var editor  = document.getElementById('notesEditor');
4748      var status  = document.getElementById('notesStatus');
4749      var dot     = document.getElementById('notesDot');
4750      var saveBtn = document.getElementById('notesSaveBtn');
4751      if (!modal || !editor) return;
4752
4753      var saveTimer = null;
4754      var dirty = false;
4755      var lastSaved = editor.innerHTML;
4756
4757      function isBlank() {
4758        return !(editor.textContent || '').replace(/ /g, ' ').trim();
4759      }
4760
4761      function refreshEmptyState() {
4762        editor.classList.toggle('is-empty', isBlank());
4763      }
4764
4765      function setStatus(text) { status.textContent = text || ''; }
4766
4767      window.saveNote = function (explicit) {
4768        clearTimeout(saveTimer);
4769        var html = editor.innerHTML;
4770        if (!dirty && !explicit) return Promise.resolve();
4771        if (html === lastSaved && !explicit) { dirty = false; return Promise.resolve(); }
4772
4773        setStatus('Saving…');
4774        if (explicit) saveBtn.disabled = true;
4775
4776        return fetch('/dashboard/api/notes', {
4777          method: 'POST',
4778          headers: { 'Content-Type': 'application/json' },
4779          body: JSON.stringify({ note: html })
4780        })
4781          .then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); })
4782          .then(function (res) {
4783            lastSaved = html;
4784            dirty = false;
4785            setStatus('Saved');
4786            if (dot) dot.style.display = res.empty ? 'none' : 'block';
4787            setTimeout(function () { if (!dirty) setStatus(''); }, 2500);
4788          })
4789          .catch(function () {
4790            // Never clear the editor on failure — the text is the client's only copy.
4791            setStatus('Could not save — your text is still here, try again.');
4792          })
4793          .finally(function () { saveBtn.disabled = false; });
4794      };
4795
4796      window.openNotesModal = function () {
4797        modal.style.display = 'flex';
4798        document.body.style.overflow = 'hidden';
4799        refreshEmptyState();
4800        editor.focus();
4801        // Put the caret at the end rather than the start of an existing note.
4802        var range = document.createRange();
4803        range.selectNodeContents(editor);
4804        range.collapse(false);
4805        var sel = window.getSelection();
4806        sel.removeAllRanges();
4807        sel.addRange(range);
4808      };
4809
4810      window.closeNotesModal = function () {
4811        var finish = function () {
4812          modal.style.display = 'none';
4813          document.body.style.overflow = '';
4814          setStatus('');
4815        };
4816        if (dirty) { saveNote(false).then(finish, finish); } else { finish(); }
4817      };
4818
4819      // Autosave once typing pauses, so a closed laptop doesn't lose the note.
4820      editor.addEventListener('input', function () {
4821        dirty = true;
4822        refreshEmptyState();
4823        setStatus('');
4824        clearTimeout(saveTimer);
4825        saveTimer = setTimeout(function () { saveNote(false); }, 1200);
4826      });
4827
4828      // Paste as plain text: pasted markup would be stripped server-side anyway,
4829      // and this keeps what the client sees identical to what gets stored.
4830      editor.addEventListener('paste', function (e) {
4831        e.preventDefault();
4832        var text = (e.clipboardData || window.clipboardData).getData('text/plain');
4833        document.execCommand('insertText', false, text);
4834      });
4835
4836      // mousedown, not click: the default would move focus out of the editor and
4837      // drop the selection before the command could apply to it.
4838      document.querySelectorAll('#notesModal .dn-notes-tool').forEach(function (btn) {
4839        btn.addEventListener('mousedown', function (e) {
4840          e.preventDefault();
4841          document.execCommand(btn.getAttribute('data-cmd'), false, null);
4842          editor.focus();
4843          dirty = true;
4844          refreshEmptyState();
4845          clearTimeout(saveTimer);
4846          saveTimer = setTimeout(function () { saveNote(false); }, 1200);
4847        });
4848      });
4849
4850      modal.addEventListener('click', function (e) { if (e.target === modal) closeNotesModal(); });
4851      document.addEventListener('keydown', function (e) {
4852        if (e.key === 'Escape' && modal.style.display === 'flex') closeNotesModal();
4853      });
4854      // A close or refresh mid-sentence still keeps the note.
4855      window.addEventListener('beforeunload', function () {
4856        if (!dirty) return;
4857        var blob = new Blob([JSON.stringify({ note: editor.innerHTML })], { type: 'application/json' });
4858        navigator.sendBeacon('/dashboard/api/notes', blob);
4859      });
4860
4861      refreshEmptyState();
4862    })();
4863  </script>
4863
4864
4865  <div id="profileModal" class="dn-modal-overlay" style="display:none;">
4866    <div class="dn-modal">
4867      <div class="dn-modal-header">
4868        <div class="dn-card-title">Edit Profile</div>
4869        <button onclick="closeProfileModal()" class="dn-modal-close">&times;</button>
4870      </div>
4871      <div class="dn-modal-body">
4872        <!-- Profile Picture -->
4873        <div style="text-align:center;margin-bottom:24px;">
4874          <div style="position:relative;display:inline-block;">
4875            
4876            <div id="modalProfileImg"
4877              style="width:80px;height:80px;border-radius:50%;background:rgba(183,82,255,0.08);display:flex;align-items:center;justify-content:center;">
4878              <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="rgba(183,82,255,0.4)"
4879                stroke-width="1.5">
4880                <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
4881                <circle cx="12" cy="7" r="4" />
4882              </svg>
4883            </div>
4884            
4885            <label for="modalProfilePicInput"
4886              style="position:absolute;bottom:0;right:0;width:26px;height:26px;border-radius:50%;background:var(--bl-primary);display:flex;align-items:center;justify-content:center;cursor:pointer;border:2px solid white;">
4887              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5">
4888                <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
4889                <circle cx="12" cy="13" r="4" />
4890              </svg>
4891            </label>
4892          </div>
4893          <input type="file" id="modalProfilePicInput" name="profile_picture" accept="image/*"
4894            onchange="previewProfilePic(this)" style="display:none;">
4895          <div style="font-size:11px;color:var(--bl-text-muted);margin-top:8px;">Click the camera icon to change photo
4896          </div>
4897        </div>
4898
4899        <!-- Form Fields -->
4900        <div class="dn-modal-form">
4901          <div class="dn-modal-field">
4902            <label for="profileName">Full Name</label>
4903            <input type="text" id="profileName" value="" placeholder="Enter your full name">
4904          </div>
4905          <div class="dn-modal-field">
4906            <label for="profileLocation">Location</label>
4907            <input type="text" id="profileLocation" value="" placeholder="e.g. Madrid, Spain">
4908          </div>
4909          <div class="dn-modal-field">
4910            <label for="profilePhone">Phone Number</label>
4911            <input type="tel" id="profilePhone" value="" placeholder="+34 600 000 000">
4912          </div>
4913          
4914          
4915          <div class="dn-modal-field">
4916            <label for="profileContactEmail">Secondary Email</label>
4917            <input type="email" id="profileContactEmail" value=""
4918              placeholder="[email protected]">
4919            <label class="dn-profile-check">
4920              <input type="checkbox" id="profileContactEmailLogin"
4921                
4922                disabled>
4923              <span>Also let me log in with this email</span>
4924            </label>
4925          </div>
4926          <div class="dn-modal-field">
4927            <label>Login Email</label>
4928            <input type="email" value="" disabled style="opacity:0.6;cursor:not-allowed;">
4929          </div>
4930        </div>
4931        <div id="profileError" class="dn-profile-error" style="display:none;"></div>
4932      </div>
4933      <div class="dn-modal-footer">
4934        <button onclick="closeProfileModal()" class="dn-modal-btn-secondary">Cancel</button>
4935        <button onclick="saveProfile()" id="profileSaveBtn" class="dn-modal-btn-primary">Save Changes</button>
4936      </div>
4937    </div>
4938  </div>
4939
4940  
4940<script>
4941    function openProfileModal() {
4942      document.getElementById('profileModal').style.display = 'flex';
4943      document.body.style.overflow = 'hidden';
4944    }
4945    function closeProfileModal() {
4946      document.getElementById('profileModal').style.display = 'none';
4947      document.body.style.overflow = '';
4948    }
4949    document.getElementById('profileModal').addEventListener('click', function (e) {
4950      if (e.target === this) closeProfileModal();
4951    });
4952    document.addEventListener('keydown', function (e) {
4953      if (e.key === 'Escape' && document.getElementById('profileModal').style.display === 'flex') closeProfileModal();
4954    });
4955
4956    // Nothing to log in with until there is an address, so the opt-in follows
4957    // the field instead of being tickable on its own.
4958    (function () {
4959      var emailInput = document.getElementById('profileContactEmail');
4960      var loginCheck = document.getElementById('profileContactEmailLogin');
4961      emailInput.addEventListener('input', function () {
4962        var empty = !emailInput.value.trim();
4963        loginCheck.disabled = empty;
4964        if (empty) loginCheck.checked = false;
4965      });
4966    })();
4967
4968    function previewProfilePic(input) {
4969      if (input.files && input.files[0]) {
4970        const reader = new FileReader();
4971        reader.onload = function (e) {
4972          const el = document.getElementById('modalProfileImg');
4973          if (el.tagName === 'IMG') {
4974            el.src = e.target.result;
4975          } else {
4976            const img = document.createElement('img');
4977            img.id = 'modalProfileImg';
4978            img.src = e.target.result;
4979            img.style.cssText = 'width:80px;height:80px;border-radius:50%;object-fit:cover;border:3px solid rgba(183,82,255,0.15);';
4980            el.parentNode.replaceChild(img, el);
4981          }
4982        };
4983        reader.readAsDataURL(input.files[0]);
4984      }
4985    }
4986
4987    function saveProfile() {
4988      const btn = document.getElementById('profileSaveBtn');
4989      const errBox = document.getElementById('profileError');
4990      errBox.style.display = 'none';
4991      btn.disabled = true;
4992      btn.textContent = 'Saving...';
4993
4994      // The secondary email doubles as a login, so the server may refuse it —
4995      // show why instead of the usual "Saved!".
4996      const showError = function (msg) {
4997        errBox.textContent = msg;
4998        errBox.style.display = 'block';
4999        btn.disabled = false;
5000        btn.textContent = 'Save Changes';
5001        btn.style.background = '';
5002      };
5003
5004      fetch('/api/dashboard/settings-personal-details', {
5005        method: 'POST',
5006        headers: { 'Content-Type': 'application/json' },
5007        body: JSON.stringify({
5008          name: document.getElementById('profileName').value,
5009          location: document.getElementById('profileLocation').value,
5010          phone: document.getElementById('profilePhone').value,
5011          contactemail: document.getElementById('profileContactEmail').value,
5012          contactemail_login: document.getElementById('profileContactEmailLogin').checked,
5013          address_bm: '',
5014          zipcode_bm: '',
5015          city_bm: '',
5016          country_bm: '',
5017          address_am: '',
5018          zipcode_am: '',
5019          city_am: '',
5020          country_am: '',
5021          applicant_names: []
5022        })
5023      }).then(r => r.json().then(data => ({ ok: r.ok, data: data || {} })))
5024          .then(res => {
5025            if (!res.ok) {
5026              showError(res.data.error || 'We could not save your details. Please try again.');
5027              return null;
5028            }
5029            const picInput = document.getElementById('modalProfilePicInput');
5030            if (picInput.files && picInput.files[0]) {
5031              const formData = new FormData();
5032              formData.append('profile_picture', picInput.files[0]);
5033              return fetch('/api/dashboard/settings-profile-picture', {
5034                method: 'POST',
5035                body: formData
5036              }).then(r => r.json());
5037            }
5038            return {};
5039          }).then(res => {
5040            if (res === null) return;
5041            btn.textContent = 'Saved!';
5042            btn.style.background = '#22c55e';
5043            setTimeout(() => { location.reload(); }, 800);
5044          }).catch(() => {
5045            btn.textContent = 'Error';
5046            btn.disabled = false;
5047            setTimeout(() => { btn.textContent = 'Save Changes'; btn.style.background = ''; }, 2000);
5048          });
5049    }
5050  </script>
5050
5051
5052  <!-- ===== Referral Program Rules Modal ===== -->
5053  <div id="referralRulesModal" class="dn-modal-overlay" style="display:none;">
5054    <div class="dn-modal" style="max-width:560px;">
5055      <div class="dn-modal-header">
5056        <div>
5057          <div
5058            style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--bl-primary);margin-bottom:2px;">
5059            Referral Program</div>
5060          <div style="font-size:16px;font-weight:700;color:var(--bl-text-primary);">How it works</div>
5061        </div>
5062        <button onclick="closeReferralRules()" class="dn-modal-close">&times;</button>
5063      </div>
5064      <div class="dn-modal-body" style="max-height:70vh;overflow-y:auto;">
5065
5066        
5067        <div class="dn-rules-list">
5068          <div class="dn-rules-item">
5069            <div class="dn-rules-num">1</div>
5070            <div>
5071              <div class="dn-rules-title">Share your link</div>
5072              Send your personal link to friends or family. It gives them <strong>10% off</strong> their visa.
5073            </div>
5074          </div>
5075          <div class="dn-rules-item">
5076            <div class="dn-rules-num">2</div>
5077            <div>
5078              <div class="dn-rules-title">They start their visa</div>
5079              A referral counts once the person you referred has made their first payment.
5080            </div>
5081          </div>
5082          <div class="dn-rules-item">
5083            <div class="dn-rules-num">3</div>
5084            <div>
5085              <div class="dn-rules-title">You earn 10%</div>
5086              Every successful referral takes <strong>10% off your next renewal</strong> — and the first 10% is on us.
5087              At ten referrals that renewal is <strong>free</strong>.
5088            </div>
5089          </div>
5090        </div>
5091
5092        
5093        <div class="dn-rules-fine">
5094          <div class="dn-rules-fine-head">Good to know</div>
5095          <ul>
5096            <li>Further referrals beyond ten do not increase that renewal's discount.</li>
5097            <li>Only the main applicant can use the renewal discount — dependants are not eligible.</li>
5098            <li>Self-referrals are not allowed, and fraudulent activity voids the reward.</li>
5099            <li>These rewards are for personal visa renewals. For commercial arrangements, see our
5100              <a href="https://www.bureaucracy.es/#b2b" target="_blank" rel="noopener">Business Solutions Affiliate
5101                Program</a>.
5102            </li>
5103          </ul>
5104        </div>
5105      </div>
5106      <div class="dn-modal-footer" style="justify-content:flex-end;">
5107        <button onclick="closeReferralRules()" class="dn-modal-btn-secondary">Close</button>
5108      </div>
5109    </div>
5110  </div>
5111  
5111<script>
5112    function openReferralRules() {
5113      document.getElementById('referralRulesModal').style.display = 'flex';
5114      document.body.style.overflow = 'hidden';
5115    }
5116    function closeReferralRules() {
5117      document.getElementById('referralRulesModal').style.display = 'none';
5118      document.body.style.overflow = '';
5119    }
5120    document.getElementById('referralRulesModal').addEventListener('click', function (e) {
5121      if (e.target === this) closeReferralRules();
5122    });
5123    document.addEventListener('keydown', function (e) {
5124      var m = document.getElementById('referralRulesModal');
5125      if (e.key === 'Escape' && m && m.style.display === 'flex') closeReferralRules();
5126    });
5127
5128    function dnRefHint(message, ok) {
5129      var hint = document.getElementById('dnRefHint');
5130      if (!hint) return;
5131      hint.textContent = message;
5132      hint.style.color = ok ? '#16a34a' : '#dc2626';
5133      hint.style.display = 'block';
5134    }
5135
5136    function dnCopyReferralLink() {
5137      var input = document.getElementById('dnRefLink');
5138      if (!input || !input.value) return;
5139
5140      var done = function () { dnRefHint('Link copied to your clipboard.', true); };
5141      // navigator.clipboard needs a secure context; on anything else fall back
5142      // to selecting the field so the client can still copy it by hand.
5143      if (navigator.clipboard && navigator.clipboard.writeText) {
5144        navigator.clipboard.writeText(input.value).then(done).catch(function () {
5145          input.select();
5146          dnRefHint('Press Ctrl/Cmd + C to copy your link.', true);
5147        });
5148      } else {
5149        input.select();
5150        dnRefHint('Press Ctrl/Cmd + C to copy your link.', true);
5151      }
5152    }
5153
5154    
5155    function dnGenerateReferralLink() {
5156      var btn = document.getElementById('dnRefCopyBtn');
5157      var input = document.getElementById('dnRefLink');
5158      if (!btn || !input) return;
5159      btn.disabled = true;
5160      btn.textContent = 'Creating…';
5161
5162      fetch('/dashboard/api/generate-referral-link', {
5163        method: 'POST',
5164        credentials: 'include',
5165        headers: { 'Content-Type': 'application/json' },
5166        body: JSON.stringify({ uid: '' })
5167      })
5168      .then(function (resp) { return resp.json().then(function (d) { return { ok: resp.ok, d: d }; }); })
5169      .then(function (result) {
5170        if (!result.ok || !result.d.domain_slug) throw new Error('no slug');
5171        input.value = 'bureaucracy.es/' + result.d.domain_slug;
5172        btn.disabled = false;
5173        btn.textContent = 'Copy';
5174        btn.setAttribute('onclick', 'dnCopyReferralLink()');
5175        btn.onclick = dnCopyReferralLink;
5176        dnCopyReferralLink();
5177      })
5178      .catch(function () {
5179        btn.disabled = false;
5180        btn.textContent = 'Get my link';
5181        dnRefHint('Could not create your link. Please try again.', false);
5182      });
5183    }
5184  </script>
5184
5185
5186  <!-- ===== Visa Appointment Modal ===== -->
5187  
5188  
5189  
5190  <div id="visaApptModal" class="dn-modal-overlay" style="display:none;">
5191    <div class="dn-modal" style="max-width:600px;">
5192      <div class="dn-modal-header">
5193        <div>
5194          <div
5195            style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--bl-primary);margin-bottom:2px;">
5196            Visa Appointment</div>
5197          <div style="font-size:16px;font-weight:700;color:var(--bl-text-primary);">Your appointment details</div>
5198        </div>
5199        <button onclick="closeVisaApptModal()" class="dn-modal-close">&times;</button>
5200      </div>
5201      <div class="dn-modal-body" style="max-height:70vh;overflow-y:auto;">
5202
5203        
5204        
5205
5206        
5207        <div style="font-size:12.5px;color:var(--bl-text-secondary);line-height:1.6;margin-bottom:16px;">
5208          You book your visa appointment yourself. Once you have it — or if it is later moved — put the date, time
5209          and location here and attach the confirmation, so your  can
5210          have everything ready in time.
5211        </div>
5212
5213        <div class="dn-modal-form">
5214          <div style="display:flex;gap:12px;flex-wrap:wrap;">
5215            <div class="dn-modal-field" style="flex:1;min-width:150px;">
5216              <label for="visaApptDate">Appointment Date</label>
5217              <input type="date" id="visaApptDate" value="">
5218            </div>
5219            <div class="dn-modal-field" style="flex:1;min-width:120px;">
5220              <label for="visaApptTime">Appointment Time</label>
5221              <input type="time" id="visaApptTime" value="">
5222            </div>
5223          </div>
5224          <div class="dn-modal-field">
5225            <label for="visaApptLocation">Location (consulate / office / city)</label>
5226            <input type="text" id="visaApptLocation" maxlength="200" value=""
5227              placeholder="e.g. Consulate of Spain, London">
5228          </div>
5229        </div>
5230
5231        <div id="visaApptHint"
5232          style="display:none;margin-top:10px;font-size:12.5px;font-weight:600;color:#dc2626;"></div>
5233
5234        
5235
5236        
5237        <div style="margin-top:24px;padding-top:20px;border-top:1px solid var(--bl-border);">
5238          <div
5239            style="font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--bl-text-muted);margin-bottom:10px;">
5240            Appointment confirmation</div>
5241
5242          
5243          
5244          
5245
5246          
5247          <div style="font-size:12.5px;color:var(--bl-text-muted);margin-bottom:12px;">No confirmation uploaded yet.</div>
5248          
5249
5250          <label class="dn-upload-btn" style="padding:9px 16px;font-size:12.5px;">
5251            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
5252              stroke-linecap="round" stroke-linejoin="round">
5253              <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
5254              <polyline points="17 8 12 3 7 8" />
5255              <line x1="12" y1="3" x2="12" y2="15" />
5256            </svg>
5257            Upload Appointment Confirmation
5258            <input type="file" multiple style="display:none;" onchange="uploadApptConfirmation(this)"
5259              accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,.heic">
5260          </label>
5261          <div id="visaApptUploadMsg" style="display:none;margin-top:10px;font-size:12.5px;font-weight:600;"></div>
5262        </div>
5263
5264      </div>
5265      <div class="dn-modal-footer">
5266        <button onclick="closeVisaApptModal()" class="dn-modal-btn-secondary">Close</button>
5267        <button onclick="saveVisaAppt()" id="visaApptSaveBtn" class="dn-modal-btn-primary">Save Details</button>
5268      </div>
5269    </div>
5270  </div>
5271
5272  
5272<script>
5273    // Renewal reminder opt-in/out, shown alongside the "Leave a Review" button
5274    // on the current-task widget when action_type is "reviews".
5275    function dn_renewal_plan(yes_no) {
5276      var yes = yes_no === 'yes';
5277      var yesBtn = document.getElementById('dnRenewalYesBtn');
5278      var noBtn = document.getElementById('dnRenewalNoBtn');
5279      var inactiveBg = 'var(--bl-bg)', inactiveColor = 'var(--bl-text-secondary)', inactiveShadow = 'inset 0 0 0 1px var(--bl-border)';
5280      if (yesBtn) {
5281        yesBtn.style.background = yes ? 'linear-gradient(135deg,#22c55e,#16a34a)' : inactiveBg;
5282        yesBtn.style.color = yes ? '#fff' : inactiveColor;
5283        yesBtn.style.boxShadow = yes ? 'none' : inactiveShadow;
5284      }
5285      if (noBtn) {
5286        noBtn.style.background = !yes ? 'linear-gradient(135deg,#ef4444,#dc2626)' : inactiveBg;
5287        noBtn.style.color = !yes ? '#fff' : inactiveColor;
5288        noBtn.style.boxShadow = !yes ? 'none' : inactiveShadow;
5289      }
5290
5291      fetch(window.origin + '/api/dashboard/renewal', {
5292        method: 'POST',
5293        credentials: 'include',
5294        body: JSON.stringify({ renewal_contact: yes }),
5295        cache: 'no-cache',
5296        headers: { 'content-type': 'application/json' }
5297      }).catch(function () {
5298        alert('Could not save your preference. Please try again.');
5299      });
5300    }
5301  </script>
5301
5302
5303  
5303<script>
5304    function openVisaApptModal() {
5305      document.getElementById('visaApptModal').style.display = 'flex';
5306      document.body.style.overflow = 'hidden';
5307    }
5308    function closeVisaApptModal() {
5309      document.getElementById('visaApptModal').style.display = 'none';
5310      document.body.style.overflow = '';
5311    }
5312    document.getElementById('visaApptModal').addEventListener('click', function (e) {
5313      if (e.target === this) closeVisaApptModal();
5314    });
5315    document.addEventListener('keydown', function (e) {
5316      var m = document.getElementById('visaApptModal');
5317      if (e.key === 'Escape' && m && m.style.display === 'flex') closeVisaApptModal();
5318    });
5319
5320    function saveVisaAppt() {
5321      var btn = document.getElementById('visaApptSaveBtn');
5322      var hint = document.getElementById('visaApptHint');
5323      var date = document.getElementById('visaApptDate').value;
5324      var time = document.getElementById('visaApptTime').value;
5325      var location = document.getElementById('visaApptLocation').value.trim();
5326
5327      hint.style.display = 'none';
5328      if (!date && !time && !location) {
5329        hint.textContent = 'Fill in at least one field before saving.';
5330        hint.style.display = 'block';
5331        return;
5332      }
5333
5334      var origText = btn.textContent;
5335      btn.disabled = true;
5336      btn.textContent = 'Saving...';
5337      fetch('/api/dashboard/visa-appointment', {
5338        method: 'POST',
5339        credentials: 'include',
5340        headers: { 'Content-Type': 'application/json' },
5341        body: JSON.stringify({ date: date, time: time, location: location })
5342      }).then(function (resp) {
5343        if (!resp.ok) throw new Error('save failed');
5344        btn.textContent = 'Saved!';
5345        btn.style.background = '#22c55e';
5346        // Reload so the task card, the activity feed and this form all show the
5347        // saved values rather than a stale page.
5348        setTimeout(function () { location.reload(); }, 900);
5349      }).catch(function () {
5350        btn.disabled = false;
5351        btn.textContent = origText;
5352        hint.textContent = 'Could not save. Please try again.';
5353        hint.style.display = 'block';
5354      });
5355    }
5356
5357    function uploadApptConfirmation(input) {
5358      if (!input.files.length) return;
5359      var label = input.closest('.dn-upload-btn');
5360      var msg = document.getElementById('visaApptUploadMsg');
5361      var origHTML = label.innerHTML;
5362      label.style.pointerEvents = 'none';
5363      label.style.opacity = '0.6';
5364      msg.style.display = 'none';
5365
5366      var formData = new FormData();
5367      for (var i = 0; i < input.files.length; i++) formData.append('files', input.files[i]);
5368
5369      fetch('/dashboard/uploads/private-upload-tagged//' +
5370            encodeURIComponent('Visa Appointment Confirmation'), {
5371        method: 'POST',
5372        credentials: 'include',
5373        body: formData
5374      }).then(function (resp) {
5375        if (!resp.ok) throw new Error('upload failed');
5376        msg.textContent = 'Uploaded. Refreshing...';
5377        msg.style.color = '#16a34a';
5378        msg.style.display = 'block';
5379        setTimeout(function () { location.reload(); }, 1000);
5380      }).catch(function () {
5381        label.innerHTML = origHTML;
5382        label.style.pointerEvents = '';
5383        label.style.opacity = '';
5384        msg.textContent = 'Upload failed. Please try again.';
5385        msg.style.color = '#dc2626';
5386        msg.style.display = 'block';
5387      });
5388    }
5389  </script>
5389
5390
5391  <!-- ===== Task Detail Modal ===== -->
5392  
5393
5394  
5395  <!-- ===== Document Info Modal (what Spain requires) ===== -->
5396  <div id="docInfoModal" class="dn-modal-overlay" style="display:none;">
5397    <div class="dn-modal" style="max-width:560px;">
5398      <div class="dn-modal-header">
5399        <div>
5400          <div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--bl-primary);margin-bottom:2px;">
5401            Document requirements</div>
5402          <div id="diTitle" style="font-size:16px;font-weight:700;color:var(--bl-text-primary);"></div>
5403        </div>
5404        <button onclick="closeDocInfoModal()" class="dn-modal-close">&times;</button>
5405      </div>
5406      <div class="dn-modal-body" style="max-height:70vh;overflow-y:auto;">
5407        
5408        <div id="diRich" style="display:none;"></div>
5409        <div id="diDefault">
5410          <div id="diSummary" style="font-size:13.5px;color:var(--bl-text-secondary);line-height:1.6;margin-bottom:18px;"></div>
5411          <div style="font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--bl-text-muted);margin-bottom:10px;">
5412            What Spain needs</div>
5413          <ul id="diRequirements" style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:10px;"></ul>
5414        </div>
5415        <div id="diDisclaimer"
5416          style="margin-top:18px;padding:10px 14px;border-radius:10px;background:var(--bl-warning-bg);border:1px solid rgba(246,177,0,0.25);font-size:12px;color:var(--bl-text-secondary);line-height:1.5;"></div>
5417      </div>
5418      <div class="dn-modal-footer" style="justify-content:flex-end;">
5419        <button onclick="closeDocInfoModal()" class="dn-modal-btn-secondary">Close</button>
5420      </div>
5421    </div>
5422  </div>
5423  <style>
5424    .dn-doc-info-btn {
5425      display: inline-flex; align-items: center; gap: 5px;
5426      padding: 5px 10px; border-radius: 7px;
5427      border: 1px solid var(--bl-border); background: var(--bl-bg-card);
5428      color: var(--bl-text-secondary); font-size: 11px; font-weight: 600;
5429      cursor: pointer; transition: all 0.15s; white-space: nowrap;
5430    }
5431    .dn-doc-info-btn:hover { color: var(--bl-primary); border-color: var(--bl-primary); background: var(--bl-primary-bg); }
5432    #diRequirements li { display: flex; gap: 10px; align-items: flex-start; font-size: 13px; color: var(--bl-text-primary); line-height: 1.55; }
5433    #diRequirements li svg { flex-shrink: 0; margin-top: 2px; color: var(--bl-success); }
5434
5435    /* Staff-written info: their own text, images and tables, kept inside the modal. */
5436    #diRich { font-size: 13.5px; color: var(--bl-text-primary); line-height: 1.65; }
5437    #diRich > *:first-child { margin-top: 0; }
5438    #diRich > *:last-child { margin-bottom: 0; }
5439    #diRich p, #diRich ul, #diRich ol, #diRich figure, #diRich table { margin: 0 0 12px; }
5440    /* The page resets list markers globally; staff-written lists need them back. */
5441    #diRich ul, #diRich ol { padding-left: 20px; list-style-position: outside; }
5442    #diRich ul { list-style-type: disc; }
5443    #diRich ol { list-style-type: decimal; }
5444    #diRich li { margin-bottom: 5px; }
5445    #diRich h1, #diRich h2, #diRich h3, #diRich h4 {
5446      font-size: 14px; font-weight: 700; margin: 16px 0 8px; color: var(--bl-text-primary);
5447    }
5448    #diRich img { max-width: 100%; height: auto; border-radius: 8px; display: block; margin: 4px 0; }
5449    #diRich figure { max-width: 100% !important; }
5450    #diRich figcaption { font-size: 11.5px; color: var(--bl-text-muted); margin-top: 4px; }
5451    #diRich a { color: var(--bl-primary); }
5452    #diRich table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
5453    #diRich th, #diRich td { border: 1px solid var(--bl-border); padding: 6px 8px; text-align: left; }
5454  </style>
5455  
5455<script>
5456    var DOCUMENT_INFO = {};
5457    var DOCUMENT_INFO_DISCLAIMER = "";
5458    function openDocInfoModal(btn) {
5459      var name = btn.getAttribute('data-doc-name');
5460      var info = DOCUMENT_INFO[name];
5461      if (!info) return;
5462      document.getElementById('diTitle').textContent = info.title || name;
5463
5464      // Staff-written info replaces the standard summary and checklist. It was
5465      // sanitized server-side before it was stored (bleach, the same allow-list
5466      // as case-manager messages), so it goes in as HTML.
5467      var rich = document.getElementById('diRich');
5468      var fallback = document.getElementById('diDefault');
5469      if (info.html) {
5470        rich.innerHTML = info.html;
5471        rich.style.display = 'block';
5472        fallback.style.display = 'none';
5473        document.getElementById('diDisclaimer').textContent = DOCUMENT_INFO_DISCLAIMER;
5474        document.getElementById('docInfoModal').style.display = 'flex';
5475        document.body.style.overflow = 'hidden';
5476        return;
5477      }
5478      rich.innerHTML = '';
5479      rich.style.display = 'none';
5480      fallback.style.display = 'block';
5481
5482      document.getElementById('diSummary').textContent = info.summary || '';
5483      var check = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
5484      var html = '';
5485      (info.requirements || []).forEach(function (r) {
5486        var span = document.createElement('span');
5487        span.textContent = r;                       // escape any user-facing text safely
5488        html += '<li>' + check + '<span>' + span.innerHTML + '</span></li>';
5489      });
5490      document.getElementById('diRequirements').innerHTML = html;
5491      document.getElementById('diDisclaimer').textContent = DOCUMENT_INFO_DISCLAIMER;
5492      document.getElementById('docInfoModal').style.display = 'flex';
5493      document.body.style.overflow = 'hidden';
5494    }
5495    function closeDocInfoModal() {
5496      document.getElementById('docInfoModal').style.display = 'none';
5497      document.body.style.overflow = '';
5498    }
5499    document.getElementById('docInfoModal').addEventListener('click', function (e) { if (e.target === this) closeDocInfoModal(); });
5500    document.addEventListener('keydown', function (e) { var m = document.getElementById('docInfoModal'); if (e.key === 'Escape' && m && m.style.display === 'flex') closeDocInfoModal(); });
5501  </script>
5501
5502
5503  <div id="taskDetailModal" class="dn-modal-overlay" style="display:none;">
5504    <div class="dn-modal" style="max-width:640px;">
5505      <div class="dn-modal-header">
5506        <div>
5507          <div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--bl-primary);margin-bottom:2px;">
5508            Task Details</div>
5509          <div id="tdmTitle" style="font-size:16px;font-weight:700;color:var(--bl-text-primary);"></div>
5510        </div>
5511        <button onclick="closeTaskDetailModal()" class="dn-modal-close">&times;</button>
5512      </div>
5513      <div class="dn-modal-body" style="max-height:70vh;overflow-y:auto;">
5514
5515        
5516        <div id="tdmMeta" style="display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin-bottom:20px;"></div>
5517
5518        
5519        <div id="tdmDescWrap" style="margin-bottom:20px;display:none;">
5520          <div style="font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--bl-text-muted);margin-bottom:8px;">
5521            Description</div>
5522          <div id="tdmDesc" class="dn-task-detail-desc" style="max-height:none;"></div>
5523        </div>
5524
5525        
5526        <div id="tdmStepsWrap" style="margin-bottom:20px;display:none;">
5527          <div style="font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--bl-text-muted);margin-bottom:10px;">
5528            Steps</div>
5529          <div id="tdmSteps" style="display:flex;flex-direction:column;gap:8px;"></div>
5530        </div>
5531
5532        
5533        <div id="tdmQuizWrap" style="margin-bottom:16px;display:none;">
5534          <button onclick="closeTaskDetailModal();openQuizModal();"
5535            style="display:inline-flex;align-items:center;gap:8px;padding:10px 20px;background:var(--bl-primary);color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;transition:all 0.2s ease;">
5536            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
5537              stroke-linecap="round" stroke-linejoin="round">
5538              <path d="M9 11l3 3L22 4" />
5539              <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
5540            </svg>
5541            Take Eligibility Quiz
5542          </button>
5543        </div>
5544
5545        
5546        <div id="tdmDocsWrap" style="margin-bottom:16px;display:none;">
5547          <button onclick="closeTaskDetailModal();scrollToDocuments('required');"
5548            style="display:inline-flex;align-items:center;gap:8px;padding:10px 20px;background:var(--bl-primary);color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;transition:all 0.2s ease;">
5549            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
5550              stroke-linecap="round" stroke-linejoin="round">
5551              <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
5552              <polyline points="17 8 12 3 7 8" />
5553              <line x1="12" y1="3" x2="12" y2="15" />
5554            </svg>
5555            Upload Documents
5556          </button>
5557        </div>
5558
5559        
5560        <div id="tdmApptWrap" style="margin-bottom:16px;display:none;">
5561          <button onclick="closeTaskDetailModal();openVisaApptModal();"
5562            style="display:inline-flex;align-items:center;gap:8px;padding:10px 20px;background:var(--bl-primary);color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;transition:all 0.2s ease;">
5563            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
5564              stroke-linecap="round" stroke-linejoin="round">
5565              <rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
5566              <line x1="16" y1="2" x2="16" y2="6" />
5567              <line x1="8" y1="2" x2="8" y2="6" />
5568              <line x1="3" y1="10" x2="21" y2="10" />
5569            </svg>
5570            Appointment Details &amp; Confirmation
5571          </button>
5572        </div>
5573
5574        
5575        <div id="tdmVideoWrap" style="margin-bottom:20px;display:none;">
5576          <div style="font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--bl-text-muted);margin-bottom:8px;">
5577            Video Guide</div>
5578          <div style="position:relative;padding-top:56.25%;border-radius:10px;overflow:hidden;">
5579            <iframe id="tdmVideo" src="" loading="lazy"
5580              style="border:0;position:absolute;top:0;left:0;height:100%;width:100%;"
5581              allow="accelerometer;gyroscope;autoplay;encrypted-media;picture-in-picture;"
5582              allowfullscreen="true"></iframe>
5583          </div>
5584        </div>
5585
5586      </div>
5587      <div class="dn-modal-footer" style="justify-content:flex-end;">
5588        <button onclick="closeTaskDetailModal()" class="dn-modal-btn-secondary">Close</button>
5589      </div>
5590    </div>
5591  </div>
5592  
5592<script>
5593    function openTaskDetailModal(btn) {
5594      var d = btn.dataset;
5595      document.getElementById('tdmTitle').textContent = d.taskName;
5596
5597      // Build metadata row
5598      var meta = '';
5599      var statusMap = {completed:'<span class=\"dn-badge dn-badge-green\" style=\"font-size:12px;\">Completed</span>',in_progress:'<span class=\"dn-badge dn-badge-blue\" style=\"font-size:12px;\">In Progress</span>',pending:'<span class=\"dn-badge dn-badge-orange\" style=\"font-size:12px;\">Pending</span>'};
5600      meta += '<div class="dn-task-detail-field"><div class="dn-task-detail-label">Status</div><div class="dn-task-detail-value">'+(statusMap[d.taskStatus]||statusMap.pending)+'</div></div>';
5601      meta += '<div class="dn-task-detail-field"><div class="dn-task-detail-label">Assigned to</div><div class="dn-task-detail-value">'+d.taskAssigned+'</div></div>';
5602      if(d.taskEstimated) meta += '<div class="dn-task-detail-field"><div class="dn-task-detail-label">Estimated time</div><div class="dn-task-detail-value">'+d.taskEstimated+'</div></div>';
5603      if(d.taskCompletedDate) meta += '<div class="dn-task-detail-field"><div class="dn-task-detail-label">Completed</div><div class="dn-task-detail-value">'+d.taskCompletedDate+'</div></div>';
5604      if(d.taskCompletedBy) meta += '<div class="dn-task-detail-field"><div class="dn-task-detail-label">Completed by</div><div class="dn-task-detail-value">'+d.taskCompletedBy+'</div></div>';
5605      document.getElementById('tdmMeta').innerHTML = meta;
5606
5607      // Description
5608      var descW = document.getElementById('tdmDescWrap');
5609      if(d.taskDescription){descW.style.display='block';document.getElementById('tdmDesc').innerHTML=d.taskDescription;}else{descW.style.display='none';}
5610
5611      // Steps
5612      var stepsW = document.getElementById('tdmStepsWrap');
5613      try{var steps=JSON.parse(d.taskSteps);if(steps&&steps.length){stepsW.style.display='block';var h='';steps.forEach(function(s,i){h+='<div style="display:flex;gap:10px;align-items:flex-start;font-size:13px;color:var(--bl-text-primary);line-height:1.5;"><div style="min-width:22px;height:22px;border-radius:50%;background:rgba(59,130,246,0.1);color:var(--bl-primary);font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0;">'+(i+1)+'</div><div>'+s+'</div></div>';});document.getElementById('tdmSteps').innerHTML=h;}else{stepsW.style.display='none';}}catch(e){stepsW.style.display='none';}
5614
5615      // Quiz button
5616      document.getElementById('tdmQuizWrap').style.display = d.taskIsQuiz==='1'?'block':'none';
5617
5618      // Documents button — closes the modal and scrolls to the upload area
5619      document.getElementById('tdmDocsWrap').style.display = d.taskIsDocs==='1'?'block':'none';
5620
5621      // Appointment button — closes the modal and opens the appointment form
5622      document.getElementById('tdmApptWrap').style.display = d.taskIsAppt==='1'?'block':'none';
5623
5624      // Video
5625      var vidW = document.getElementById('tdmVideoWrap');
5626      if(d.taskVideo){vidW.style.display='block';document.getElementById('tdmVideo').src=d.taskVideo;}else{vidW.style.display='none';document.getElementById('tdmVideo').src='';}
5627
5628      document.getElementById('taskDetailModal').style.display='flex';
5629      document.body.style.overflow='hidden';
5630    }
5631    function closeTaskDetailModal(){
5632      document.getElementById('taskDetailModal').style.display='none';
5633      document.body.style.overflow='';
5634      document.getElementById('tdmVideo').src='';
5635    }
5636    document.getElementById('taskDetailModal').addEventListener('click',function(e){if(e.target===this)closeTaskDetailModal();});
5637    document.addEventListener('keydown',function(e){var m=document.getElementById('taskDetailModal');if(e.key==='Escape'&&m&&m.style.display==='flex')closeTaskDetailModal();});
5638  </script>
5638
5639  <style>
5640    .dn-task-view-btn {
5641      display: inline-flex; align-items: center; gap: 5px;
5642      padding: 4px 12px; border: 1px solid var(--bl-primary); background: transparent;
5643      color: var(--bl-primary); border-radius: 6px; font-size: 12px; font-weight: 600;
5644      cursor: pointer; transition: all 0.2s ease; white-space: nowrap; flex-shrink: 0;
5645    }
5646    .dn-task-view-btn:hover { background: var(--bl-primary); color: #fff; }
5647    .dn-task-view-btn svg { flex-shrink: 0; }
5648  </style>
5649
5650  
5650<script>
5651    function openQuizModal() {
5652      var m = document.getElementById('quizModal');
5653      if (m) { m.style.display = 'flex'; document.body.style.overflow = 'hidden'; }
5654    }
5655    function closeQuizModal(skipReload) {
5656      var m = document.getElementById('quizModal');
5657      if (m) { m.style.display = 'none'; document.body.style.overflow = ''; if (!skipReload) location.reload(); }
5658    }
5659  </script>
5659
5660
5661  
5662  
5663  
5664  
5665  
5666  
5667  
5668  
5669
5670  <!-- ===== Service Modal ===== -->
5671  <div id="serviceModal" class="dn-modal-overlay" style="display:none;">
5672    <div class="dn-modal" style="max-width:560px;">
5673      <div class="dn-modal-header">
5674        <div>
5675          <div
5676            style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--bl-primary);margin-bottom:2px;">
5677            Recommended Service</div>
5678          <div id="svcModalTitle" style="font-size:16px;font-weight:700;color:var(--bl-text-primary);"></div>
5679        </div>
5680        <button onclick="closeServiceModal()" class="dn-modal-close">&times;</button>
5681      </div>
5682      <div class="dn-modal-body" style="padding:20px 24px;">
5683        <div id="svcModalFlag" class="dn-svc-flag" style="display:none;">Free Consultation</div>
5684        <p id="svcModalDesc" style="font-size:13px;color:var(--bl-text-secondary);line-height:1.6;margin:0 0 14px;"></p>
5685        <div id="svcModalIncWrap" style="margin:0 0 20px;">
5686          <div
5687            style="font-size:11px;text-transform:uppercase;letter-spacing:0.5px;font-weight:600;color:var(--bl-text-muted);margin-bottom:8px;">
5688            What's included</div>
5689          <ul id="svcModalInc" class="dn-svc-inc"></ul>
5690          <div id="svcModalPerk" class="dn-svc-perk" style="display:none;margin-top:10px;"></div>
5691        </div>
5692        <div style="display:flex;gap:8px;flex-wrap:wrap;">
5693          <a id="svcModalLink" href="#" target="_blank" rel="noopener noreferrer" class="dn-modal-btn-primary"
5694            style="text-decoration:none;display:inline-flex;align-items:center;gap:6px;">
5695            Visit Website
5696            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
5697              <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
5698              <polyline points="15 3 21 3 21 9" />
5699              <line x1="10" y1="14" x2="21" y2="3" />
5700            </svg>
5701          </a>
5702          <button id="svcModalCopy" onclick="copyServiceLink()" class="dn-modal-btn-secondary"
5703            style="display:inline-flex;align-items:center;gap:6px;">
5704            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
5705              <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
5706              <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
5707            </svg>
5708            Copy link
5709          </button>
5710        </div>
5711      </div>
5712      <div class="dn-modal-footer">
5713        <div></div>
5714        <button onclick="closeServiceModal()" class="dn-modal-btn-secondary">Close</button>
5715      </div>
5716    </div>
5717  </div>
5718  
5718<script>
5719    // Built from the same list the cards above render, so the two never drift.
5720    var SVC_PARTNERS = {};
5721    ([]).forEach(function (s) {
5722      if (s.placeholder) return;   // no partner behind it yet, so no modal
5723      SVC_PARTNERS[s.slug] = { title: s.modal_title, url: s.url, desc: s.long, includes: s.includes,
5724                               perk: s.perk, free: !!s.free_consultation };
5725    });
5726
5727    // Category filter for the services directory. Sections and chips share the
5728    // same data-cat, so filtering just shows and hides the sections.
5729    function filterServices(cat, btn) {
5730      // Clear any per-card filtering first. The chips only ever hid whole
5731      // categories, so without this a card hidden by showServiceSlugs would
5732      // stay hidden inside a category the client had just asked to see.
5733      document.querySelectorAll('.dn-svc-card').forEach(function (card) {
5734        card.style.display = '';
5735      });
5736      var note = document.getElementById('svcSubsetNote');
5737      if (note) note.style.display = 'none';
5738
5739      document.querySelectorAll('.dn-svc-section').forEach(function (sec) {
5740        sec.style.display = (cat === 'all' || sec.getAttribute('data-cat') === cat) ? '' : 'none';
5741      });
5742      document.querySelectorAll('.dn-svc-chip').forEach(function (chip) {
5743        chip.classList.toggle('is-active', chip === btn);
5744      });
5745    }
5746
5747    /* Show a named handful of partners, whatever categories they sit in.
5748       The chips filter by category, which cannot express "the two partners
5749       that matter after approval" — those are one Legalization and one Taxes.
5750       Task copy calls this with the slugs it wants, so which partners a task
5751       points at is editable in the workspace and not pinned in this file. */
5752    function showServiceSlugs(slugs) {
5753      var want = slugs || [];
5754      if (!want.length) return;
5755
5756      document.querySelectorAll('.dn-svc-section').forEach(function (sec) {
5757        var shown = 0;
5758        sec.querySelectorAll('.dn-svc-card').forEach(function (card) {
5759          var match = want.indexOf(card.getAttribute('data-slug')) !== -1;
5760          card.style.display = match ? '' : 'none';
5761          if (match) shown++;
5762        });
5763        // A category with nothing left in it would render as a bare heading.
5764        sec.style.display = shown ? '' : 'none';
5765      });
5766
5767      // No chip matches a cross-category subset, so none is left looking
5768      // selected, and the note is the way back to the full list.
5769      document.querySelectorAll('.dn-svc-chip').forEach(function (chip) {
5770        chip.classList.remove('is-active');
5771      });
5772      // Explicitly flex, not '': the element's resting state is an inline
5773      // display:none, so clearing it would fall back to a plain block and drop
5774      // the row layout the note is built with.
5775      var note = document.getElementById('svcSubsetNote');
5776      if (note) note.style.display = 'flex';
5777    }
5778
5779    // Called from task copy stored in the workspace, so it has to be reachable
5780    // from an inline onclick rather than only inside this scope.
5781    window.dnShowPartners = function (slugs) {
5782      if (window.dnNav) window.dnNav.show('perks');
5783      showServiceSlugs(slugs);
5784      var card = document.getElementById('svcSubsetNote');
5785      if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });
5786      return false;
5787    };
5788    var _svcUrl = '';
5789    function openServiceModal(slug) {
5790      var p = SVC_PARTNERS[slug];
5791      if (!p) return;
5792      document.getElementById('svcModalTitle').textContent = p.title;
5793      document.getElementById('svcModalDesc').textContent = p.desc;
5794      document.getElementById('svcModalFlag').style.display = p.free ? 'inline-block' : 'none';
5795
5796      var incList = document.getElementById('svcModalInc');
5797      incList.innerHTML = '';
5798      (p.includes || []).forEach(function (inc) {
5799        var li = document.createElement('li');
5800        li.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
5801        var span = document.createElement('span');
5802        span.textContent = inc;
5803        li.appendChild(span);
5804        incList.appendChild(li);
5805      });
5806      var perk = document.getElementById('svcModalPerk');
5807      perk.textContent = p.perk || '';
5808      perk.style.display = p.perk ? 'inline-block' : 'none';
5809      document.getElementById('svcModalIncWrap').style.display =
5810        ((p.includes || []).length || p.perk) ? 'block' : 'none';
5811
5812      document.getElementById('svcModalLink').href = p.url;
5813      _svcUrl = p.url;
5814      document.getElementById('svcModalCopy').innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> Copy link';
5815      document.getElementById('serviceModal').style.display = 'flex';
5816      document.body.style.overflow = 'hidden';
5817    }
5818    function closeServiceModal() {
5819      document.getElementById('serviceModal').style.display = 'none';
5820      document.body.style.overflow = '';
5821    }
5822    function copyServiceLink() {
5823      var btn = document.getElementById('svcModalCopy');
5824      navigator.clipboard.writeText(_svcUrl).then(function () {
5825        btn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg> Copied!';
5826        setTimeout(function () {
5827          btn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> Copy link';
5828        }, 1500);
5829      });
5830    }
5831    document.getElementById('serviceModal').addEventListener('click', function (e) {
5832      if (e.target === this) closeServiceModal();
5833    });
5834    document.addEventListener('keydown', function (e) {
5835      var sm = document.getElementById('serviceModal');
5836      if (e.key === 'Escape' && sm && sm.style.display === 'flex') closeServiceModal();
5837    });
5838  </script>
5838
5839
5840  <!-- ===== Terms & Conditions Modal ===== -->
5841  
5842  <style>
5843    /* The terms read exactly as they do on /policy/terms-conditions: the type
5844       rules below are lifted from lp2_main.css's .pp-* content styles, scoped
5845       to this dialog so none of it reaches the rest of the dashboard. The
5846       landing page's stylesheet is deliberately not loaded here — it would
5847       collide with the dashboard's own. */
5848    @font-face {
5849      font-family: "PT Serif (Custom)";
5850      src: url(https://w-cdn.b-cdn.net/vw_bureaucracy/fonts/69168fb81b24f6e5d070ee47_subset-PTSerif-Regular.woff2) format("woff2");
5851      font-weight: 400; font-style: normal; font-display: swap;
5852    }
5853    @font-face {
5854      font-family: "PT Serif (Custom)";
5855      src: url(https://w-cdn.b-cdn.net/vw_bureaucracy/fonts/69168f8e633706c5fe25b421_subset-PTSerif-Italic.woff2) format("woff2");
5856      font-weight: 400; font-style: italic; font-display: swap;
5857    }
5858
5859    #termsModal .dn-modal { max-width: 860px; }
5860    #termsModal .dn-modal-header { align-items: flex-start; }
5861    #termsModal .dn-terms-title {
5862      font-family: "PT Serif (Custom)", 'PT Serif', Georgia, 'Times New Roman', serif;
5863      font-size: 26px; font-weight: 400; color: #003943; line-height: 1.2;
5864    }
5865    #termsModal .dn-terms-title em { font-style: italic; color: #02b7bf; }
5866
5867    #termsModal .pp-content {
5868      max-width: 70ch;
5869      color: #3d5a61;
5870      font-size: 1rem;
5871      line-height: 1.75;
5872    }
5873    #termsModal .pp-content p { margin: 0 0 1.25rem; }
5874    #termsModal .pp-meta {
5875      font-size: 0.875rem;
5876      color: #7a9499;
5877      margin-bottom: 2.5rem !important;
5878    }
5879    #termsModal .pp-section-heading {
5880      font-family: "PT Serif (Custom)", 'PT Serif', Georgia, 'Times New Roman', serif;
5881      font-size: 1.65rem;
5882      font-weight: 400;
5883      color: #003943;
5884      margin: 3rem 0 1.25rem;
5885      line-height: 1.3;
5886    }
5887    #termsModal .pp-content .pp-section-heading:first-of-type { margin-top: 2rem; }
5888    #termsModal .pp-subheading {
5889      font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
5890      font-size: 1.05rem;
5891      font-weight: 600;
5892      color: #003943;
5893      margin: 2rem 0 0.75rem;
5894      line-height: 1.4;
5895    }
5896    #termsModal .pp-link {
5897      color: #02b7bf;
5898      font-weight: 600;
5899      text-decoration: none;
5900      transition: color 0.2s;
5901    }
5902    #termsModal .pp-link:hover { color: #00959c; text-decoration: underline; }
5903    #termsModal .pp-list {
5904      margin: 0.75rem 0 1.5rem;
5905      padding-left: 1.5rem;
5906      list-style: disc;
5907    }
5908    #termsModal .pp-list li { margin-bottom: 0.5rem; line-height: 1.7; }
5909    #termsModal .pp-content strong { color: #003943; font-weight: 600; }
5910
5911    @media (max-width: 575px) {
5912      #termsModal .pp-section-heading { font-size: 1.35rem; }
5913      #termsModal .pp-content { font-size: 0.95rem; }
5914    }
5915  </style>
5916  <div id="termsModal" class="dn-modal-overlay" style="display:none;">
5917    <div class="dn-modal">
5918      <div class="dn-modal-header">
5919        <div>
5920          <div
5921            style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--bl-primary);margin-bottom:2px;">
5922            Legal</div>
5923          <div class="dn-terms-title">Terms &amp; <em>Conditions</em></div>
5924        </div>
5925        <button onclick="closeTermsModal()" class="dn-modal-close">&times;</button>
5926      </div>
5927      <div class="dn-modal-body">
5928        <article class="pp-content">
5929          
5930
5931        <p>These Legal Terms constitute a legally binding agreement made between you, whether personally or on behalf of an entity ("you"), and Bureaucracy International Group B.V., doing business as bureaucracy.es, concerning your access to and use of the Services.</p>
5932        <p class="pp-meta">Last updated: 19 August 2026</p>
5933
5934        <!-- 1. Agreement to Legal Terms -->
5935        <h2 id="agreement" class="pp-section-heading">Agreement to Our Legal Terms</h2>
5936        <p>We are Bureaucracy International Group B.V., doing business as bureaucracy.es ("Company," "we," "us," "our"), a company registered in the Netherlands at Lange Wijngaardstraat 31-RD, 2011RK, Haarlem, Nederland 2033DS. Our VAT number is NL864561702B01.</p>
5937        <p>We operate the website <a href="https://www.bureaucracy.es" class="pp-link">http://www.bureaucracy.es</a> (the "Site"), as well as any other related products and services that refer or link to these legal terms (the "Legal Terms") (collectively, the "Services").</p>
5938        <p>
5938You can contact us by email at <a href="/cdn-cgi/l/email-protection#9af3f4fcf5daf8efe8fffbeff9e8fbf9e3b4ffe9" class="pp-link"><span class="__cf_email__" data-cfemail="0f666169604f6d7a7d6a6e7a6c7d6e6c76216a7c">[email&#160;protected]</span></a> or by mail to Lange Wijngaardstraat 31-RD, 2011RK., Haarlem, Nederland 2033DS, Netherlands.</p>
5939        <p>These Legal Terms constitute a legally binding agreement made between you, whether personally or on behalf of an entity ("you"), and Bureaucracy International Group B.V., concerning your access to and use of the Services. You agree that by accessing the Services, you have read, understood, and agreed to be bound by all of these Legal Terms. IF YOU DO NOT AGREE WITH ALL OF THESE LEGAL TERMS, THEN YOU ARE EXPRESSLY PROHIBITED FROM USING THE SERVICES AND YOU MUST DISCONTINUE USE IMMEDIATELY.</p>
5940        <p>Supplemental terms and conditions or documents that may be posted on the Services from time to time are hereby expressly incorporated herein by reference. We reserve the right, in our sole discretion, to make changes or modifications to these Legal Terms from time to time. We will alert you about any changes by updating the "Last updated" date of these Legal Terms, and you waive any right to receive specific notice of each such change.</p>
5941        <p>The Services are intended for users who are at least 18 years old. Persons under the age of 18 are not permitted to use or register for the Services.</p>
5942        <p>We recommend that you print a copy of these Legal Terms for your records.</p>
5943
5944        <!-- 2. Services Offered -->
5945        <h2 id="services-offered" class="pp-section-heading">1. Services Offered</h2>
5946        <p>Bureaucracy International Group B.V. provides advisory and management services for immigration processes related to residency in Spain. The types of processes offered include:</p>
5947
5948        <h3 class="pp-subheading">Free Intake Call Before Hiring our Services of NLV, DNV and renewals</h3>
5949        <p>We offer a complimentary intake call to potential clients as a one-time service. Please note that if a potential client misses their scheduled call, they will not be able to book another one, as this would take away the opportunity for other potential clients to receive assistance. We encourage clients to proceed with the initial payment to secure a dedicated case manager who will provide personalized support and initial calls.</p>
5950
5951        <h3 class="pp-subheading">Citizenship</h3>
5952        <p>This is a paid consultation for Spanish citizenship services provided by our immigration attorney in Spain, Douaa Elamrani. As this service is handled directly by an attorney, the consultation carries a fee and therefore we are unable to offer a complimentary meeting for this specific service.</p>
5953
5954        <h3 class="pp-subheading">Non-Lucrative Residency Visa</h3>
5955        <p>Advisory and management services for obtaining the Non-Lucrative Residency Visa, allowing individuals to reside in Spain for non-lucrative purposes.</p>
5956
5957        <h3 class="pp-subheading">Remote Work Residency Visa (Digital Nomad Visa)</h3>
5958        <p>Consulting and management services for international remote workers seeking to obtain the Digital Nomad Visa and live in Spain.</p>
5959
5960        <h3 class="pp-subheading">Visa Renewals</h3>
5961        <p>Continuity services for renewing visas and residency permits previously granted.</p>
5962
5963        <h3 class="pp-subheading">Visa Cancellations</h3>
5964        <p>If a client decides to cancel their visa, we will assist them with this process for a fee of €100 per visa cancelled per adult and €50 per child.</p>
5965
5966        <h3 class="pp-subheading">After-Approval Visa Submissions</h3>
5967        <p>In case the client does not comply with their visa obligations and their visa is further analyzed by the competent authorities, and any requests are to be handled, a fee of €200 will be required.</p>
5968
5969        <h3 class="pp-subheading">Express Application (Priority Processing)</h3>
5970        <p>An add-on available on top of any existing visa service (NLV, DNV, Student Visa, or Renewals) that prioritises the client's file within our internal workflow for faster internal preparation and case management. This add-on does not affect or accelerate government or consulate processing times. Full details and eligibility requirements are set out in the "Express Application" section below.</p>
5971
5972        <!-- 3. Subservices Included -->
5973        <h2 id="subservices" class="pp-section-heading">2. Subservices Included in the Process</h2>
5974
5975        <h3 class="pp-subheading">Document Review</h3>
5976        <p>Verification and guidance regarding the documents required for the process.</p>
5977
5978        <h3 class="pp-subheading">Sworn Translations</h3>
5979        <p>Translation of required documents, up to a pre-agreed value. Any difference must be covered by the client. Please note that we are not responsible for any changes in the law or in its interpretation. Some consulates may require extra documents to be sworn-translated, and we must comply with their requests. The client may choose not to translate these additional documents; however, this may result in delays in the submission and approval of the visa.</p>
5980
5981        <h3 class="pp-subheading">Document Delivery</h3>
5982        <p>Documents and printed booklets are sent via the Dashboard or courier services such as DHL, when applicable. Physical delivery is only available in certain countries such as the US, UK, and Canada. If physical delivery is not possible for any reason, files will be sent digitally through the Dashboard, with no impact on your payment structure or the amount due.</p>
5983        <p>Printed booklets are not included in all service plans and are only provided where applicable under the contracted service type and Bureaucracy's internal operational criteria.</p>
5984        <p>Express Application clients are not entitled to printing or physical delivery of application booklets as part of their contracted service. The booklet will be made available exclusively in digital format through the Dashboard. Any printing or shipping costs the client chooses to incur independently shall be borne solely by the client and will not entitle them to any reimbursement, discount, or reduction of future installments.</p>
5985        <p>Standard Service clients: If the client fails to provide the required documentation, information, or approvals within the deadlines communicated by the assigned Case Manager — making it impossible to prepare and dispatch printed booklets within the required t
5985imeframe — all printing and shipping costs shall become the client's sole responsibility. In such circumstances, Bureaucracy may, at its sole discretion, provide the booklet exclusively in digital format via the Dashboard, with no adjustment to the contracted service fee.</p>
5986
5987        <h3 class="pp-subheading">Assistance with Requirements and Appeals</h3>
5988        <p>We provide support for meeting specific requirements and managing appeals at the UGE, where applicable. Please note that we do not handle appeals at the Spanish court level.</p>
5989
5990        <h3 class="pp-subheading">Application Submission</h3>
5991        <p>Assistance in submitting applications in Spain or forwarding documents to the consulate in the applicant's country of residence, as applicable.</p>
5992
5993        <h3 class="pp-subheading">Payment of Government Fees in Spain</h3>
5994        <p>We will cover the payment of applicable Spanish government fees, such as those for the DNV service or renewals of NLV and DNV. Our service includes a single submission. If a re-submission is required, the responsibility for further costs will not rest with us.</p>
5995
5996        <!-- 4. Services Not Included -->
5997        <h2 id="not-included" class="pp-section-heading">3. Services Not Included</h2>
5998
5999        <h3 class="pp-subheading">Tax Liability Consultation</h3>
6000        <p>Bureaucracy does not provide tax advice. Clients are encouraged to consult specialized tax advisors for any queries regarding tax obligations related to Spanish residency.</p>
6001
6002        <h3 class="pp-subheading">Schengen Visa Applications</h3>
6003        <p>Bureaucracy does not handle Schengen visa applications. Clients are responsible for applying on their own, if this visa is needed. As well, we are unable to respond to any questions related to this visa or review documents.</p>
6004
6005        <h3 class="pp-subheading">Real Estate Services</h3>
6006        <p>We do not assist with property rental or purchase but can recommend trusted real estate professionals.</p>
6007
6008        <h3 class="pp-subheading">Social Security and Autónomo Registration</h3>
6009        <p>After securing a Digital Nomad Visa, self-employed clients are responsible for registering with Spanish Social Security and handling their tax matters independently.</p>
6010
6011        <h3 class="pp-subheading">Travel Authorization</h3>
6012        <p>This type of service is not provided by our company, so we cannot offer any information related to your travel plans or how they might impact your travel authorization.</p>
6013
6014        <h3 class="pp-subheading">Obtaining Public Documents</h3>
6015        <p>Please note that we do not provide services for obtaining public documents, such as criminal records, certificates of company incorporation, or apostilles. Customers are responsible for securing these documents independently through the appropriate official channels.</p>
6016
6017        <h3 class="pp-subheading">Residency Card Services</h3>
6018        <p>As of October 2024, we have discontinued offering the TIE (Residency Card in Spain) service. Current clients who have already purchased this service will still receive full support, as promised. However, for clients who engage our services from October 2024 onward, Bureaucracy's assistance will conclude upon the positive resolution of their visa or residency authorization.</p>
6019
6020        <h3 class="pp-subheading">Post-Visa Approval Assistance</h3>
6021        <p>Our agreement concludes once your visa or residency authorization is approved. Beyond this point, we are not obligated to provide additional services unless otherwise agreed.</p>
6022
6023        <!-- 5. Appointments for TIE -->
6024        <h2 id="tie-appointments" class="pp-section-heading">4. Appointments for TIE (Foreigner Identity Card) — for Clients Who Contracted Our Services Prior to October 2024</h2>
6025
6026        <h3 class="pp-subheading">City and Regional Variations</h3>
6027        <p>In high-demand cities, such as Madrid, Barcelona, and Valencia, clients may experience delays in securing appointments. Please be advised that Bureaucracy cannot influence or expedite government processes; flexibility and patience are appreciated.</p>
6028
6029        <h3 class="pp-subheading">Service Inclusions</h3>
6030        <p>As we've discontinued the TIE service, this part of the process is not included for clients who contracted Bureaucracy's services after October 2024.</p>
6031
6032        <h3 class="pp-subheading">Appointment Rescheduling</h3>
6033        <p>
6033Bureaucracy schedules one appointment per applicant. If an applicant cannot attend the scheduled appointment due to circumstances beyond our control, the client will be responsible for rescheduling directly.</p>
6034
6035        <h3 class="pp-subheading">Timeline</h3>
6036        <p>We will begin processing your Padrón and TIE application within 10 business days after receiving the rental agreement. Unfortunately, we cannot expedite this timeline. Our team will organize your file upon the date of receiving the documents and proceed with scheduling the Padrón appointment, preparing the TIE booklet, and TIE appointments. If our timeline does not align with the client's preferred schedule, they are welcome to manage these processes independently at their own responsibility.</p>
6037
6038        <h3 class="pp-subheading">Apartment Hunting</h3>
6039        <p>Bureaucracy does not provide recommendations or advice regarding the choice of city for residency in Spain. As we are not a relocation company, we are unable to liaise with or communicate with any third parties responsible for relocation services. Additionally, we are not responsible for advising on or reviewing rental contracts.</p>
6040
6041        <!-- 6. Express Application -->
6042        <h2 id="express-application" class="pp-section-heading">5. Express Application (Priority Processing Upgrade)</h2>
6043        <p>The Express Application is a paid add-on available on top of any existing visa service (NLV, DNV, Student Visa, or Renewals). It prioritises the client's file within our internal workflow and does not affect or accelerate government or consulate processing times.</p>
6044
6045        <h3 class="pp-subheading">Eligibility and Minimum Lead Times</h3>
6046        <ul class="pp-list">
6047          <li><strong>Digital Nomad Visa:</strong> minimum 3 weeks before intended application date.</li>
6048          <li><strong>Non-Lucrative Visa:</strong> minimum 1 month before confirmed consulate appointment (a confirmed appointment date is required; Express Application cannot be contracted for NLV without one).</li>
6049          <li><strong>NLV and DNV Renewals:</strong> minimum 2 weeks before application deadline.</li>
6050        </ul>
6051
6052        <h3 class="pp-subheading">Response Time</h3>
6053        <p>Email enquiries and document reviews are handled within 2 business days (compared to the standard 4-business-day SLA).</p>
6054
6055        <h3 class="pp-subheading">Client Responsibilities</h3>
6056        <p>This is a collaborative service. If the client is unresponsive or fails to provide required documents in time, the case will be automatically downgraded to standard processing and the Express Application fee will not be refunded.</p>
6057
6058        <h3 class="pp-subheading">Non-Refundable Fee</h3>
6059        <p>The Express Application fee is non-refundable in all circumstances, including where the case is downgraded to standard processing due to client-caused delays.</p>
6060
6061        <!-- 6. Urgency and Documentation Requirements -->
6062        <h2 id="urgency" class="pp-section-heading">6. Urgency and Documentation Requirements</h2>
6063
6064        <h3 class="pp-subheading">Urgency Definition</h3>
6065        <p>Urgent requests are strictly limited to situations where specific required documents or government-mandated deadlines are at risk of being missed. Only these essential requirements are treated as urgent.</p>
6066
6067        <h3 class="pp-subheading">Required Documentation</h3>
6068        <p>Clients are solely responsible for providing all necessary documentation for their visa applications and ensuring that all information, declarations, and supporting documents provided to Bureaucracy are accurate, complete, and truthful. Bureaucracy is not liable for any delays, refusals, rejections, penalties, or adverse outcomes resulting from missing, incomplete, misleading, or fraudulent submissions. Any additional costs incurred due to incomplete or delayed documentation, such as expedited sworn translations, shall be the sole responsibility of the client.</p>
6069
6070        <!-- 7. Response Times -->
6071        <h2 id="response-times" class="pp-section-heading">7. Response Times</h2>
6072        <p>
6072Bureaucracy commits to responding to inquiries within 4 business days, with exceptions for holiday periods and peak times that we will inform our clients in advance. The review of documents will also be handled within this specified timeframe.</p>
6073
6074        <h3 class="pp-subheading">Documentation and FAQs on Dashboard</h3>
6075        <p>For general questions, clients are encouraged to consult the Dashboard. Questions addressed there will not receive additional responses via email or other channels.</p>
6076        <p>Clients with an active Express Application benefit from a reduced response time of 2 business days for email enquiries and document reviews. See the Express Application section above for full details.</p>
6077
6078        <!-- 8. Official Communication Channels -->
6079        <h2 id="communication" class="pp-section-heading">8. Official Communication Channels</h2>
6080
6081        <h3 class="pp-subheading">Dashboard</h3>
6082        <p>All documentation must be submitted through the Bureaucracy Dashboard to ensure secure processing.</p>
6083
6084        <h3 class="pp-subheading">Email</h3>
6085        <p>Our main communication channel for inquiries and updates.</p>
6086
6087        <h3 class="pp-subheading">Virtual Meetings (Google Meet)</h3>
6088        <p>Each client is entitled to a set number of scheduled sessions, depending on the visa type. The number of meetings in credit will be informed by the assigned Case Manager during the client's initial meeting. The initial meeting has a maximum length of 45 minutes, and subsequent meetings of 30 minutes. We reserve the right to postpone or cancel meetings as necessary, with an explanation provided for any such changes.</p>
6089        <p>We will communicate any additional correspondence with the Spanish government to you via the email address initially provided. Should you choose to change your email address and consequently not receive further information from us, this will not be our responsibility.</p>
6090        <p>Any communication outside these official channels is not considered valid, and we are not obligated to respond.</p>
6091
6092        <!-- 9. Case Manager Assignment -->
6093        <h2 id="case-manager" class="pp-section-heading">9. Case Manager Assignment and Substitution</h2>
6094        <p>Bureaucracy assigns a Case Manager to each client to oversee and guide the process, on behalf of Bureaucracy. However, the assignment of a specific Case Manager is not guaranteed. The company reserves the right to replace or temporarily reassign the Case Manager at any time due to operational needs, including but not limited to vacation leave, illness, workload distribution, or staff changes. Such substitutions do not affect the scope or quality of the services contracted, and clients will continue to receive full support from the Bureaucracy team.</p>
6095
6096        <!-- 10. Case Pausing, Reactivation, and Expiration -->
6097        <h2 id="case-pausing" class="pp-section-heading">10. Case Pausing, Reactivation, and Expiration</h2>
6098
6099        <h3 class="pp-subheading">Inactivity and Case Pausing</h3>
6100        <p>If a client does not respond to follow-ups or fails to submit the required documents within a period of one and a half months, their case will be put on hold. The client will be duly notified of this. To resume a paused case that's been on hold for six months or more, a reactivation fee of €100 will apply.</p>
6101
6102        <h3 class="pp-subheading">Completion Deadline</h3>
6103        <p>The visa process must be completed within one (1) year from the date of payment. Failure to do so may result in additional costs or the need to restart the process. If a case has been handled for over six months in relation to the initial payment date or reactivation date — if applicable — without us being able to submit it due to missing documents or lack of proactivity from the client, we will require the second installment to be paid before being able to continue with the case processing.</p>
6104
6105        <!-- 11. Right to Refuse or Terminate Services -->
6106        <h2 id="right-to-refuse" class="pp-section-heading">11. Right to Refuse or Terminate Services</h2>
6107        <p>
6107Bureaucracy reserves the right to refuse, suspend, or terminate services at its sole discretion where the client:</p>
6108        <ul class="pp-list">
6109          <li>provides false, misleading, incomplete, or fraudulent information;</li>
6110          <li>requests unlawful actions;</li>
6111          <li>engages in abusive, threatening, discriminatory, or inappropriate conduct toward staff; or</li>
6112          <li>otherwise compromises the integrity or lawful execution of the immigration process.</li>
6113        </ul>
6114        <p>In such cases, any fees already paid shall remain non-refundable.</p>
6115
6116        <!-- 12. Missed Online Meeting Appointments -->
6117        <h2 id="missed-meetings" class="pp-section-heading">12. Missed Online Meeting Appointments</h2>
6118        <p>If the client fails to attend a scheduled call, it will count as one of the allotted sessions. Rescheduling is subject to availability.</p>
6119
6120        <!-- 13. Visa Change Policy -->
6121        <h2 id="visa-change" class="pp-section-heading">13. Visa Change Policy</h2>
6122        <p>Clients may request a visa type change once during the process. If the new visa is more expensive than your original choice, you will be charged the difference accordingly.</p>
6123        <p>Clients may request one visa category change during the process, provided the request is made within 90 days of the service agreement date and before the second payment is due.</p>
6124        <p>If the new visa category has a higher service fee than the original option, the client will be responsible for paying the applicable price difference.</p>
6125        <p>Requests submitted after 90 days from the service agreement date, after the second payment has been made, or once substantial work has already been completed may be subject to an administrative reassessment fee and/or additional professional service fees, at the Company's discretion, to cover the additional work required.</p>
6126        <p>Where the visa change requires a significant revision of strategy, documentation, eligibility assessment, or case preparation, the Company reserves the right to treat the request as a new application process and charge the corresponding fees.</p>
6127        <p>Any subsequent visa category change request beyond the first approved change shall be treated as a new application process and charged accordingly.</p>
6128
6129        <!-- 14. Purchases and Payment Method -->
6130        <h2 id="purchases" class="pp-section-heading">14. Purchases and Payment Method</h2>
6131        <p>We accept the following forms of payment:</p>
6132        <ul class="pp-list">
6133          <li>Visa</li>
6134          <li>Mastercard</li>
6135          <li>American Express</li>
6136          <li>Bank Transfer</li>
6137        </ul>
6138        <p>You agree to provide current, complete, and accurate purchase and account information for all purchases made via the Services. We may change prices at any time. All payments shall be in Euros.</p>
6139
6140        <!-- 15. Refund Policy -->
6141        <h2 id="refund-policy" class="pp-section-heading">15. Refund Policy</h2>
6142        <p>Please read this policy carefully before proceeding with our services. All payments are non-refundable under any circumstances, as outlined below:</p>
6143
6144        <h3 class="pp-subheading">1. General Non-Refundability</h3>
6145        <p>Payments made to our company are non-refundable. This applies regardless of the following:</p>
6146        <ul class="pp-list">
6147          <li><strong>Change of Mind:</strong> If the client decides not to proceed with the service.</li>
6148          <li><strong>Failure to Obtain Approvals:</strong> If the client is unable to secure necessary approvals or permits from relevant authorities.</li>
6149          <li><strong>External Factors Beyond Our Control:</strong> This includes changes in personal circumstances, government or consular policies, or other unpredictable events.</li>
6150        </ul>
6151
6152        <h3 class="pp-subheading">2. Discontinuation by the Client</h3>
6153        <p><strong>No Refund for Payments Made:</strong> Payments already made will not be refunded. This includes any costs for completed work, including but not limited to: sworn translations, document deliveries, or government fees.</p>
6154        <p><strong>Installment Payments:</strong> Our services are structured into three installments. If the client discontinues services, no further installments will be required, provided no additional work has been performed.</p>
6155
6156        <h3 class="pp-subheading">3. Commitment to Service Upon Welcome Email</h3>
6157        <p>Once the Welcome Email is sent, the first payment is considered non-refundable. This payment covers:</p>
6158        <ul class="pp-list">
6159          <li>The creation of the client profile.</li>
6160          <li>Preliminary consultations.</li>
6161          <li>Access to our service platform and initial setup.</li>
6162        </ul>
6163
6164        <h3 class="pp-subheading">4. Platform Use and Familiarity</h3>
6165        <ul class="pp-list">
6166          <li>Challenges or misunderstandings in using the platform (e.g., uploading documents).</li>
6167          <li>Technical issues unrelated to our system.</li>
6168          <li>Support is always available to assist with platform navigation and use.</li>
6169        </ul>
6170
6171        <h3 class="pp-subheading">5. Schengen Visa Applications</h3>
6172        <p>If a client requires a Schengen visa and their application is denied, no refund will be provided. Schengen visa processing is beyond the scope of our services, and we cannot be held responsible for outcomes related to such applications.</p>
6173        <p><strong>Important Note:</strong> By proceeding with our services and making the first payment, clients acknowledge and accept the terms of this refund policy.</p>
6174
6175        <!-- 16. Resubmission of Applications -->
6176        <h2 id="resubmission" class="pp-section-heading">16. Resubmission of Applications</h2>
6177        <p>If an application must be resubmitted for any reason not attributable to Bureaucracy International Group B.V., including but not limited to rejection of the visa application, expiration of documents, missing or incomplete documentation, failure to attend scheduled appointments, changes in the client's personal circumstances, or lack of timely cooperation from the client, all additional costs, fees, and professional services required for the resubmission shall be borne exclusively by the client.</p>
6178        <p>In the event that a visa application is rejected for any reason, the application process shall be considered closed. Should the client wish to proceed with a new application, the process must be restarted, and the client will be required to pay the applicable first and second installments again in order to initiate the new application.</p>
6179
6180        <h3 class="pp-subheading">No Compensation for Third-Party or Incidental Expenses</h3>
6181        <p>
6181Bureaucracy International Group B.V. shall not be liable to reimburse or compensate the client for any third-party, incidental, or out-of-pocket expenses incurred in connection with the immigration process, regardless of the outcome of the application or any delays, cancellations, or rescheduling attributable to government authorities, consulates, or other third parties.</p>
6182        <p>Such expenses include, without limitation: consular, BLS fees; travel costs (flights, accommodation, car rentals, and transportation); courier and delivery charges; parking fees; apostilles; notarizations; document issuance fees; printing costs; and any other personal or third-party expenditure. All such costs are incurred solely at the client's own risk and remain the client's exclusive responsibility.</p>
6183
6184        <!-- 17. Confidentiality and Data Protection -->
6185        <h2 id="confidentiality" class="pp-section-heading">17. Confidentiality and Data Protection</h2>
6186
6187        <h3 class="pp-subheading">Data Retention</h3>
6188        <p>Client documentation will be securely stored for a period of 10 months after service completion. If the client does not request renewal or extended retention, all data will be deleted after this period.</p>
6189
6190        <h3 class="pp-subheading">Data Protection and Consent</h3>
6191        <p>Bureaucracy complies with the GDPR and ensures that all client information is used solely for the agreed-upon services. Client data will not be shared outside of the company unless legally required.</p>
6192
6193        <!-- 18. Privacy and Data Protection (GDPR Compliance) -->
6194        <h2 id="privacy-gdpr" class="pp-section-heading">18. Privacy and Data Protection (GDPR Compliance)</h2>
6195
6196        <h3 class="pp-subheading">Data Controller</h3>
6197        <p>Bureaucracy International Group B.V. acts as the Data Controller for all personal information provided. We process your data in strict accordance with the EU General Data Protection Regulation (GDPR) and the 2026 EU Data Act.</p>
6198
6199        <h3 class="pp-subheading">What We Collect &amp; Why</h3>
6200        <p>We collect personal identity documents, financial records, criminal record certificates, and health insurance details. The legal basis for this processing is Contractual Necessity and Legal Obligation.</p>
6201
6202        <h3 class="pp-subheading">How We Protect Your Data</h3>
6203        <p>All documents must be uploaded via our secure Dashboard. We use end-to-end encryption for all data at rest and in transit. Only your assigned Case Manager and authorized legal staff have access to your private files. We never sell or lease your data to marketers.</p>
6204
6205        <h3 class="pp-subheading">Third-Party Sharing</h3>
6206        <p>To complete your application, we must share specific data with Spanish Authorities (Consulates, the UGE, and immigration offices) and Service Partners (sworn translators and courier services such as DHL). All our partners are bound by Data Processing Agreements (DPAs).</p>
6207
6208        <h3 class="pp-subheading">Your Rights</h3>
6209        <p>Under EU law, you have the right to access &amp; portability, correction, erasure, and withdrawal of consent. For any privacy-related concerns, please contact our Data Protection lead at <a href="/cdn-cgi/l/email-protection#daaaa8b3acbbb9a39ab8afa8bfbbafb9a8bbb9a3f4bfa9" class="pp-link"><span class="__cf_email__" data-cfemail="95e5e7fce3f4f6ecd5f7e0e7f0f4e0f6e7f4f6ecbbf0e6">[email&#160;protected]</span></a>.</p>
6210
6211        <h3 class="pp-subheading">Retention &amp; Deletion</h3>
6212        <p>Active files are stored for the duration of your visa process. Post-service, data is securely archived for 10 months after your case is closed. After 10 months, all personal documents and sensitive files are permanently deleted from our servers, unless Spanish tax or immigration law requires a longer statutory retention period.</p>
6213
6214        <!-- 19. Sworn Translations -->
6215        <h2 id="sworn-translations" class="pp-section-heading">19. Sworn Translations</h2>
6216        <p>Our company partners with third-party providers to facilitate sworn translations across multiple languages. We manage the translation process for all supported languages into Spanish.</p>
6217        <ul class="pp-list">
6218          <li><strong>Non-Lucrative Visas:</strong> Bureaucracy provides coverage for the adult applicant up to a limit of €80. Any sum that exceeds this amount will be covered by the applicant.</li>
6219          <li><strong>Digital Nomad Visas:</strong> The coverage provided by Bureaucracy is up to €150 per adult applicant. Any sum that exceeds this amount will be covered by the applicant.</li>
6220          <li><strong>Student Visas:</strong> Bureaucracy covers up to €50 per adult applicant. Any sum that exceeds this amount will be covered by the applicant.</li>
6221          <li><strong>Non-Lucrative Visa Renewals:</strong> Applicants are responsible for their own translation costs during the Non-Lucrative Visa Renewal process, as Bureaucracy does not provide coverage for this specific service.</li>
6222          <li><strong>Digital Nomad Visa Renewals:</strong> Bureaucracy provides coverage for adult applicants up to a maximum of €75. Any sum that exceeds this amount will be covered by the applicant.</li>
6223          <li><strong>Citizenship:</strong> Bureaucracy provides coverage for the adult applicant up to a limit of €200. Any sum that exceeds this amount will be covered by the applicant.</li>
6224          <li><strong>All other services:</strong> Services not listed above do not include any sworn translation coverage, and translation costs are the responsibility of the applicant.</li>
6225        </ul>
6226        <p>The amounts above are per adult applicant: a family's coverage is the applicable amount multiplied by the number of adult applicants, and no additional coverage is provided for children.</p>
6227
6228        <p>If the client chooses to handle sworn translations independently, we are not responsible for providing any reimbursement or deduction from the initial cost.</p>
6229
6230        <p>The cost for translations in languages other than English is determined by word or page count and the specific language required. We collaborate with various independent sworn translators who provide services into Spanish, each establishing their own individual rates, response times, and project schedules. Consequently, per-word or per-page costs cannot be predicted in advance. While Bureaucracy remains committed to securing the most competitive pricing for our clients, these external rates and timelines are outside our direct control and are not subject to negotiation.</p>
6231
6232        <!-- 20. AI Assistant (Carlos) -->
6233        <h2 id="ai-assistant" class="pp-section-heading">20. AI Assistant (Carlos)</h2>
6234        <p>Your client dashboard includes an AI assistant, "Carlos", that helps you by summarising where you are in y
6234our process, which documents are still needed and what to do next, and by answering your questions. To provide this, your case information (such as your service, progress, documents and payments) is shared with our own AI service, operated for us under confidentiality.</p>
6235        <p>This data is used <strong>solely to assist you</strong> within your dashboard. It is not used for advertising, it is not used to train third‑party AI models, and it is <strong>never shared with, sold to, or made available to anyone else</strong>. For more detail, see the "Our AI Assistant (Carlos)" section of our <a href="/policy/privacy-cookies#ai-assistant">Privacy Policy</a>.</p>
6236
6237        <!-- 21. Payments -->
6238        <h2 id="payments" class="pp-section-heading">21. Payments</h2>
6239        <p>At Bureaucracy.es, we split payments into three installments to make the process easier and more affordable for our clients. However, all payments are mandatory. If we do not receive any of the scheduled payments (except in cases where visas are not approved), we reserve the right to initiate legal debt recovery proceedings to collect the unpaid balance, as the service has been provided in full. Upon visa approval, the final payment must be made by the client in full within the next three days after the approval of the visa has been notified.</p>
6240        <p>If the client chooses to submit an application without providing all the required documents, the third payment must be made in full before submission.</p>
6241        <p>If a client decides to withdraw their application after submission, they are required to make the third payment in full. We will then proceed with the withdrawal request through the corresponding channel.</p>
6242        <p>If after all the work is conducted, including but not limited to review of documents, review calls and shipment of complete applications, the client decides not to attend their scheduled appointment, they are required to make the third payment in full.</p>
6243        <p>A detailed breakdown of the services included in each installment is provided below:</p>
6244
6245        <h3 class="pp-subheading">Initial Payment</h3>
6246        <p>This installment covers the assignment of a dedicated case manager and the comprehensive study of your case. It includes preparing for the onboarding meeting, conducting an eligibility review, and establishing a customized checklist. During this phase, we begin processing initial documentation, validating files, and providing continuous guidance to address client inquiries.</p>
6247        <p>The initial payment is non-refundable if you decide to cancel or delay your arrival date before the first meeting takes place.</p>
6248
6249        <h3 class="pp-subheading">Second Payment</h3>
6250        <p>Required mid-process, this payment ensures the continuation of work initiated during the earlier stages. It encompasses ongoing management, document delivery, sworn translations, and the settlement of relevant fees where applicable. This installment also covers the final preparation and formal submission of the application. Please note that this payment is non-refundable, as it represents the active continuation of professional services already in progress.</p>
6251
6252        <h3 class="pp-subheading">Third Payment</h3>
6253        <p>Typically due upon the successful granting of the visa. However, in complex cases and by prior agreement with the Bureaucracy team, we may require this final payment upfront to manage specific risks. In such instances, the payment is requested before the result is known and is not subject to refund obligations.</p>
6254
6255        <h3 class="pp-subheading">Multi-Applicant Visa Applications</h3>
6256        <p>Where a visa application is contracted for more than one applicant (main applicant and dependents or family members), the full third installment — covering all applicants within the contracted service — shall become due and payable upon notification of the main applicant's approval, regardless of whether decisions on the remaining applicants have been issued at that time.</p>
6257
6258        <!-- 22. Disclaimer -->
6259        <h2 id="disclaimer" class="pp-section-heading">22. Disclaimer</h2>
6260        <p>
6260Bureaucracy operates within the bounds of Spanish immigration law but cannot control or expedite administrative processes regulated by external entities (e.g., consulates or local authorities). Delays in appointment availability, document processing, or government responses are outside our control. Patience and adherence to the established timelines are essential for a successful immigration process.</p>
6261
6262        </article>
6263      </div>
6264      <div class="dn-modal-footer" style="justify-content:space-between;align-items:center;">
6265        <a href="/policy/terms-conditions" target="_blank" rel="noopener"
6266          style="font-size:12px;color:var(--bl-text-muted);text-decoration:none;">Open in a new tab</a>
6267        <button onclick="closeTermsModal()" class="dn-modal-btn-primary">Close</button>
6268      </div>
6269    </div>
6270  </div>
vendor: 111 bytes, lines 6270-6271
6270
6271  <script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script>
6271<script>
6272    function openTermsModal() {
6273      document.getElementById('termsModal').style.display = 'flex';
6274      document.body.style.overflow = 'hidden';
6275      // Always opens at the top, however far the reader scrolled last time.
6276      var body = document.querySelector('#termsModal .dn-modal-body');
6277      if (body) body.scrollTop = 0;
6278    }
6279    function closeTermsModal() {
6280      document.getElementById('termsModal').style.display = 'none';
6281      document.body.style.overflow = '';
6282    }
6283    document.getElementById('termsModal').addEventListener('click', function (e) {
6284      if (e.target === this) closeTermsModal();
6285    });
6286    document.addEventListener('keydown', function (e) {
6287      var tm = document.getElementById('termsModal');
6288      if (e.key === 'Escape' && tm && tm.style.display === 'flex') closeTermsModal();
6289    });
6290  </script>
6290
6291
6292  <!-- ===== Bug Report Modal ===== -->
6293  <div id="bugModal" class="dn-modal-overlay" style="display:none;">
6294    <div class="dn-modal" style="max-width:480px;">
6295      <div class="dn-modal-header">
6296        <div>
6297          <div
6298            style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:#ef4444;margin-bottom:2px;">
6299            IT Bug Report</div>
6300          <div style="font-size:16px;font-weight:700;color:var(--bl-text-primary);">Report an Issue</div>
6301        </div>
6302        <button onclick="closeBugModal()" class="dn-modal-close">&times;</button>
6303      </div>
6304      <div class="dn-modal-body" style="padding:20px 24px;">
6305        <p style="font-size:12px;color:var(--bl-text-muted);margin:0 0 12px;">Describe the issue you encountered.
6306          Include details like what you were doing and what went wrong.</p>
6307        <textarea id="bugReportText" rows="5"
6308          style="width:100%;border:1.5px solid var(--bl-border);border-radius:8px;padding:10px 12px;font-size:13px;font-family:inherit;resize:vertical;outline:none;transition:border-color 0.15s;"
6309          placeholder="e.g. When I click upload, nothing happens..."></textarea>
6310        <div id="bugReportMsg" style="display:none;margin-top:8px;font-size:12px;font-weight:600;"></div>
6311      </div>
6312      <div class="dn-modal-footer">
6313        <div></div>
6314        <div style="display:flex;gap:8px;">
6315          <button onclick="closeBugModal()" class="dn-modal-btn-secondary">Cancel</button>
6316          <button id="bugSubmitBtn" onclick="submitBugReport()" class="dn-modal-btn-primary">Submit Report</button>
6317        </div>
6318      </div>
6319    </div>
6320  </div>
6321  
6321<script>
6322    function openBugModal() {
6323      document.getElementById('bugReportText').value = '';
6324      var msg = document.getElementById('bugReportMsg');
6325      msg.style.display = 'none';
6326      document.getElementById('bugSubmitBtn').disabled = false;
6327      document.getElementById('bugSubmitBtn').textContent = 'Submit Report';
6328      document.getElementById('bugModal').style.display = 'flex';
6329      document.body.style.overflow = 'hidden';
6330      setTimeout(function () { document.getElementById('bugReportText').focus(); }, 100);
6331    }
6332    function closeBugModal() {
6333      document.getElementById('bugModal').style.display = 'none';
6334      document.body.style.overflow = '';
6335    }
6336    function submitBugReport() {
6337      var text = document.getElementById('bugReportText').value.trim();
6338      if (!text) { document.getElementById('bugReportText').style.borderColor = '#ef4444'; return; }
6339      var btn = document.getElementById('bugSubmitBtn');
6340      btn.disabled = true;
6341      btn.textContent = 'Sending...';
6342      fetch('/dashboard/api/bug-report', {
6343        method: 'POST',
6344        credentials: 'include',
6345        headers: { 'Content-Type': 'application/json' },
6346        body: JSON.stringify({
6347          dashboard: '',
6348          it_message: text,
6349          name: '',
6350          login: '',
6351          service: ''
6352        })
6353      }).then(function (r) {
6354        var msg = document.getElementById('bugReportMsg');
6355        if (r.ok) {
6356          msg.style.color = '#22c55e';
6357          msg.textContent = '✓ Bug report submitted. Thank you!';
6358          msg.style.display = 'block';
6359          btn.textContent = 'Sent!';
6360          setTimeout(closeBugModal, 1500);
6361        } else {
6362          msg.style.color = '#ef4444';
6363          msg.textContent = 'Failed to submit. Please try again.';
6364          msg.style.display = 'block';
6365          btn.disabled = false;
6366          btn.textContent = 'Submit Report';
6367        }
6368      }).catch(function () {
6369        var msg = document.getElementById('bugReportMsg');
6370        msg.style.color = '#ef4444';
6371        msg.textContent = 'Network error. Please try again.';
6372        msg.style.display = 'block';
6373        btn.disabled = false;
6374        btn.textContent = 'Submit Report';
6375      });
6376    }
6377    document.getElementById('bugModal').addEventListener('click', function (e) {
6378      if (e.target === this) closeBugModal();
6379    });
6380    document.getElementById('bugReportText').addEventListener('input', function () {
6381      this.style.borderColor = 'var(--bl-border)';
6382    });
6383  </script>
6383
6384
6385  
6386  
6386<script>
6387    var dnPaymentAmounts = {
6388      '1': '',
6389      '2': '',
6390      '3': '',
6391      '4': ''
6392    };
6393    var dnPaymentLabels = { '1': 'First Payment', '2': 'Second Payment', '3': 'Third Payment', '4': 'Fourth Payment' };
6394
6395    function update_mollie(payment) {
6396      var amount = dnPaymentAmounts[payment] || '';
6397      if (!amount) return;
6398
6399      // Set Stripe fallback link
6400      document.getElementById('dn_stripe_link').href = '/stripe-payment/' + payment;
6401
6402      // Set modal title and the amount inside the card button
6403      document.getElementById('mollieModalTitle').textContent = dnPaymentLabels[payment] + ' — € ' + amount;
6404      document.getElementById('mollie_pay_amount').textContent = '€ ' + amount;
6405
6406      // Reset to loading state
6407      document.getElementById('mollieLoadingState').style.display = 'block';
6408      document.getElementById('mollieOpenedState').style.display = 'none';
6409      document.getElementById('mollieErrorState').style.display = 'none';
6410
6411      // Show modal
6412      document.getElementById('mollieCheckoutModal').style.display = 'flex';
6413      document.body.style.overflow = 'hidden';
6414
6415      // Ensure amount has decimal places
6416      if (amount.indexOf('.') === -1) amount = amount + '.00';
6417
6418      // Create Mollie payment via API
6419      fetch('/api/dashboard/create-mollie-payment', {
6420        method: 'POST',
6421        credentials: 'include',
6422        headers: { 'Content-Type': 'application/json' },
6423        body: JSON.stringify({ mollie_amount: amount, mollie_payment: payment })
6424      })
6425      .then(function(r) { return r.json(); })
6426      .then(function(data) {
6427        if (data.checkout_url) {
6428          document.getElementById('mollie_reopen_link').href = data.checkout_url;
6429          document.getElementById('mollieLoadingState').style.display = 'none';
6430          document.getElementById('mollieOpenedState').style.display = 'block';
6431          // No automatic redirect. The client leaves only when they click the
6432          // card button, which is what keeps the Stripe alternative reachable.
6433          // The link navigates in the same tab rather than a popup: a second
6434          // tab gets blocked on some phone browsers, and the ones that allow it
6435          // leave the client paying in one tab with a stale dashboard in another.
6436        } else {
6437          document.getElementById('mollieLoadingState').style.display = 'none';
6438          document.getElementById('mollieErrorState').style.display = 'block';
6439        }
6440      })
6441      .catch(function(err) {
6442        console.error('Mollie payment error:', err);
6443        document.getElementById('mollieLoadingState').style.display = 'none';
6444        document.getElementById('mollieErrorState').style.display = 'block';
6445      });
6446    }
6447
6448    function closeMollieModal() {
6449      document.getElementById('mollieCheckoutModal').style.display = 'none';
6450      document.body.style.overflow = '';
6451    }
6452    document.getElementById('mollieCheckoutModal').addEventListener('click', function(e) { if (e.target === this) closeMollieModal(); });
6453    document.addEventListener('keydown', function(e) { var m = document.getElementById('mollieCheckoutModal'); if (e.key === 'Escape' && m && m.style.display === 'flex') closeMollieModal(); });
6454  </script>
6454
6455  
6456
6457  <!-- ===== Guided Dashboard Tour (Shepherd.js) ===== -->
6458  <style>
6459    .shepherd-element.bl-tour {
6460      background: var(--bl-bg-card);
6461      border: 1px solid var(--bl-border);
6462      border-radius: var(--bl-radius-lg, 14px);
6463      box-shadow: var(--bl-shadow-lg, 0 10px 40px rgba(0, 0, 0, 0.12));
6464      max-width: 380px;
6465      font-family: inherit;
6466    }
6467    .shepherd-element.bl-tour .shepherd-content { border-radius: inherit; padding: 0; }
6468    .shepherd-element.bl-tour .shepherd-header { background: transparent; padding: 18px 20px 0; }
6469    .shepherd-element.bl-tour .shepherd-title { font-size: 16px; font-weight: 700; color: var(--bl-text-primary); }
6470    .shepherd-element.bl-tour .shepherd-text { padding: 8px 20px 4px; font-size: 13.5px; line-height: 1.6; color: var(--bl-text-secondary); }
6471    .shepherd-element.bl-tour .shepherd-footer { padding: 12px 20px 18px; display: flex; align-items: center; gap: 8px; }
6472    .shepherd-element.bl-tour .shepherd-button { border-radius: 8px; font-size: 13px; font-weight: 600; padding: 8px 16px; border: none; cursor: pointer; transition: all .15s; }
6473    .shepherd-element.bl-tour .shepherd-button:not(.shepherd-button-secondary) { background: var(--bl-primary); color: #fff; }
6474    .shepherd-element.bl-tour .shepherd-button:not(.shepherd-button-secondary):hover { background: var(--bl-primary-dark, #0D6EE1); }
6475    .shepherd-element.bl-tour .shepherd-button.shepherd-button-secondary { background: var(--bl-bg); color: var(--bl-text-secondary); }
6476    .shepherd-element.bl-tour .shepherd-button.shepherd-button-secondary:hover { background: var(--bl-border); }
6477    .shepherd-element.bl-tour .shepherd-cancel-icon { color: var(--bl-text-muted); }
6478    .shepherd-element.bl-tour .shepherd-arrow:before { background: var(--bl-bg-card); border: 1px solid var(--bl-border); }
6479    .bl-tour-progress { font-size: 12px; color: var(--bl-text-muted); font-weight: 600; }
6480    .shepherd-modal-overlay-container { z-index: 9997; }
6481    .shepherd-element.bl-tour { z-index: 9999; }
6482    #tourBtn:hover { background: var(--bl-bg); color: var(--bl-text-primary); border-color: var(--bl-text-muted); }
6483    /* Labels drop away on narrow screens, leaving both as icon-only buttons.
6484       No margin here either — the header's flex gap already spaces them. */
6485    @media (max-width: 600px) { .dn-tour-btn-label { display: none; }
6486      #tourBtn, #notesBtn { padding: 7px 10px; } }
6487  </style>
6488  
6488<script>
6489    (function () {
6490      // True until this client has been shown the tour once — including every
6491      // client from before the flag existed — and cleared the moment it opens.
6492      // It used to live in localStorage, which meant a second browser, a
6493      // cleared cache or a private window brought the tour back for someone
6494      // who had already been through it.
6495      var TOUR_PENDING = false;
6496
6497      function markTourSeen() {
6498        try {
6499          fetch('/api/dashboard/tour-seen', { method: 'POST' });
6500        } catch (e) {}
6501      }
6502
6503      // Tour copy names the client's actual contact role, which is
6504      // "immigration specialist" while no case manager is assigned.
6505      var CONTACT_ROLE_LC = "";
6506
6507      // Step definitions; steps whose target is missing/hidden are skipped automatically,
6508      // so conditional sections (timeline, payments, etc.) never break the tour.
6509      function stepsForPage() {
6510        var defs = [
6511          { attach: null, title: 'Welcome to your dashboard 👋',
6512            text: 'This is your home base for your visa application. Here is a quick tour of how everything works.' },
6513          { attach: '.dn-stats-row', on: 'bottom', title: 'Your snapshot',
6514            text: 'At a glance: your service type, your overall progress, and your profile. The progress bar fills up as you complete tasks.' },
6515          { attach: '#current_task_card', on: 'left', title: 'Your current task',
6516            text: 'The single most important thing to do next. Follow this to keep your application moving forward.' },
6517          { attach: '[data-tour="tasks"]', on: 'top', title: 'Your full checklist',
6518            text: 'Every step of your process, grouped by stage. Click any task to expand it, watch a how-to video, or see the details.' },
6519          { attach: '#aiCard', on: 'right', title: 'Carlos',
6520            text: 'Ask anything about your visa process and get instant answers, any time of day.' },
6521          { attach: '#dnPaymentsCard', on: 'top', title: 'Payments',
6522            text: 'Review and pay your fees here in easy installments — no large upfront payment required.' },
6523          { attach: '[data-tour="timeline"]', on: 'top', title: 'Your visa journey',
6524            text: 'A timeline of the key milestones ahead, from your onboarding meeting all the way to arriving in Spain.' },
6525          { attach: '[data-tour="documents"]', on: 'top', title: 'Documents',
6526            text: 'Upload the documents we request, and download any files your ' + CONTACT_ROLE_LC + ' shares with you.' },
6527          { attach: '#dnCaseManagerCard', on: 'top', title: 'Your ' + CONTACT_ROLE_LC,
6528            text: 'Message your dedicated ' + CONTACT_ROLE_LC + ' or check your upcoming meetings right here.' },
6529          { attach: '.dn-avatar-dropdown', on: 'bottom-end', title: 'Your account',
6530            text: 'Edit your profile — including your profile picture, which appears here and on your snapshot card — or log out from this menu whenever you need to.' },
6531          { attach: '#tourBtn', on: 'bottom-end', title: "That's it! 🎉",
6532            text: 'You can replay this tour any time by clicking “Take a tour” up here. Good luck with your application!' }
6533        ];
6534        return defs.filter(function (d) {
6535          if (!d.attach) return true;
6536          var el = document.querySelector(d.attach);
6537          if (!el) return false;
6538          if (el.offsetParent !== null) return true; // exists and visible
6539          // Hidden only because another section is open: the tour switches
6540          // sections per step, so keep it. A card the client hid with the eye
6541          // control (inline display:none) stays skipped, as before.
6542          return !!(window.dnNav && dnNav.groupFor(el) && el.style.display !== 'none');
6543        });
6544      }
6545
6546      window.startDashboardTour = function () {
6547        if (typeof Shepherd === 'undefined') return; // CDN unavailable → no-op
6548        var tour = new Shepherd.Tour({
6549          useModalOverlay: true,
6550          defaultStepOptions: {
6551            classes: 'bl-tour',
6552            scrollTo: { behavior: 'smooth', block: 'center' },
6553            cancelIcon: { enabled: true },
6554            modalOverlayOpeningPadding: 6,
6555            modalOverlayOpeningRadius: 10
6556          }
6557        });
6558        var steps = stepsForPage();
6559        steps.forEach(function (d, i) {
6560          var buttons = [];
6561          if (i > 0) buttons.push({ text: 'Back', classes: 'shepherd-button-secondary', action: tour.back });
6562          buttons.push({ text: (i === steps.length - 1 ? 'Finish' : 'Next'), action: tour.next });
6563          var step = {
6564            title: d.title,
6565            text: '<div>' + d.text + '</div>' +
6566                  '<div class="bl-tour-progress" style="margin-top:10px;">Step ' + (i + 1) + ' of ' + steps.length + '</div>',
6567            buttons: buttons
6568          };
6569          if (d.attach) {
6570            step.attachTo = { element: d.attach, on: d.on || 'bottom' };
6571            // Open the section the target lives in, and give layout a frame
6572            // to settle before Shepherd measures the card's position.
6573            step.beforeShowPromise = function () {
6574              try {
6575                var el = document.querySelector(d.attach);
6576                if (el && window.dnNavRevealFor) dnNavRevealFor(el);
6577              } catch (e) {}
6578              return new Promise(function (resolve) {
6579                requestAnimationFrame(function () { requestAnimationFrame(resolve); });
6580              });
6581            };
6582          }
6583          tour.addStep(step);
6584        });
6585        // Each step opens whichever section its target lives in, so the tour
6586        // ends with the client parked on Documents or Partners — wherever the
6587        // last step happened to point — rather than where they started. Send
6588        // them back to Overview, which is where the dashboard opens and the
6589        // section that answers "what now?".
6590        //
6591        // Same on cancel: someone who backs out halfway has been moved through
6592        // two or three sections without choosing any of them.
6593        var finish = function () {
6594          try { if (window.dnNav) window.dnNav.show('overview'); } catch (e) {}
6595        };
6596        tour.on('complete', finish);
6597        tour.on('cancel', finish);
6598        tour.start();
6599      };
6600
6601      // Auto-start on this client's first dashboard visit, and only that once.
6602      // Every later visit gets the dashboard straight away; the "Take a tour"
6603      // button is still there for anyone who wants it again.
6604      function maybeAutoStart() {
6605        if (!TOUR_PENDING || typeof Shepherd === 'undefined') return;
6606        // Not while the login/signup modal owns the screen: the tour renders
6607        // beneath it (z-index 9999 vs 99999) so nobody sees it, but Shepherd's
6608        // overlay still installs a window-level touchmove preventDefault that
6609        // froze all scrolling on phones — including the signup form itself.
6610        // The record stays pending, so the tour still greets the client on
6611        // their first real (logged-in) dashboard visit.
6612        if (document.getElementById('loginModal')) return;
6613        // A freshly signed-up client is greeted by the eligibility quiz, not
6614        // two overlays at once. The tour stays pending for the next visit.
6615        if (window.QUIZ_AUTO_OPEN) return;
6616        // Same reasoning, in the webinar popup's favor: if it's about to show
6617        // this visit, the tour steps aside and stays pending for the next one.
6618        if (false) return;
6619        // Marked the moment it opens, not when it ends: a client who closes
6620        // the tab halfway through has still had their one showing.
6621        markTourSeen();
6622        setTimeout(window.startDashboardTour, 700);
6623      }
6624      if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', maybeAutoStart);
6625      else maybeAutoStart();
6626    })();
6627  </script>
6627
6628
6629  <!-- ============ Section navigation logic ============ -->
6630  
6630<script>
6631    (function () {
6632      // Cards without an id or data-hideable hook are found by their title.
6633      function cardByTitle(txt) {
6634        var titles = document.querySelectorAll('.dn-card .dn-card-title');
6635        for (var i = 0; i < titles.length; i++) {
6636          if (titles[i].textContent.trim().indexOf(txt) === 0) return titles[i].closest('.dn-card');
6637        }
6638        return null;
6639      }
6640
6641      // Resolve a selector list to the elements present for this client, and
6642      // pull in each card's "Show …" button (the eye-hide system inserts it as
6643      // the card's previous sibling) so it changes section with its card.
6644      function els(list) {
6645        var out = [];
6646        list.forEach(function (sel) {
6647          var el = typeof sel === 'string' ? document.querySelector(sel) : sel;
6648          if (!el || out.indexOf(el) !== -1) return;
6649          var prev = el.previousElementSibling;
6650          if (prev && prev.classList.contains('dn-show-btn') && out.indexOf(prev) === -1) out.push(prev);
6651          out.push(el);
6652        });
6653        return out;
6654      }
6655
6656      var GROUPS = {
6657        overview:  els(['.dn-stats-row', '#carlosOverviewCard', '#dnPaymentsShowBtn', '#dnPaymentsCard',
6658                        '[data-hideable="express"]', '[data-hideable="timeline"]',
6659                        '#current_task_card', '[data-hideable="activity"]']),
6660        tasks:     els(['[data-hideable="tasks"]']),
6661        documents: els(['#dnDocumentsCard']),
6662        included:  els(['#dnIncludedCard']),
6663        help:      els(['#aiCard', '#dnCaseManagerCard']),
6664        perks:     els([cardByTitle('Recommended Services')]),
6665        socials:   els(['[data-hideable="referral"]', '[data-hideable="social"]'])
6666      };
6667      var ALL = [];
6668      Object.keys(GROUPS).forEach(function (g) {
6669        GROUPS[g].forEach(function (el) { if (ALL.indexOf(el) === -1) ALL.push(el); });
6670      });
6671      var grid2 = document.querySelector('.dn-grid-2');
6672      var current = null;
6673
6674      function groupFor(el) {
6675        var gs = Object.keys(GROUPS);
6676        for (var i = 0; i < gs.length; i++) {
6677          var list = GROUPS[gs[i]];
6678          for (var j = 0; j < list.length; j++) {
6679            if (list[j] === el || list[j].contains(el)) return gs[i];
6680          }
6681        }
6682        return null;
6683      }
6684
6685      function show(group, opts) {
6686        opts = opts || {};
6687        if (!GROUPS[group] || !GROUPS[group].length) group = 'overview';
6688        current = group;
6689        ALL.forEach(function (el) { el.classList.add('dnv-hide'); });
6690        GROUPS[group].forEach(function (el) { el.classList.remove('dnv-hide'); });
6691        if (grid2) {
6692          // Count only real cards, not the eye-hide "Show" buttons the hide
6693          // system inserts as grid children (they are display:none until a
6694          // card is hidden, but would otherwise inflate the count and keep a
6695          // lone card in a two-column grid at half width).
6696          var visible = Array.prototype.filter.call(grid2.children, function (c) {
6697            return !c.classList.contains('dnv-hide') && !c.classList.contains('dn-show-btn');
6698          }).length;
6699          grid2.classList.toggle('dnv-single', visible === 1);
6700          grid2.classList.toggle('dnv-hide', visible === 0);
6701        }
6702        document.querySelectorAll('.dnv-pill').forEach(function (p) {
6703          var on = p.dataset.dnv === group;
6704          p.classList.toggle('on', on);
6705          // Keep the active pill in view when the bar scrolls on mobile.
6706          if (on && p.scrollIntoView && !opts.noScroll) {
6707            try { p.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } catch (e) {}
6708          }
6709        });
6710        markAvatarNavActive(group);
6711        // Carlos chat should open on its newest message so the client can just
6712        // keep typing. Its history loads while the Help section is still hidden
6713        // (scrollHeight is 0 then, so the append-time scroll never sticks), so
6714        // re-pin it to the bottom once the card is actually visible.
6715        if (group === 'help') {
6716          requestAnimationFrame(function () {
6717            var m = document.getElementById('aiMessages');
6718            if (m) m.scrollTop = m.scrollHeight;
6719          });
6720        }
6721        if (!opts.noScroll) window.scrollTo(0, 0);
6722        if (!opts.noHash) { try { history.replaceState(null, '', '#' + group); } catch (e) {} }
6723      }
6724
6725      // Public hooks: the tour and the scroll helpers use these to open the
6726      // section a target card lives in before pointing at it.
6727      window.dnNav = { show: show, groupFor: groupFor };
6728      window.dnNavRevealFor = function (el) {
6729        var g = el && groupFor(el);
6730        if (g && g !== current) show(g, { noScroll: true, noHash: true });
6731      };
6732
6733      // Anything on the page that scrolls to a card (Go to current task, the
6734      // pay buttons, document links) first opens that card's section.
6735      var origSIV = Element.prototype.scrollIntoView;
6736      Element.prototype.scrollIntoView = function () {
6737        try { window.dnNavRevealFor(this); } catch (e) {}
6738        return origSIV.apply(this, arguments);
6739      };
6740
6741      // Marks the open section in the avatar menu. Defined even when the menu
6742      // has no links yet, because show() calls it on the first render.
6743      function markAvatarNavActive(group) {
6744        document.querySelectorAll('.dn-avatar-nav-item').forEach(function (link) {
6745          var on = link.getAttribute('data-dnv') === group;
6746          link.style.color = on ? 'var(--bl-primary)' : '';
6747          link.style.background = on ? 'var(--bl-primary-bg)' : '';
6748          link.style.fontWeight = on ? '600' : '';
6749        });
6750      }
6751
6752      // Pills: hide the ones whose section is empty for this client.
6753      document.querySelectorAll('.dnv-pill').forEach(function (p) {
6754        if (!(GROUPS[p.dataset.dnv] || []).length) p.classList.add('gone');
6755        p.addEventListener('click', function () { show(p.dataset.dnv); });
6756      });
6757
6758      // How the wrapped bar divides on a phone. Thirds suit six sections and
6759      // five; four and two want halves, or the last row is a single stretched
6760      // pill. Only settled once here — sections do not appear or vanish after
6761      // load. The class is inert above the phone breakpoint.
6762      var navIn = document.getElementById('dnvNavIn');
6763      if (navIn) {
6764        var shownPills = navIn.querySelectorAll('.dnv-pill:not(.gone)').length;
6765        navIn.classList.toggle('dnv-even', shownPills === 4 || shownPills === 2);
6766      }
6767
6768      // The same sections, mirrored into the avatar menu. Built from the pills
6769      // themselves so a section hidden there is absent here too, and so the
6770      // labels can never disagree. The count badge is left behind — "Tasks 14/16"
6771      // is a bar affordance, and the number would go stale in a menu that is
6772      // only rebuilt on load.
6773      var avatarNav = document.getElementById('avatarNavLinks');
6774      if (avatarNav) {
6775        navIn.querySelectorAll('.dnv-pill:not(.gone)').forEach(function (pill) {
6776          var group = pill.dataset.dnv;
6777          var link = document.createElement('a');
6778          link.className = 'dn-avatar-menu-item dn-avatar-nav-item';
6779          link.href = 'javascript:void(0)';
6780          link.setAttribute('data-dnv', group);
6781          // firstChild is the label text node;
6781 the badge is a following <span>.
6782          link.textContent = (pill.firstChild && pill.firstChild.textContent || group).trim();
6783          link.addEventListener('click', function () {
6784            show(group);
6785            closeAvatarDropdown();
6786          });
6787          avatarNav.appendChild(link);
6788        });
6789        markAvatarNavActive(current);
6790      }
6791
6792      // The bar sits under the sticky header; keep its offset matched to the
6793      // header's real height (it changes between breakpoints).
6794      var header = document.querySelector('.dn-header');
6795      var nav = document.getElementById('dnvNav');
6796      function syncTop() {
6797        if (header && nav) nav.style.top = (header.offsetHeight - 1) + 'px';
6798      }
6799      syncTop();
6800      window.addEventListener('resize', syncTop);
6801      if (window.ResizeObserver && header) new ResizeObserver(syncTop).observe(header);
6802
6803      // Initial section: a #hash deep-link wins; landing back from a payment
6804      // opens Overview, where the payments card now lives, so the success
6805      // modal closes onto the right section.
6806      var initial = (location.hash || '').replace('#', '');
6807      if (!(GROUPS[initial] && GROUPS[initial].length)) initial = 'overview';
6808      if (document.getElementById('dnPaymentSuccessModal')) initial = 'overview';
6809      window.addEventListener('hashchange', function () {
6810        var g = (location.hash || '').replace('#', '');
6811        if (GROUPS[g] && GROUPS[g].length) show(g);
6812      });
6813      show(initial, { noScroll: true, noHash: true });
6814
6815      // Card entry animations belong to page load only — see .dnv-ready CSS.
6816      // The longest load animation is 0.5s + 0.4s delay; after that, freeze
6817      // them so cards don't replay their entrance on every section switch.
6818      setTimeout(function () { document.body.classList.add('dnv-ready'); }, 1000);
6819    })();
6820  </script>
6820
6821
6822<script src="/static/js/carlos_md.js?v=3"></script>
6822
6823
6824<!-- In-place page translation, same widget and cookie as the marketing site
6825     (see base.html for the full rationale). Loads only when a language is
6826     picked or the googtrans cookie already carries one. -->
6827<div id="bu_gt_holder" class="notranslate" style="display:none" aria-hidden="true"></div>
6828<script>
6829(function () {
6830  function readLang() {
6831    var m = document.cookie.match(/(?:^|;\s*)googtrans=\/en\/(\w+)/);
6832    return m ? m[1] : 'en';
6833  }
6834  function setCookie(value, maxAge) {
6835    var tails = ['path=/'];
6836    if (/[a-z]/i.test(location.hostname) && location.hostname.indexOf('.') > -1) {
6837      tails.push('path=/;domain=.' + location.hostname.replace(/^www\./, ''));
6838    }
6839    tails.forEach(function (t) {
6840      document.cookie = 'googtrans=' + value + ';' + t + (maxAge != null ? ';max-age=' + maxAge : '');
6841    });
6842  }
6843  var loaded = false;
6844  function applyLang(lang, tries) {
6845    tries = tries || 0;
6846    var combo = document.querySelector('select.goog-te-combo');
6847    if (!combo || combo.options.length < 2) {
6848      if (tries < 50) setTimeout(function () { applyLang(lang, tries + 1); }, 200);
6849      return;
6850    }
6851    combo.value = lang;
6852    combo.dispatchEvent(new Event('change'));
6853    if (combo.value !== lang && tries < 50) {
6854      setTimeout(function () { applyLang(lang, tries + 1); }, 200);
6855      return;
6856    }
6857    setTimeout(function () {
6858      if (document.documentElement.lang === 'en' && readLang() === lang && tries < 50) {
6859        applyLang(lang, tries + 5);
6860      }
6861    }, 1200);
6862  }
6863  window.buGTInit = function () {
6864    new google.translate.TranslateElement({ pageLanguage: 'en', autoDisplay: false }, 'bu_gt_holder');
6865    var lang = readLang();
6866    if (lang !== 'en') applyLang(lang);
6867  };
6868  function loadWidget() {
6869    if (loaded) return;
6870    loaded = true;
6871    var s = document.createElement('script');
6872    s.src = 'https://translate.google.com/translate_a/element.js?cb=buGTInit';
6873    s.async = true;
6874    document.body.appendChild(s);
6875  }
6876  window.buSetLang = function (lang) {
6877    var menu = document.getElementById('bulangMenu');
6878    if (menu) menu.classList.remove('open');
6879    var cur = document.getElementById('bulangCur');
6880    if (cur) cur.textContent = lang.toUpperCase();
6881    if (lang === 'en') {
6882      setCookie('', 0);
6883      location.reload();
6884      return;
6885    }
6886    setCookie('/en/' + lang);
6887    if (loaded) { applyLang(lang); } else { loadWidget(); }
6888  };
6889  window.bulangToggle = function () {
6890    var menu = document.getElementById('bulangMenu');
6891    var btn = document.getElementById('bulangBtn');
6892    if (!menu) return;
6893    var open = menu.classList.toggle('open');
6894    if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
6895  };
6896  document.addEventListener('click', function (e) {
6897    if (!e.target.closest('#bulang')) {
6898      var menu = document.getElementById('bulangMenu');
6899      if (menu) menu.classList.remove('open');
6900    }
6901  });
6902  var lang = readLang();
6903  if (lang !== 'en') {
6904    var cur = document.getElementById('bulangCur');
6905    if (cur) cur.textContent = lang.toUpperCase();
6906    loadWidget();
6907  }
6908})();
6909</script>
6909
6910</body>
6911
6912</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.