/* Language gate — hide the page until translations.js applies the stored
   (non-EN) language, so Danish users never see a flash of English on load or
   navigation. The `ds-lang-loading` class is added by a tiny inline <head>
   script ONLY when a non-default language is stored, and removed at the end of
   applyTranslations(). Kept unlayered so it reliably wins the cascade; a safety
   timeout in the inline script guarantees the page can never stay hidden. */
html.ds-lang-loading body { visibility: hidden; }

/* Page-entry transitions — soften the hard "snap" when navigating between pages.
   Rides the two existing reveal gates so motion only plays once content is
   actually visible (never while hidden), and never races the flash prevention:
     • marketing pages (no <main>) fade in on first paint;
     • Danish pages fade in when the language gate lifts (applyTranslations);
     • data pages fade + rise <main> in when DS.dom.reveal() clears the data gate.
   Unconditional (not gated on prefers-reduced-motion) — the site defaults to
   animations on regardless of the OS setting; only the manual html.ds-reduce-motion
   toggle below collapses this to an instant snap.

   Do NOT widen the body rule below to a bare `body`: a whole-page opacity fade
   flashes the root backdrop on two-tone / purple-hero pages (login, Browse
   partners). Pages with a <main> stay off the body fade on purpose. */
@keyframes ds-page-in { from { opacity: 0; } to { opacity: 1; } }
/* Marketing pages have no <main> gate — fade the whole page in on load. */
body:not(:has(main)) { animation: ds-page-in .24s ease-out both; }
/* Danish pages sit behind the language gate; skip the load keyframe and fade
   in when the gate lifts instead. */
html.ds-lang-loading body { animation: none; opacity: 0; transition: opacity .24s ease-out; }
/* Data pages hold <main> behind the data gate; fade + rise it in on reveal().
   The app pages' header now sits in `.page-band` OUTSIDE <main>, so it rides
   the same gate — but via its inner .container, never the section itself: the
   band has to keep painting its gradient from the first frame, or the page
   shows a light strip where the band belongs and then snaps to purple. */
main, .page-band > .container { transition: opacity .2s ease-out, transform .2s ease-out; }
body.ds-pending main,
body.ds-pending .page-band > .container { opacity: 0; transform: translateY(8px); }

/* Language switch — a brand-colour wash. #ds-lang-veil is a fixed overlay
   created on demand by toggleLang() in js/translations.js: it fades up to full
   cover, the strings swap behind it, and it fades back out. toggleLang() skips
   it entirely under either reduced-motion signal and just swaps.

   Deliberately a separate overlay rather than fading the PAGE. Fading body's
   own opacity exposes whatever sits behind it — on a two-tone page like the
   purple home hero under a white nav that reads as a flash of the wrong colour
   — which is the same trap documented on .ds-hero-in further down. Here the
   page underneath is never touched at all; something opaque simply passes over
   it.

   ── Why a fade and not the old curtain ──
   This used to sweep an oversized tilted band across the screen, and it went
   through several rounds of iOS-only rendering artefacts: a mask that forced a
   per-frame re-rasterise, then a layer so large (280vw) that Safari split it
   into tiles which rasterise asynchronously — the ones along the moving edge
   flashed as blocky squares before they were filled in. All of it came from
   the same root: a big layer in motion. Nothing here is larger than the screen
   and nothing moves; only opacity animates, which is the cheapest thing a
   compositor can be asked to do. Keep it that way. If this ever wants to be
   fancier again, that whole class of bug comes back with it. */
#ds-lang-veil {
  position: fixed;
  inset: 0;
  z-index: 99999; /* above every other fixed layer (cookie banner, overlays top out at 9999) */
  pointer-events: none;
  opacity: 0;     /* inert at rest — no class or visibility juggling needed */
  background: linear-gradient(135deg, #8A74E4, #5962C0);
}

/* Manual "Reduce animations" preference. The inline head guard + js/dom.js set
   html.ds-reduce-motion from localStorage — the SOLE source of truth; it does
   NOT follow the OS prefers-reduced-motion setting (the site defaults to
   animations on regardless of that), only this explicit opt-out. Collapses
   every animation/transition to an instant jump to its end state so decorative
   motion (entrance fades, scroll reveals, the rotating headline, the hero
   cloud video) stops while content still ends up fully visible.
   Unlayered + !important so it overrides the motion defined unconditionally
   above. */
html.ds-reduce-motion *,
html.ds-reduce-motion *::before,
html.ds-reduce-motion *::after {
  animation-duration: 0.001ms !important;
  animation-delay: 0ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.001ms !important;
  transition-delay: 0ms !important;
  scroll-behavior: auto !important;
}

@layer reset, tokens, base, nav, components, hero, dashboards, footer, layout, utilities, responsive;

