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 & 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 & 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">×</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 · 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 — 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–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–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 — 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="all"]'))" 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. · 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 · 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 & 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 ' · <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 => ({ '&': '&', '<': '<', '>': '>', "'": "'", '"': '"' }[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 — your case manager doesn't see this. 4694 </div> 4695 </div> 4696 <button onclick="closeNotesModal()" class="dn-modal-close" aria-label="Close notes">×</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">×</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">×</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">×</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">×</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">×</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 & 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">×</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 & <em>Conditions</em></div> 5924 </div> 5925 <button onclick="closeTermsModal()" class="dn-modal-close">×</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 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 & 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 & 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 protected]</span></a>.</p> 6210 6211 <h3 class="pp-subheading">Retention & 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">×</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.