@layer reset {
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  html {
    -webkit-text-size-adjust: 100%;
    /* Reserve the scrollbar's width permanently, whether or not it's actually
       showing. Without this, a page whose height crosses the "needs a
       scrollbar" threshold mid-load (e.g. a data page starts tall while
       async-loading content sits invisible-but-still-in-flow behind the
       pending gate, then shrinks once real/empty content replaces it) shifts
       every centred element sideways by the scrollbar's width the instant it
       appears or disappears. Only visible on tall viewports (5K iMac etc.)
       where short pages don't need a scrollbar at all once loaded. */
    scrollbar-gutter: stable;
    /* Overscroll (rubber-band) backdrop colour, and the colour shown behind the
       page while it fades in on load. The browser fills the overscroll area with
       the root's SOLID background colour (gradients / fixed backgrounds are
       unreliable here). js/dom.js swaps this to match the nav near the top of the
       page and the footer near the bottom. Pages whose top area isn't the default
       light nav backdrop (e.g. the home hero) override --top-bg so the load fade
       never flashes a mismatched colour. */
    --top-bg: #F1F2FD;
    /* The other end of the same swap — the footer's colour. A token rather
       than a constant in js/dom.js because the dark theme moves the footer
       into the page's own hue (see css/theme-dark.css). */
    --bottom-bg: #0F0B0A;
    background-color: var(--top-bg);
  }
  /* Sticky footer: on short pages (e.g. an empty state) with a tall viewport
     (5K iMac etc.), content used to end well above the fold and leave this
     purple background exposed as a big gap below the footer. flex + the
     footer's margin-top:auto (see footer.css) pins it to the viewport bottom
     instead — a no-op on normal pages where content already fills the screen. */
  body { min-height: 100vh; background: #5962C0; display: flex; flex-direction: column; }
  img, svg { display: block; max-width: 100%; }
  button { font: inherit; cursor: pointer; }
  a { color: inherit; text-decoration: none; }
  input, textarea, select { font: inherit; }
}

@layer tokens {
  :root {
    --bg:      oklch(53.6% 0.207 276);
    --surface: oklch(100% 0 0);
    --fg:      oklch(100% 0 0);
    /* Muted text on the purple bg. Raised from 80% (3.2:1 — failed AA body) to
       94% (4.6:1) so hero subtitles clear AA; the faint lavender tint keeps them
       reading as secondary next to pure-white headings. */
    --muted:   oklch(94% 0.03 276);
    --border:  oklch(100% 0 0 / 0.12);
    --accent:  oklch(100% 0 0);
    --accent-hover: oklch(69% 0.15 276);
    --accent-light: oklch(100% 0 0 / 0.1);
    /* Unified brand purple for buttons/icons/badges, muted to match the band
       palette (chroma .207 -> .145, same recipe as .band-deep etc.) instead of
       the old saturated #5456E2/#4345c9 hex pair those elements carried. Named
       apart from --bg/--accent because those still double as the page's own
       deep-purple backdrop token in places this palette pass deliberately
       left alone. */
    --accent-solid: oklch(53.6% 0.145 276);
    --accent-solid-hover: oklch(46% 0.145 276);
    --space-xs: 8px;
    --space-sm: 16px;
    --space-md: 24px;
    --space-lg: 60px;
    --space-xl: 100px;
    --space-2xl: 140px;
    --elevation-1: 0 1px 2px oklch(0% 0 0 / 0.04), 0 1px 4px oklch(0% 0 0 / 0.04);
    --elevation-2: 0 4px 12px oklch(0% 0 0 / 0.06), 0 2px 4px oklch(0% 0 0 / 0.04);
    --elevation-3: 0 8px 24px oklch(0% 0 0 / 0.08), 0 4px 8px oklch(0% 0 0 / 0.04);
    --font-display: 'Inter', -apple-system, system-ui, sans-serif;
    --font-body: 'Inter', -apple-system, system-ui, sans-serif;
    --font-mono: ui-monospace, 'JetBrains Mono', Menlo, monospace;
    --radius-sm: 8px;
    --radius-md: 9px;
    --radius-lg: 10px;
    --radius-xl: 24px;
    --max-width: 1400px;
    --content-pad: clamp(20px, 5vw, 56px);
  }
}

@layer base {
  body {
    font-family: var(--font-body);
    font-size: 16px;
    line-height: 1.6;
    color: var(--fg);
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
  }
  h1, h2, h3, h4, h5, h6 {
    font-family: var(--font-display);
    font-weight: 600;
    line-height: 1.2;
    color: inherit;
  }
  h1 { font-size: clamp(36px, 5vw, 64px); letter-spacing: -0.02em; }
  h2 { font-size: clamp(28px, 3.5vw, 48px); letter-spacing: -0.015em; }
  h3 { font-size: clamp(22px, 2.5vw, 32px); letter-spacing: -0.01em; }
  h4 { font-size: clamp(18px, 1.8vw, 24px); }
  p { color: inherit; opacity: .88; }
  p + p { margin-top: 1em; }

  /* .rs-card = the white setup card on role-select + become-partner. Without it
     here, those pages' headings/labels inherited the page-level white --fg and
     rendered white-on-white. */
  .card, .auth-card, .search-card, .compare-agency-card,
  .review-card, .agency-card, .step, .nav-links.open,
  .profile-sidebar .card, .brief-sidebar .card, .rs-card,
  .dash-tabs,
  input, select, textarea, .hero-search {
    --fg:      oklch(18% 0.015 276);
    --muted:   oklch(50% 0.015 276);
    --border:  oklch(88% 0.005 276);
    --accent:  oklch(53.6% 0.145 276);
    --accent-light: oklch(92% 0.04 276 / 0.25);
    color: var(--fg);
  }
}

@layer layout {
  .container {
    width: 100%;
    max-width: var(--max-width);
    margin: 0 auto;
    padding-inline: var(--content-pad);
  }
  .section { padding-block: var(--space-xl); }
  .section-spacious { padding-block: var(--space-2xl); }
  .section-alt { padding-block: var(--space-xl); }
  .grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-md); }
  .grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-md); }
  .grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-md); }
  .grid-12 { display: grid; grid-template-columns: repeat(12, 1fr); gap: var(--space-md); }
  .flex { display: flex; }
  .flex-col { flex-direction: column; }
  .items-center { align-items: center; }
  .justify-center { justify-content: center; }
  .justify-between { justify-content: space-between; }
  .gap-xs { gap: var(--space-xs); }
  .gap-sm { gap: var(--space-sm); }
  .gap-md { gap: var(--space-md); }
  .gap-lg { gap: var(--space-lg); }
  .text-center { text-align: center; }
  .mx-auto { margin-inline: auto; }

  /* ── Page bands (interior pages) ─────────────────────────────────────────
     The home page alternates deep-purple and light bands down its length
     (.section-glow / .section-light, css/hero.css) — that alternation is the
     single biggest reason it reads as newer than everything else. Every other
     page was still what the home page used to be: one unbroken field of the
     body's #5456E2 from the nav to the footer, with white cards floating on
     it, and every secondary control (counts, sort, chips, pagination, the
     mobile Filters button) rebuilt as a low-alpha white ghost purely to
     survive that backdrop.

     These two classes are the same two bands, promoted out of the home page so
     any page can use them. Identical values to .section-glow / .section-light,
     so a page dressed with them resolves the shared components (.card, .btn,
     .tag, form fields) exactly the way the home page's own sections do.

     `.page-light` goes on <body>, not on the content element: that element is
     usually also `.container` (max-width 1400px), so painting the band there
     would leave purple gutters on wide screens and a purple gap under short
     pages. On <body> the light band is full-bleed and reaches the sticky
     footer, while the header band and the footer paint their own colours over
     the top of it.

     The token flip is a separate concern, because the base tokens are tuned for
     the purple backdrop (--fg is WHITE, --muted a near-white lavender): anything
     sitting on the band has to opt into the light set or its text disappears.
     `.band-light` does that for a content section by name; the <main> half of
     the selector covers the app pages, which have one. The marketing pages
     (terms, FAQ, how-it-works…) have no <main> at all — sections sit straight
     under <body> — so they use the class. ── */
  /* A step deeper than `.section-light`'s #ECECF7 (css/hero.css), on purpose.
     That value is tuned for marketing sections, where a white card is usually
     the only thing on a wide band and has plenty of room to read as separate.
     The app pages are dense — a five-across stat row, list cards, action grids —
     and at #ECECF7 those cards very nearly dissolved into the field. The extra
     step, together with the border and shadow `.dash-stat-card` now carries
     (css/dashboards.css), is what makes a row of cards legible AS cards. */
  body.page-light { background: #E3E4F3; }
  .band-light,
  body.page-light > main {
    color: oklch(18% 0.015 276);
    --fg:     oklch(18% 0.015 276);
    --muted:  oklch(42% 0.02 276);
    --accent: oklch(50% 0.145 277);
    --border: oklch(88% 0.005 276);
  }

  /* The deep counterpart, for the page's header banner. Replaces the static
     SVG `.sky-clouds` those banners used to carry: those are two blurred cloud
     shapes bobbing on a FLAT purple rectangle, imitating the home hero's
     animated volumetric sky (the hero's cloud video) without any of it actually being
     there — which is exactly why they read as decoration rather than design.
     A lit gradient costs no assets, no WebGL on a results page, and is the
     same language as the deep bands further down the home page. */
  /* Chroma, not lightness, is what makes a large purple field tiring to look at:
     the brand's 0.207 is tuned for BUTTONS — small, surrounded by white — and at
     full-band scale it vibrates against the white headings and cards sitting on
     it. These bands run at ~0.145 instead. Every lightness value in the band
     hierarchy is untouched (57→49 here, 45→38 in `.section-band`), so the
     relative depth relationships the design leans on all still hold; the field
     just stops buzzing. Accents stay fully saturated on purpose — high chroma
     belongs on the 40px-wide active tab, not on 300px of backdrop. Kept in
     lockstep with `.section-glow` (css/hero.css), the home page's copy of this
     same band. */
  .band-deep {
    position: relative;
    overflow: hidden;
    background: linear-gradient(180deg, oklch(56% 0.145 276), oklch(48% 0.15 277));
    /* Same seam .section-band already draws between itself and its neighbours —
       gives every banner a defined bottom edge instead of a hard, unmarked
       colour cut into whatever comes after it. */
    border-bottom: 1px solid oklch(100% 0 0 / 0.10);
  }
  .band-deep > * { position: relative; z-index: 1; }
  .band-deep::before {
    content: '';
    position: absolute;
    inset: 0;
    background: radial-gradient(ellipse 900px 460px at 50% 0%,
                oklch(100% 0 0 / 0.11), transparent 70%);
    pointer-events: none;
  }
  /* `.hero::after` (css/hero.css) fades the hero's bottom edge into the BODY's
     purple. Over a light band that fade is a purple smear ending at a hard
     seam; a band meets the one below it at a clean edge instead. */
  .hero.band-deep::after { display: none; }

  /* The app pages' header banner. Their header (title, subtitle, the one or two
     header actions) used to be the first child of <main>; it now lives in its
     own section so the band can reach the viewport edges — <main> is
     `.container` (max-width 1400px), so a background painted there would leave
     purple-free gutters on wide screens.

     Being outside <main> also puts the header back on the ROOT token set, which
     is the one tuned for the brand purple: the heading inherits white and the
     subtitle the near-white lavender --muted, with no per-page colour rules.
     That is the whole reason the token flip is scoped to `> main`. Height is
     matched to the Browse-partners hero band (css/hero.css `.hero` +
     search.html's desktop/mobile padding overrides — measured, not derived,
     since that band's height comes from content + padding rather than a
     min-height) so every app page's header band reads at the same scale
     site-wide, whatever its own heading/subtitle content happens to be.
     profile.html carries its own equal 266px (its padding differs to make
     room for the hero card riding up into the band), so this and that stay
     in lockstep by design, not by inheritance. */
  .page-band { padding-block: 40px var(--space-lg); min-height: 266px; }
}

@layer utilities {
  .sr-only {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap;
  }

  /* Global keyboard focus ring */
  :focus-visible {
    outline: 2px solid oklch(53.6% 0.145 276);
    outline-offset: 3px;
  }
  /* Inputs handle focus with box-shadow — skip outline there */
  input:focus-visible,
  textarea:focus-visible,
  select:focus-visible { outline: none; }
}

@layer responsive {
  @media (max-width: 1024px) {
    .grid-4 { grid-template-columns: repeat(2, 1fr); }
    .grid-3 { grid-template-columns: repeat(2, 1fr); }
    .section { padding-block: var(--space-lg); }
    .section-spacious { padding-block: var(--space-xl); }
  }
  @media (max-width: 640px) {
    .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
    .section { padding-block: var(--space-lg); }
    .section-spacious { padding-block: var(--space-lg); }
    .dash-main, .analytics-main, .briefs-main, .inquiries-main, .listings-main,
    .inbox-main, .settings-main { gap: var(--space-md); }
    .dash-action-sep { display: none; }
    /* Mobile counterpart to .page-band's 266px above, matched to the
       Browse-partners hero's own mobile height the same way. */
    .page-band { min-height: 228px; }
  }
}

/* ── Mobile burger menu — UNLAYERED so it beats per-page <style> ── */
@media (min-width: 769px) and (max-width: 1024px) {
  /* The divider goes with the icon it belongs to — on its own it would be a
     bare bar floating to the left of "Browse". */
  .nav-search,
  .nav-search-divider { display: none !important; }
}
@media (min-width: 769px) {
  .nav-links { display: flex !important; }
  .nav-mobile-toggle { display: none !important; }
  .nav-separator { order: 2; }
  /* Wants `.nav-separator`'s 1px-bar LOOK but not its order. That order:2
     exists to drag the pre-login separator out of its markup position and
     park it beside the language button; this one already sits exactly where
     it belongs in the markup (right after the search form), so it opts back
     into DOM order. Without this it inherited order:2 and stacked up against
     the other separator, rendering as a double bar with nothing between. */
  .nav-search-divider { order: 0; }
  .nav-lang-btn { order: 3; }
  .nav-dash-row {
    display: inline-flex;
    align-items: center;
    margin-left: auto;
    order: 4;
  }
  .nav-dash-row .btn-signout-burger,
  .nav-links > .btn-signout-burger { display: none; }
  .nav-links > .btn.btn-primary {
    margin-left: auto;
    order: 4;
  }
}

/* ── Item stagger keyframe ── */
/* Rows rise into the panel one after another. This was NOT possible under the
   old drop-down panel: that one animated max-height to a 560px ceiling far
   above its real content (~300px), so it hit full height in ~90ms and
   effectively snapped open, and any staggered entrance played out AFTER the
   container had already stopped moving — rows visibly twitching into place,
   then the dark CTA fading up ~200ms late and reading as a flash. The side
   sheet below removes the cause: its height is fixed (the viewport minus the
   header), so it never animates height at all — it slides, for 620ms, and the
   rows now animate WITH a container that is still moving. Keep it that way; if
   the panel ever goes back to a content-driven height, this stagger has to go
   with it. */
@keyframes sm-row-in {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}

@media (max-width: 768px) {
  /* Shared sheet geometry — the panel, the prelayers behind it and the backdrop
     all hang off the header's real height, which js/staggered-menu.js measures
     into --sm-top (68px is the min-height fallback for the pre-JS frame). */
  :root {
    --sm-w: min(84vw, 380px);
    --sm-h: calc(100dvh - var(--sm-top, 68px));
  }

  /* ── Why the nav loses its blur at this width ──
     backdrop-filter makes .nav the containing block for FIXED descendants, not
     just absolute ones. That trapped the sheet inside the document: parked a
     full width past the right edge it became real scrollable overflow, and on a
     phone you could swipe the entire page sideways out of frame. Root-level
     `overflow-x: clip` papered over it in desktop Chrome and did nothing on
     iOS. It is also the known iOS Safari repaint artifact behind the flicker —
     anything animating over a backdrop-filtered ancestor tears as it moves.

     Dropping the blur off .nav ITSELF lets the sheet be genuinely fixed, which
     takes it out of scrollable overflow altogether, so no clip hack is needed.

     The blur comes back on a pseudo-element instead. ::before has no
     descendants, so nothing can be trapped by it — .nav stays a plain sticky
     box and the sheet stays viewport-fixed — while the bar still reads as
     frosted rather than flatly translucent. It needs .nav's own background
     moved onto it as well: .nav is a stacking context (sticky + z-index), so a
     z-index:-1 pseudo paints above .nav's background and below its content,
     and a backdrop-filter sitting on top of an opaque white background would
     have nothing but that white to blur. With the fill on the pseudo, the
     backdrop it samples is the page scrolling underneath. */
  .nav {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: transparent;
  }
  /* ── Why this covers the border row, and the seam bug it fixes ──
     `inset: 0` on an absolutely positioned box resolves against the PADDING
     box, which EXCLUDES the border. .nav is `background: transparent` here
     (its fill moved onto this pseudo, see above) and carries a 1px
     `border-bottom` at only 6% black — so with a plain `inset: 0` the bar fill
     stopped at 68px while .nav's box ended at 69px, leaving that 1px border row
     with no fill behind it and 94% of the live page showing straight through.
     A 1px see-through line (3 device px on a 3x phone) sitting exactly between
     the bar and the open sheet — and .sm-backdrop's blur smeared it wider.
     Desktop never had it: there .nav keeps its own background, and an element's
     background paints under its own border.
     `bottom: -1px` extends the fill over that border row, and the divider moves
     here so it draws on TOP of the fill instead of over bare page. Note what
     this is NOT: it does not reach past .nav's border-box edge (0 -> 69 exactly,
     where the sheet begins), so it cannot cover the sheet or shrink the header,
     and it is static — no transition, no state change. An earlier attempt
     extended this pseudo 10px BEYOND the header on a delayed transition; that
     both ate into the header and, being a second backdrop-filter layer changing
     state next to .sm-backdrop's, flickered on close. Don't reintroduce that.
     This is a fixed 1px landing on the header's own edge. */
  .nav::before {
    content: '';
    position: absolute;
    inset: 0 0 -1px 0;
    box-sizing: border-box;
    border-bottom: 1px solid oklch(0% 0 0 / 0.06);
    z-index: -1;
    background: oklch(100% 0 0 / 0.92);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
  }

  /* The back-to-top arrow would sit on top of the open sheet. Fade it out
     while the sheet is open. */
  html.sm-open .back-to-top {
    opacity: 0 !important;
    pointer-events: none !important;
  }

  /* ─── Prelayers ───
     Two colour panes that lead the sheet in on a 70ms offset — the "staggered"
     of staggered menu. Absolute inside .nav (which is sticky, so they stay
     pinned under the header) and below the sheet's z-index, so each is covered
     in turn as the next one arrives. */
  .sm-prelayers {
    display: block;
    position: absolute;
    inset: 0;
    pointer-events: none;
  }
  .sm-prelayer {
    position: fixed;
    top: var(--sm-top, 68px);
    right: 0;
    width: var(--sm-w);
    height: var(--sm-h);
    z-index: 190;
    /* Same park distance and same close timing as the sheet, so all three
       leave in lockstep. Mismatch them and the sheet outruns the panes it is
       covering, exposing a colour strip down its left edge on the way out. */
    transform: translateX(calc(100% + 80px));
    /* Own compositor layer: these slide across a full-bleed hero image, which
       is exactly where mobile GPUs repaint in bands. */
    will-change: transform;
    visibility: hidden;
    transition: transform .21s cubic-bezier(0.4, 0, 1, 1),
                visibility 0s linear .21s;
  }
  .sm-prelayer:nth-child(1) { background: oklch(84% 0.07 288); }
  .sm-prelayer:nth-child(2) { background: oklch(53.6% 0.145 276); }
  html.sm-open .sm-prelayer {
    transform: translateX(0);
    visibility: visible;
    /* easeOutCubic, not the near-exponential curve this used to share with the
       sheet. That one put ~85% of the travel into the first quarter of its
       duration, so the panes didn't slide in so much as appear and then creep
       the last few pixels — the "it arrives too suddenly" that read as a
       flicker. Cubic keeps the soft landing without the snap. */
    transition: transform .29s cubic-bezier(0.33, 1, 0.68, 1) calc(var(--sm-i, 0) * 42ms),
                visibility 0s;
  }

  /* ─── Backdrop ───
     Dims the page and swallows taps meant for the sheet. It does not need a
     click handler: translations.js already closes the menu on any click outside
     #navLinks, and a click here is exactly that. */
  .sm-backdrop {
    display: block;
    position: fixed;
    top: var(--sm-top, 68px);
    left: 0;
    right: 0;
    height: var(--sm-h);
    background: oklch(20% 0.02 276 / 0.42);
    /* A sibling of .nav-links, not an ancestor — unlike .nav (see that rule's
       note above), this has no fixed descendants of its own, so blurring it
       doesn't trap the sheet. */
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    z-index: 180;
    /* Belt-and-braces alongside the root-level lock below: stops a touch drag
       that starts on the backdrop itself from being read as a page scroll. */
    touch-action: none;
    opacity: 0;
    visibility: hidden;
    transition: opacity .16s ease, visibility 0s linear .16s;
  }
  html.sm-open .sm-backdrop {
    opacity: 1;
    visibility: visible;
    transition: opacity .25s ease, visibility 0s;
  }

  /* Stops the page scrolling under the open sheet, WITHOUT changing layout.
     `touch-action: none` on the root refuses touch panning for the whole
     document — the only scroll vector that matters at this width — and
     js/staggered-menu.js adds wheel/touchmove preventDefault for the rest.
     Deliberately nothing that resizes or repositions anything: the two
     layout-based locks tried before this (`overflow: hidden`, then pinning
     `body` to `position: fixed`) each reflowed the whole document in the
     frame the sheet's slide-in was starting, which killed the animation
     outright and moved the header's measured bottom edge. See that file. */
  html.sm-open {
    touch-action: none;
    overscroll-behavior: none;
  }

  /* ─── Panel ─── */
  .nav-links {
    display: flex;
    flex-direction: column;
    /* Both of these MUST be reset from the desktop .nav-links rule (nav.css),
       which sets align-items:center + gap:var(--space-md) for the horizontal
       bar. Inherited into this column they wrecked the panel: center made every
       row shrink-wrap to its text and float mid-panel (so the row padding,
       12px radius and space-between chevron below never showed), and the 24px
       gap stacked on top of the per-row margins for a loose, accidental rhythm.
       stretch + 0 let the row rules below actually define the layout. */
    align-items: stretch;
    gap: 0;
    /* fixed, not absolute: a fixed box whose containing block is the viewport
       contributes nothing to scrollable overflow, so the closed sheet parked
       past the right edge can't be swiped into. Only works because the blur is
       off .nav above — see that note. */
    position: fixed;
    top: var(--sm-top, 68px);
    left: auto;
    right: 0;
    width: var(--sm-w);
    height: var(--sm-h);
    background: #ffffff;
    /* Square, top-left included — the sheet butts straight up against the nav's
       bottom border so the two edges meet as one line. */
    border-radius: 0;
    box-shadow: -18px 0 60px oklch(0% 0 0 / 0.18), -4px 0 16px oklch(0% 0 0 / 0.07);
    /* The shadow's blur spreads in every direction from the (shifted) box, not
       just left/right — with 0 offset on Y it still bled up past the sheet's
       own top edge and over the header, since nothing was clipping it there.
       Clip exactly at the box's own top edge (0) while leaving the other three
       sides open (negative inset = don't clip) so the depth on the left/bottom
       — the part that's actually meant to be seen — is untouched. */
    clip-path: inset(0 -100px -100px -100px);
    z-index: 200;
    padding: 0 0 24px;
    /* closed — off to the right, and made invisible only once it has finished
       leaving so its shadow doesn't linger over the page edge. */
    overflow: hidden;
    pointer-events: none;
    /* Parked a full width PLUS the reach of its own shadow (18px offset + 60px
       blur). At a plain 101% the shadow still fell on-screen, so the moment
       visibility flipped a dark band appeared at the right edge and then sat
       there for the whole 160ms before the sheet started moving — motion
       announcing itself before any motion happened. Costs nothing to park it
       further out now the sheet is fixed and adds no scrollable overflow. */
    transform: translateX(calc(100% + 80px));
    will-change: transform;
    visibility: hidden;
    /* Matches .sm-prelayer's close duration exactly (see that rule — mismatch
       and the sheet outruns the panes it covers). The visibility delay MUST
       stay equal to the duration — any less and the sheet blinks out while it
       is still visibly moving. */
    transition: transform .21s cubic-bezier(0.4, 0, 1, 1),
                visibility 0s linear .21s;
  }
  .nav-links.open {
    pointer-events: auto;
    overflow-y: auto;
    overscroll-behavior: contain;
    transform: translateX(0);
    visibility: visible;
    /* open — eased out over its full duration and held back until the prelayers
       have led the way in. See the prelayer note on why the curve changed. */
    transition: transform .33s cubic-bezier(0.33, 1, 0.68, 1) .08s,
                visibility 0s;
  }

  /* ─── Row entrance ───
     An ANIMATION, not a transition: the row rules further down each declare
     `transition: background .15s, color .15s !important` at a specificity this
     selector can't reach, so a transition here would simply be overwritten.
     Animations are a separate cascade origin and are unaffected. `both` holds
     the from-state through the delay, and html.ds-reduce-motion flattens
     duration and delay globally, so the reduce-animations switch is free. */
  .nav-links.open > * {
    animation: sm-row-in .27s cubic-bezier(0.33, 1, 0.68, 1) both;
    animation-delay: calc(195ms + var(--sm-i, 0) * 29ms);
  }

  /* Accent line at top of panel. Deliberately NOT gated on .open: gated, it
     blinked out of existence the instant the class was removed, while the sheet
     it belongs to was still sliding away for another 210ms — the line vanishing
     before the panel did. Declared on the panel itself, it simply travels with
     it. Invisible when closed, since the sheet is visibility:hidden and
     off-screen. */
  .nav-links::before {
    content: '';
    display: block;
    flex-shrink: 0;
    height: 3px;
    background: linear-gradient(90deg, oklch(53.6% 0.145 276 / 0.7) 0%, transparent 100%);
    margin: 0 0 6px;
  }

  .nav-mobile-toggle { display: inline-flex; }
  /* Sign-out moves into the burger menu here. Its separator has to go with it,
     or the header ends on a divider with nothing after it — barely visible
     before, obvious now that the header sits on the deep band. */
  .dash-signout-header,
  .dash-signout-header + .dash-action-sep,
  .dash-action-sep:has(+ .dash-signout-header) { display: none !important; }
  .nav-links .nav-separator { display: none !important; }
  .nav-links a:not(.btn)::after { display: none !important; }

  /* (No per-item entrance stagger — see the note above @media.) */

  /* ─────────────────────────────────────────────────────────────────────────
     Every row rule below is scoped to `.nav-links`, NOT `.nav-links.open`.

     Removing the class removes the styling with it — instantly — while the
     sheet it belongs to is still sliding out for another 320ms. Gated, the
     whole panel silently re-laid itself out mid-close: rows collapsing back to
     14px text, the language row jumping up from the foot of the sheet, the
     separator popping back in, the accent line blinking away. The old panel got
     away with it because max-height:0 collapsed it in ~90ms; a 320ms slide puts
     the reflow in plain sight.

     The search pill was already fixed this way for exactly this reason ("the
     pill snapped to full width mid-close"); the rest of the rows just never
     followed. Safe to apply unconditionally: this whole block is ≤768px, the
     panel is the only place these elements live at that width, and it is
     visibility:hidden and off-screen when closed.

     Only three things stay gated on .open, all of them genuinely stateful: the
     panel's own transform/visibility, the row entrance animation, and the
     white-surface token re-scope up in @layer base.
     ───────────────────────────────────────────────────────────────────────── */

  /* ─── Search bar ─── */
  /* No opacity/transform resets here. They used to pin both to their neutral
     values, which silently excluded the search pill from the row entrance
     above — an !important author declaration outranks an animation, so it was
     the one row that never moved. Nothing at this width sets either property
     on .nav-search anyway: the rules they were defending against (the
     collapsing desktop search) all live in @media (min-width: 769px). */
  .nav-links .nav-search {
    max-width: none !important;
    width: auto !important;
    overflow: visible !important;
    pointer-events: auto !important;
    padding: 9px 14px !important;
    /* 10px side margin matches the link rows / CTA below, so every row in the
       panel shares one gutter — at 14px the search pill sat 4px inboard of
       everything else. */
    margin: 8px 10px 6px !important;
    border: 1.5px solid oklch(0% 0 0 / 0.09) !important;
    border-radius: 12px !important;
    background: oklch(97% 0.003 276) !important;
    gap: 8px !important;
  }
  .nav-links .nav-search input {
    width: 100% !important;
    font-size: 14px !important;
  }

  /* ─── Nav links (Browse, How it works) ─── */
  .nav-links a:not(.btn):not(.btn-signout-burger),
  .nav-links > button:not(.nav-lang-btn):not(.nav-mobile-toggle) {
    display: flex !important;
    align-items: center !important;
    gap: 10px !important;
    padding: 13px 16px !important;
    margin: 1px 10px !important;
    font-size: 15px !important;
    font-weight: 500 !important;
    border-radius: 12px !important;
    color: oklch(22% 0.015 276) !important;
    text-decoration: none !important;
    transition: background .15s, color .15s !important;
  }
  .nav-links a:not(.btn):not(.btn-signout-burger):hover,
  .nav-links > button:not(.nav-lang-btn):not(.nav-mobile-toggle):hover {
    background: oklch(53.6% 0.145 276 / 0.07) !important;
    color: oklch(53.6% 0.145 276) !important;
  }

  /* ─── Messages ─── */
  /* Match the link rows exactly: icon + label on the left, unread count pushed
     to the right edge. On the desktop bar this is a bare icon with an absolutely
     positioned badge; in a full-width row that left the envelope centred on its
     own line with the count pinned to the row's top-right corner. */
  .nav-links .nav-inbox-link {
    display: flex !important;
    align-items: center !important;
    justify-content: flex-start !important;
    gap: 10px !important;
    padding: 13px 16px !important;
    margin: 1px 10px !important;
    border-radius: 12px !important;
    color: oklch(22% 0.015 276) !important;
    transition: background .15s, color .15s !important;
  }
  .nav-links .nav-inbox-link:hover {
    background: oklch(53.6% 0.145 276 / 0.07) !important;
    color: oklch(53.6% 0.145 276) !important;
  }
  .nav-links .nav-inbox-label {
    display: inline !important;
    font-size: 15px;
    font-weight: 500;
  }
  .nav-links .nav-inbox-link .nav-inbox-badge {
    position: static !important;
    margin-left: auto !important;
    flex-shrink: 0;
  }

  /* ─── CTA: Login / Register ─── */
  .nav-links .btn.btn-primary {
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
    background: #292846 !important;
    color: #fff !important;
    border-radius: 12px !important;
    padding: 14px 20px !important;
    margin: 10px 10px 4px !important;
    font-size: 15px !important;
    font-weight: 600 !important;
    letter-spacing: 0.01em !important;
    border: none !important;
    text-decoration: none !important;
    transition: background .15s !important;
  }
  .nav-links .btn.btn-primary:hover {
    background: #56556f !important;
  }

  /* ─── Sign out ─── */
  .nav-links .btn-signout-burger {
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
    gap: 8px !important;
    padding: 11px 20px !important;
    /* Sits directly under the "My account" button and must not outgrow it: the
       account button is inset 10px inside .nav-dash-row (itself inset 10px), so
       20px here lines the two up, and the explicit line-height stops body's 1.5
       from making the secondary action the TALLER of the two. */
    margin: 4px 20px 2px !important;
    line-height: 1.2 !important;
    font-size: 14px !important;
    font-weight: 500 !important;
    border-radius: 12px !important;
    color: #DC2626 !important;
    background: oklch(55% 0.12 25 / 0.05) !important;
    border: 1.5px solid oklch(55% 0.12 25 / 0.14) !important;
    transition: background .15s, color .15s, border-color .15s !important;
    text-decoration: none !important;
  }
  .nav-links .btn-signout-burger:hover {
    background: #DC2626 !important;
    color: #fff !important;
    border-color: #DC2626 !important;
  }

  /* ─── Language toggle ─── */
  /* margin-top:auto pins it to the foot of the sheet. The old drop-down panel
     was only as tall as its content so it already sat last; the sheet is full
     height, and without this the language row floated in the middle of a large
     empty space. */
  .nav-links .nav-lang-btn {
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
    gap: 8px !important;
    padding: 13px 20px !important;
    margin: auto 0 0 !important;
    width: 100% !important;
    border-radius: 0 !important;
    font-size: 14px !important;
    font-weight: 500 !important;
    color: oklch(38% 0.01 276) !important;
    background: none !important;
    border: none !important;
    border-top: 1px solid oklch(0% 0 0 / 0.07) !important;
    transition: background .15s, color .15s !important;
  }
  .nav-links .nav-lang-btn:hover {
    background: oklch(53.6% 0.145 276 / 0.06) !important;
    color: oklch(53.6% 0.145 276) !important;
  }

  /* ─── Dashboard row (logged-in pages) ─── */
  .nav-links .nav-dash-row {
    display: flex !important;
    flex-direction: column !important;
    gap: 2px !important;
    margin: 8px 10px 0 !important;
    padding: 10px 0 4px !important;
    border-top: none !important;
  }
  .nav-links .nav-dash-row > * {
    display: flex !important;
    align-items: center !important;
    gap: 8px !important;
    padding: 11px 12px !important;
    font-size: 14px !important;
    font-weight: 500 !important;
    border-radius: 10px !important;
    color: oklch(25% 0.015 276) !important;
    background: none !important;
    border: none !important;
    margin: 0 !important;
    text-decoration: none !important;
    transition: background .15s, color .15s !important;
  }
  .nav-links .nav-dash-row > *:hover {
    background: oklch(53.6% 0.145 276 / 0.07) !important;
    color: oklch(53.6% 0.145 276) !important;
  }

  /* ─── Visual order ─── */
  /* .nav-inbox-link is injected by nav.js and needs an explicit order: without
     one it defaulted to 0, tying with .nav-search and landing between the search
     field and the nav links. It belongs with the account controls, not the
     browse links. */
  .nav-links > .nav-search               { order: 0 !important; }
  /* Browse before How it works, matching the desktop bar. These orders are the
     only thing that decides the sheet's sequence, so moving the link in the
     markup was not enough on its own. */
  .nav-links > a[data-i18n="nav.services"]   { order: 1 !important; }
  .nav-links > a[data-i18n="nav.howItWorks"] { order: 2 !important; }
  .nav-links > .nav-inbox-link           { order: 3 !important; }
  .nav-links > a.btn.btn-primary,
  .nav-links > .nav-dash-row             { order: 4 !important; }
  .nav-links > .btn-signout-burger       { order: 5 !important; }
  .nav-links > .nav-lang-btn             { order: 6 !important; }
}

/* ── Resize guard ──
   Set on <html> by js/staggered-menu.js while the viewport is being resized
   (see the comment there). Crossing the 768px breakpoint re-declares every nav
   element below with DIFFERENT transitioned properties, and a media-query
   change animates like any other style change — so each one played an
   open/close it was never asked for, just from dragging a window:

     · .nav-links / .sm-prelayer / .sm-backdrop — narrowing hands the closed
       sheet a transform + visibility it did not have, so it slid its whole
       exit across the screen.
   Deliberately at GLOBAL scope, not inside the mobile media query: nothing in
   the nav should EVER animate as a side effect of a resize, and a transition is
   governed by the style in effect AFTER the change — so a guard scoped to
   max-width:768px would already have stopped matching when widening. */
html.sm-resizing .nav-links,
html.sm-resizing .sm-prelayer,
html.sm-resizing .sm-backdrop {
  transition: none !important;
}

/* ── Page loading state (prevents flash of placeholder data) ──
   A page sets <body class="ds-pending"> in its markup so <main> is hidden and a
   subtle centred spinner shows until JS calls DS.dom.reveal() after data loads. */
@keyframes ds-spin { to { transform: rotate(360deg); } }
body.ds-pending main { visibility: hidden; }
/* Same gate for the header band's CONTENT — the greeting is filled in from the
   profile, so it would otherwise flash its "Welcome back, Jane" placeholder. The
   section itself stays visible so its gradient paints immediately. */
body.ds-pending .page-band > .container { visibility: hidden; }
body.ds-pending::after {
  content: "";
  position: fixed;
  top: 50%;
  left: 50%;
  width: 40px;
  height: 40px;
  margin: -20px 0 0 -20px;
  border: 3px solid rgba(89, 98, 192, 0.2);
  border-top-color: #5962C0;
  border-radius: 50%;
  animation: ds-spin 0.8s linear infinite;
  z-index: 9999;
  pointer-events: none;
}

/* Avatar images — dom.setAvatar() drops an <img class="avatar-img"> into any
   existing avatar box (search/profile/home/saved/inbox). The image covers the
   box and inherits its radius; .has-img suppresses the fallback gradient behind
   it (background is a shorthand → override background-image, beating inline). */
.avatar-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
  display: block;
}
.has-img { background-image: none !important; }

/* Subtle lift on the small profile-picture avatars (search / home / saved /
   inbox), matching the shadow the public profile hero and the settings preview
   already carry — just scaled down for the ~44px boxes so it reads as the same
   gentle lift rather than a heavy drop. The "+" CTA placeholder tile shares the
   .agency-avatar class but isn't a photo, so it stays flat. */
.agency-avatar,
.saved-card-avatar,
.conversation-avatar,
.thread-header-avatar {
  box-shadow: 0 2px 8px oklch(53.6% 0.145 276 / 0.2);
}
.cta-tile .agency-avatar { box-shadow: none; }


/* ── Rating stars ────────────────────────────────────────────────────────────
   Shared rounded star, used for every rating across the app (search/saved/home
   cards, profile hero + review wall + rate input, provider stat cards). A text
   glyph (★) can't have rounded points via CSS, so the star is a self-contained
   SVG with round line-joins: muted-blue fill + a thin dark rim (--star-on), or a flat
   grey for empty states (--star-off). Any element with class `.star` becomes a
   1em rounded star that scales with its font-size; the old ★ text it may contain
   is hidden. Filled by default — override `background` with --star-off for empty
   stars. See css/hero.css, screens/search.html, client-saved.html, profile.html. */
:root {
  --star-on: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M12%202%2014.53%208.52%2021.51%208.91%2016.09%2013.33%2017.88%2020.09%2012%2016.3%206.12%2020.09%207.91%2013.33%202.49%208.91%209.47%208.52Z'%20fill='%23363C7A'%20stroke='%23363C7A'%20stroke-width='3.4'%20stroke-linejoin='round'%20stroke-linecap='round'/%3E%3Cpath%20d='M12%202%2014.53%208.52%2021.51%208.91%2016.09%2013.33%2017.88%2020.09%2012%2016.3%206.12%2020.09%207.91%2013.33%202.49%208.91%209.47%208.52Z'%20fill='%235962C0'%20stroke='%235962C0'%20stroke-width='2.2'%20stroke-linejoin='round'%20stroke-linecap='round'/%3E%3C/svg%3E");
  --star-off: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M12%202%2014.53%208.52%2021.51%208.91%2016.09%2013.33%2017.88%2020.09%2012%2016.3%206.12%2020.09%207.91%2013.33%202.49%208.91%209.47%208.52Z'%20fill='%23d6d6de'%20stroke='%23d6d6de'%20stroke-width='2.4'%20stroke-linejoin='round'%20stroke-linecap='round'/%3E%3C/svg%3E");
}
.star {
  display: inline-block;
  width: 1em;
  height: 1em;
  vertical-align: -0.14em;
  background: var(--star-on) center / contain no-repeat;
  color: transparent;              /* hide any ★ text fallback */
  -webkit-text-stroke: 0;
  overflow: hidden;
  text-indent: 2em;                /* push the ★ glyph out of the box */
  white-space: nowrap;
}

/* ── iOS: never zoom on focus ────────────────────────────────────────────────
   Safari on iOS auto-zooms the page whenever a focused form control renders
   below 16px, and then leaves the viewport scaled in. Every control in this app
   sits at 13–15px, so tapping any of them — the nav search, the hero search, a
   login field — zoomed the page. The only way to suppress it is to make the
   text at least 16px; `maximum-scale=1` on the viewport would also stop it but
   at the cost of blocking pinch-zoom entirely, which is an accessibility
   regression we won't take.

   Applies on touch pointers (phones/tablets), plus any narrow viewport so the
   behaviour is testable in a desktop preview. Nothing in the app is above 16px,
   so this only ever raises a size, never shrinks one.

   The `:not()` clauses are load-bearing for specificity, not just for excluding
   boxes and radios: this has to outrank `.nav-links .nav-search input` (0,2,1)
   above, which is itself `!important`. Two element names + two attribute
   selectors puts this at (0,2,2). */
@media (pointer: coarse), (max-width: 768px) {
  body input:not([type="checkbox"]):not([type="radio"]),
  body select:not([multiple]):not([size]),
  body textarea:not([readonly]):not([disabled]) {
    font-size: 16px !important;
  }
}

/* ── Search autocomplete dropdown (js/search-suggest.js) ──────────────────
   Shared by .nav-search, .hero-search and .search-header-bar. Appended to
   <body> and positioned with `position: fixed` from the input's own
   getBoundingClientRect() rather than nested in any of those three
   containers — two of them clip or animate their own box, so a child
   positioned relative to them would get clipped (.search-header-bar has
   overflow:hidden for its rounded corners) or have to chase a width
   transition (.nav-search expanding). Fixed positioning needs neither. */
.ds-suggest {
  position: fixed;
  z-index: 5000;
  background: oklch(100% 0 0);
  border: 1px solid oklch(0% 0 0 / 0.08);
  border-radius: 12px;
  box-shadow: 0 12px 32px oklch(0% 0 0 / 0.16), 0 2px 8px oklch(0% 0 0 / 0.06);
  padding: 6px;
  max-height: 320px;
  overflow-y: auto;
  font-size: 14px;
}
.ds-suggest-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  border-radius: 8px;
  color: oklch(30% 0.015 276);
  cursor: pointer;
}
.ds-suggest-item span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-suggest-item svg { width: 16px; height: 16px; flex-shrink: 0; color: oklch(55% 0.01 276); }
.ds-suggest-item strong { font-weight: 700; }
.ds-suggest-item.active,
.ds-suggest-item:hover { background: oklch(53.6% 0.145 276 / 0.08); }
.ds-suggest-item.active svg,
.ds-suggest-item:hover svg { color: oklch(53.6% 0.145 276); }
