/* ===================================================================
   Wordly — the public shell

   Landing, pricing, the ten service pages, login and register. DARK
   charcoal ground, glass cards, and a violet → fuchsia → cyan accent.
   The signed-in app is NOT this: it stays light, because it is forms,
   tables, reports and a printable CV, and that palette is already
   contrast-measured.

   ══ ⚠️ READ THIS BEFORE TOUCHING THE TOKEN BLOCK ══════════════════

   A dark layer was tried on 2026-09-23 and the whole landing page went
   BLANK — every element present, correct, and invisible. The cause:

       index.css declares its OWN palette on :root — --ink, --muted,
       --line, --card-bg, --lav, --lav-2 — and every heading, card
       title and step title is coloured with var(--ink), not
       var(--text). The first dark layer overrode main.css's tokens and
       never touched index.css's, so all of that content rendered
       #1a1424 on #05070d. Near-black on near-black.

   Nothing threw. Nothing 404'd. A page-level 200 said it was fine.

   THE RULE: a token-override layer only reaches the tokens it knows
   about. Both sets are overridden below, and there is a short list of
   hardcoded literals at the bottom of this file that no token can
   reach. If you add a page with its own :root palette, it has to be
   added here too — or it will be invisible in exactly this way.
   =================================================================== */

body.site-public {
    /* ── Ground ─────────────────────────────────────────────────── */
    --site-bg:     #0b0b14;   /* near-black with a violet cast */
    --site-bg-2:   #12121f;
    --site-bg-3:   #1a1a2b;

    /* Glass: everything raised off the ground is white at low alpha,
       so the particle field shows through and the page reads as one
       continuous surface rather than a stack of boxes. */
    --glass:       rgba(255, 255, 255, 0.055);
    --glass-2:     rgba(255, 255, 255, 0.085);
    --hair:        rgba(255, 255, 255, 0.12);
    --hair-2:      rgba(255, 255, 255, 0.20);

    /* ── main.css tokens ────────────────────────────────────────── */
    --surface:     var(--glass);
    --canvas:      var(--site-bg);
    --border:      var(--hair);
    --white:       #12121f;
    --off-white:   #0b0b14;
    --light-gray:  var(--hair);
    --text:        rgba(255, 255, 255, 0.92);
    --text-1:      rgba(255, 255, 255, 0.97);
    --text-2:      rgba(233, 231, 245, 0.72);
    --text-3:      rgba(233, 231, 245, 0.55);
    --black:       rgba(255, 255, 255, 0.97);
    --gray:        rgba(233, 231, 245, 0.55);
    --dark-gray:   rgba(233, 231, 245, 0.72);

    /* ── index.css's PRIVATE tokens — the ones that broke it ───── */
    --ink:         rgba(255, 255, 255, 0.97);
    --muted:       rgba(233, 231, 245, 0.72);
    --line:        var(--hair);
    --card-bg:     var(--glass);
    --lav:         var(--glass);
    --lav-2:       var(--glass-2);

    /* ── Accent, lifted for a dark ground ───────────────────────
       Measured against #0b0b14: violet-300 7.2:1, fuchsia-400 7.96,
       cyan-400 10.84 — every one clears AA for text. Button FILLS stay
       on the deeper end because they carry white text: #7c3aed is
       5.7:1, where violet-300 would be 1.6. Two jobs, two shades. */
    --brand:        #a78bfa;
    --brand-dark:   #7c3aed;
    --brand-light:  #c4b5fd;
    --brand-tint:   rgba(167, 139, 250, 0.13);
    --primary-blue:       #a78bfa;
    --primary-blue-dark:  #7c3aed;
    --primary-blue-light: #c4b5fd;
    --pale-blue:    rgba(167, 139, 250, 0.13);
    --accent-cyan:  #22d3ee;

    /* The signature run. Decorative only — gradient text and icon
       tiles, where both stops sit light on near-black. */
    --grad-accent: linear-gradient(120deg, #a78bfa 0%, #e879f9 52%, #22d3ee 100%);
    /* Buttons. Stays deep enough that white text clears AA at BOTH
       stops — 5.7:1 and 6.32:1. The showy run above puts white on
       cyan at 1.5:1, which looks superb and cannot be read. */
    --grad-cta:    linear-gradient(120deg, #7c3aed 0%, #a21caf 100%);
    --gradient-primary: var(--grad-cta);

    --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.5);
    --shadow-sm: 0 2px 10px rgba(0, 0, 0, 0.45);
    --shadow-md: 0 16px 48px rgba(0, 0, 0, 0.45);
    --shadow-lg: 0 24px 70px rgba(0, 0, 0, 0.55);
    --shadow-xl: 0 32px 90px rgba(0, 0, 0, 0.65);
    --shadow-brand: 0 12px 34px rgba(124, 58, 237, 0.42);

    background: var(--site-bg);
    color: var(--text);
}

/* The ground is a slow wash rather than a flat fill — what stops a
   full-height near-black page looking like a switched-off screen.
   Fixed, so it does not scroll away. */
body.site-public::before {
    content: '';
    position: fixed;
    inset: 0;
    z-index: -2;
    background:
        radial-gradient(1000px 560px at 10% -10%, rgba(124, 58, 237, 0.28), transparent 62%),
        radial-gradient(860px 500px at 92% 2%,   rgba(232, 121, 249, 0.18), transparent 62%),
        radial-gradient(760px 520px at 50% 108%, rgba(34, 211, 238, 0.12), transparent 64%),
        linear-gradient(180deg, var(--site-bg) 0%, var(--site-bg-2) 60%, var(--site-bg) 100%);
}

.site-particles {
    position: fixed;
    inset: 0;
    z-index: -1;
    /* Never eats a click — it covers the viewport. The cursor-reach
       lines still work: pointermove is listened for on the WINDOW. */
    pointer-events: none;
    opacity: 0.62;
}

/* ═══════════════════════════════════════════════════════════════════
   MOTION
   Small, cheap and everywhere. Every one of these is disabled outright
   under prefers-reduced-motion at the bottom of this file.
   ═══════════════════════════════════════════════════════════════════ */

/* Scroll reveal. Elements start shifted and transparent and are
   released by an IntersectionObserver in reveal.js adding .is-in.
   The START state is applied ONLY when the script is present
   (html.js-reveal), so a page with no JavaScript shows everything
   rather than a column of invisible sections. */
html.js-reveal .reveal {
    opacity: 0;
    transform: translateY(18px);
    transition: opacity 0.6s var(--ease), transform 0.6s var(--ease);
}
html.js-reveal .reveal.is-in { opacity: 1; transform: none; }

/* Stagger inside a grid, so a row arrives as a row rather than all at
   once. Capped at six: past that the last card waits long enough to
   feel broken. */
html.js-reveal .reveal-stagger > * {
    opacity: 0;
    transform: translateY(16px);
    transition: opacity 0.55s var(--ease), transform 0.55s var(--ease);
}
html.js-reveal .reveal-stagger.is-in > * { opacity: 1; transform: none; }
html.js-reveal .reveal-stagger.is-in > *:nth-child(1) { transition-delay: 0.04s; }
html.js-reveal .reveal-stagger.is-in > *:nth-child(2) { transition-delay: 0.10s; }
html.js-reveal .reveal-stagger.is-in > *:nth-child(3) { transition-delay: 0.16s; }
html.js-reveal .reveal-stagger.is-in > *:nth-child(4) { transition-delay: 0.22s; }
html.js-reveal .reveal-stagger.is-in > *:nth-child(5) { transition-delay: 0.28s; }
html.js-reveal .reveal-stagger.is-in > *:nth-child(n+6) { transition-delay: 0.34s; }

/* The accent gradient drifts. 18s and a tiny range — it should be
   noticed only if you look for it. */
@keyframes grad-drift {
    0%, 100% { background-position:   0% 50%; }
    50%      { background-position: 100% 50%; }
}

/* ═══════════════════════════════════════════════════════════════════
   NAVBAR
   ═══════════════════════════════════════════════════════════════════ */

/* ── A slightly taller bar, and a slightly larger wordmark ───────
   Scoped to body.site-public, so ONLY the public shell changes. The
   signed-in app keeps main.css's 0.625rem / 32px: its navbar carries
   credit chips and a longer menu, and the humanizer measures the bar's
   real height into --nav-h for its sticky workbar — growing it there
   moves a control the customer is typing under.

   32 → 38px on the mark, 0.625 → 0.85rem on the padding. The wordmark
   is sized by HEIGHT with width auto because logo.png is a wordmark,
   not a square mark; constraining its width instead squashes it. */
body.site-public .navbar {
    padding: 0.85rem 0;
    background: rgba(11, 11, 20, 0.72);
    border-bottom: 1px solid var(--hair);
    -webkit-backdrop-filter: saturate(150%) blur(16px);
    backdrop-filter: saturate(150%) blur(16px);
}
body.site-public .logo img { height: 38px; }

/* ⚠️ `:not(.btn)` IS LOAD-BEARING — do not shorten these selectors.

   "Get Started" is an <a class="btn btn-primary"> inside this list, so
   a bare `body.site-public .nav-menu li a` matches it too — and at
   (0,2,3) it OUTRANKS both `body.site-public .btn-primary` (0,2,1),
   which sets the white, and `.nav-auth .btn` (0,2,0), which sets the
   weight. The button was rendering with --text-2 at weight 500: a
   muted grey label on a violet capsule, and a `font-weight: 700` added
   to .nav-auth did nothing at all.

   Nothing threw and nothing 404'd. It is the same class of bug as the
   --ink palette collision further up this file: a rule that is correct
   for what it was written for, quietly reaching something else. */
body.site-public .nav-menu li a:not(.btn) { color: var(--text-2); font-weight: 500; }
body.site-public .nav-menu li a:not(.btn):hover,
body.site-public .nav-menu li a:not(.btn).nav-active { color: #fff; background: var(--glass); }

/* ── THREE ZONES: contact · wordmark · menu ─────────────────────────
   The wordmark is CENTRED, which means it needs a column on each side
   of it — `1fr auto 1fr` and nothing else. A flex bar with
   `justify-content: space-between` centres the logo only while the two
   outer blocks happen to be the same width, and they never are.

   Below 1180px the contact strip is hidden: the menu carries six items
   and the bar runs out of room before the drawer breakpoint does. The
   grid keeps its three columns, so the wordmark stays centred with an
   empty column on its left rather than jumping across the bar.
   ────────────────────────────────────────────────────────────────── */
body.site-public .nav-container {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: var(--space-md);
}
/* Placed explicitly, by column. Letting the three zones fall into
   `grid-auto-flow` order works right up until one of them is hidden —
   the contact strip goes at 1180px and the burger appears at 860 — and
   then everything after it shifts one column left and the wordmark is
   no longer centred. Naming the column is what makes that impossible. */
body.site-public .nav-container > .nav-contact { grid-column: 1; }
body.site-public .nav-container > .logo     { grid-column: 2; justify-self: center; }
body.site-public .nav-container > .nav-menu { grid-column: 3; justify-self: end; }
body.site-public .nav-container > .nav-toggle { grid-column: 3; justify-self: end; }

.nav-contact {
    display: flex;
    align-items: center;
    gap: 1.25rem;
    min-width: 0;
}
.nav-contact-link {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--text-2);
    white-space: nowrap;
    transition: color 0.15s var(--ease);
}
.nav-contact-link:hover { color: #fff; }
.nav-contact-link .icon {
    width: 16px; height: 16px;
    flex: none;
    color: var(--brand);
}
/* In the bar only. The drawer gets its own copy, below. */
.nav-contact-drawer { display: none; }

/* The menu's own gap is tight on purpose — these are text links and a
   wide gap makes a navbar look sparse. The BUTTONS are the exception
   and carry their own. */
body.site-public .nav-menu { gap: 0.25rem; }

/* ── The auth slot ─────────────────────────────────────────────────
   ONE button lives here — "Get Started", pointing at /login. It was a
   Login / Get Started pair; see public-nav.js for why it is not any
   more.

   It keeps its own `gap` and `margin-left` regardless: the margin is
   what separates the button from "Pricing" (the menu's own 0.25rem
   gap is for text links sitting in a row, which is a different
   relationship), and the gap is what any second control dropped in
   here would need. A pair separated only by the menu gap reads as one
   split control rather than two buttons — which is what it did. */
.nav-auth {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    margin-left: 0.5rem;
}
/* 700, not the .btn default of 600. It is the only call to action
   in the bar and the one white-on-violet label on the page — at 600 it
   read as another menu item that happened to have a background. */
.nav-auth .btn { padding: 0.5rem 1rem; font-size: 0.875rem; font-weight: 700; }

@media (max-width: 1180px) {
    .nav-contact { display: none; }
}

.nav-drop { position: relative; }
/* On the bar the panel is absolutely positioned against .nav-drop, so
   the wrapper has to get out of the way entirely — display: contents
   leaves the <ul> parented where it was. It becomes a real box only in
   the drawer, where it is the thing that animates. */
.nav-drop-wrap { display: contents; }
/* Home is a drawer row. See public-nav.js for why it is not on the bar. */
.nav-home { display: none; }
.nav-drop-head { display: flex; align-items: center; }
/* Desktop: the chevron is drawn on the link and the real <button> is
   hidden — there is nothing for it to do while hover opens the panel. */
.nav-drop-toggle { display: none; }
.nav-drop-head > a::after {
    content: '';
    display: inline-block;
    width: 0.36em; height: 0.36em;
    margin-left: 0.42em;
    border-right: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    transform: translateY(-0.15em) rotate(45deg);
    transition: transform 0.18s var(--ease);
}
.nav-drop:hover .nav-drop-head > a::after,
.nav-drop:focus-within .nav-drop-head > a::after { transform: translateY(0.05em) rotate(225deg); }

.nav-drop-panel {
    position: absolute;
    top: calc(100% + 0.5rem);
    left: 0;
    min-width: 258px;
    padding: 0.4rem;
    list-style: none;
    border-radius: var(--radius-md);
    background: rgba(18, 18, 31, 0.97);
    border: 1px solid var(--hair-2);
    box-shadow: var(--shadow-lg);
    -webkit-backdrop-filter: blur(16px);
    backdrop-filter: blur(16px);
    /* visibility, not display:none — display cannot transition, and it
       yanks the panel out of the tab order rather than fading it. */
    opacity: 0;
    visibility: hidden;
    transform: translateY(-8px);
    transition: opacity 0.18s var(--ease), transform 0.18s var(--ease), visibility 0.18s;
}
.nav-drop:hover .nav-drop-panel,
.nav-drop:focus-within .nav-drop-panel { opacity: 1; visibility: visible; transform: none; }

.nav-drop-panel li a {
    display: block;
    padding: 0.55rem 0.7rem;
    border-radius: var(--radius-xs);
    font-size: 0.9375rem;
    font-weight: 600;
    color: var(--text-2);
    transition: background 0.15s var(--ease), color 0.15s var(--ease), transform 0.15s var(--ease);
}
.nav-drop-panel li a:hover { background: var(--glass-2); color: #fff; transform: translateX(3px); }
.nav-drop-sub {
    display: block;
    font-size: 0.75rem;
    font-weight: 400;
    color: var(--text-3);
    margin-top: 0.1rem;
}

/* ═══════════════════════════════════════════════════════════════════
   THE MOBILE DRAWER

   ⚠️ THE GROUPS ARE COLLAPSED, and that is the whole point of this
   block. The previous version set the panel `position: static;
   opacity: 1; visibility: visible` — so opening the drawer listed
   every one of the ten services immediately, and the four things that
   are actually top-level (Home, Services, Written for you, Pricing)
   were lost in the middle of them. A menu that shows everything is not
   a menu.

   It animates on `grid-template-rows`, not on max-height. A
   max-height accordion has to guess a number bigger than the content:
   guess low and the last item is clipped, guess high and the close is
   a slow nothing-happening followed by a jump. `0fr → 1fr` animates to
   the content's REAL height, whatever it turns out to be — which
   matters here because the two groups have different numbers of items
   and both are rendered from a list somebody will add to.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 860px) {
    /* The wordmark stays CENTRED on a phone, with the burger — and the
       X it becomes — on the right. The drawer drops onto its own full
       width row underneath. Column 1 is empty and that is the point:
       it is what holds the wordmark in the middle. */
    body.site-public .nav-container {
        grid-template-columns: 1fr auto 1fr;
        row-gap: 0;
    }
    /* A phone has one row to spend. The mark keeps most of the increase
       but not all of it, so the 44px burger beside it still has room. */
    body.site-public .navbar { padding: 0.7rem 0; }
    body.site-public .logo img { height: 34px; }
    body.site-public .nav-container > .nav-menu {
        grid-column: 1 / -1;
        grid-row: 2;
        justify-self: stretch;
    }

    /* The X is the accent colour while the drawer is open — it is the
       only control on the screen at that moment and it has to read as
       the way out, not as three bars that happen to have moved. */
    body.nav-open .nav-toggle span { background: var(--accent-cyan); }

    /* ── Full-bleed rows ────────────────────────────────────────────
       The divider runs the whole width of the screen while the text
       keeps its inset, which is what makes a drawer read as a list of
       rows rather than a stack of links. The negative margin cancels
       .nav-container's side padding and the padding puts it back on
       the text — the container cannot simply drop its padding, because
       the wordmark and the burger above still need it. */
    /* ⚠️ --nav-rule, NOT --hair. The hairline used everywhere else on
       this site is rgba(255,255,255,0.12) — 1.34:1 against the drawer's
       ground, which is a line you can only find if you know it is
       there. These dividers are not decoration: they are what makes the
       drawer read as a list of rows at all, so they are held to the 3:1
       floor a real UI element gets. 0.34 measures 3.04:1. */
    body.site-public .nav-menu {
        --nav-rule: rgba(255, 255, 255, 0.34);
        margin-inline: calc(var(--space-md) * -1);
        border-top: 1px solid var(--nav-rule);
    }
    body.site-public .nav-menu > li {
        border-bottom: 1px solid var(--nav-rule);
        padding-inline: var(--space-md);
    }
    /* The contact block at the foot of the drawer closes the list
       itself; a second line under it is a double rule. */
    body.site-public .nav-menu > li.nav-contact-drawer,
    body.site-public .nav-menu > li.nav-auth { border-bottom: 0; }
    /* The drawer's own contact block already draws a rule above itself;
       inherited from --hair it would be the faint one, next to the
       bright ones. */
    body.site-public .nav-contact-drawer { border-top-color: var(--nav-rule); }

    body.site-public .nav-menu li a:not(.btn) { border-radius: 0; }

    /* Where you are, in the accent. A row you are already on is the one
       thing a drawer can tell you that the bar above it cannot. */
    body.site-public .nav-menu li a:not(.btn).nav-active,
    body.site-public .nav-menu li a:not(.btn).nav-active:hover {
        color: var(--accent-cyan);
        background: transparent;
    }

    /* The sub-rows are indented and quieter, so an open group reads as
       belonging to the row above it rather than as more top level. */
    body.site-public .nav-drop-panel li a {
        padding-left: 1rem;
        font-size: 0.9375rem;
    }

    .nav-drop-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 0.25rem;
    }
    .nav-drop-head > a { flex: 1 1 auto; }
    .nav-drop-head > a::after { display: none; }

    /* 44×44, the Apple HIG minimum — the same floor the burger uses.
       A chevron drawn at its own size is a 12px tap target, which on a
       phone is the control everybody misses. */
    .nav-drop-toggle {
        display: block;
        flex: none;
        width: 44px; height: 44px;
        padding: 0;
        border: 0;
        background: transparent;
        cursor: pointer;
        position: relative;
    }
    .nav-drop-toggle::after {
        content: '';
        position: absolute;
        top: 50%; left: 50%;
        width: 0.5rem; height: 0.5rem;
        border-right: 2px solid var(--text-2);
        border-bottom: 2px solid var(--text-2);
        transform: translate(-50%, -70%) rotate(45deg);
        transition: transform 0.28s var(--ease), border-color 0.28s var(--ease);
    }
    .nav-drop.is-open .nav-drop-toggle::after {
        transform: translate(-50%, -30%) rotate(225deg);
        border-color: var(--brand);
    }

    .nav-home { display: block; }

    /* ⚠️ THE GRID LIVES ON THE WRAPPER, WHICH HAS EXACTLY ONE CHILD.

       It was on the <ul> itself, and that is a bug with a very specific
       shape: `grid-template-rows: 0fr` sizes the rows you NAME, and the
       panel has five <li>s. Item one collapsed; items two to five fell
       into IMPLICIT rows, which are `auto`, and stood at full height
       inside a group that was supposed to be shut — a screen of empty
       space under "Services" with nothing visible in it.

       Collapsing a list of unknown length means putting the single row
       around the whole list, never around its first item. */
    .nav-drop-wrap {
        display: grid;
        grid-template-rows: 0fr;
        transition: grid-template-rows 0.3s var(--ease);
    }
    .nav-drop.is-open > .nav-drop-wrap { grid-template-rows: 1fr; }

    .nav-drop-panel {
        position: static;
        visibility: visible;
        transform: none;
        min-width: 0;
        margin: 0;
        padding: 0;
        background: transparent;
        border: none;
        box-shadow: none;
        -webkit-backdrop-filter: none;
        backdrop-filter: none;

        /* The grid row's child is what clips. A 0fr row still lays its
           content out at full height unless the child sets min-height: 0
           and hides its own overflow. */
        min-height: 0;
        overflow: hidden;
        opacity: 0;
        transition: opacity 0.22s var(--ease);
    }
    .nav-drop.is-open .nav-drop-panel {
        opacity: 1;
        margin: 0.15rem 0 0.5rem 0.75rem;
    }

    /* Hover is not a thing on a phone, and leaving the desktop rule in
       means the panel springs open under a finger that is only
       scrolling past. */
    .nav-drop:hover .nav-drop-panel { opacity: 0; }
    .nav-drop.is-open:hover .nav-drop-panel { opacity: 1; }

    /* Contact moves INTO the drawer rather than disappearing. Somebody
       on a phone is the likeliest of all to want to call. */
    .nav-contact-drawer {
        display: flex;
        flex-direction: column;
        gap: 0.25rem;
        margin-top: 0.5rem;
        padding-top: 0.75rem;
        border-top: 1px solid var(--hair);
    }
    .nav-contact-drawer .nav-contact-link { padding: 0.5rem 0.75rem; }

    /* Full width in the drawer — a capsule floating in a column of
       full-width rows reads as decoration rather than the action. */
    .nav-auth {
        margin: 0.5rem 0 0;
        flex-direction: column;
        align-items: stretch;
        gap: 0.5rem;
    }
    .nav-auth .btn { width: 100%; padding: 0.7rem 1rem; }
}

/* The drawer's own animation is a courtesy, not information. */
@media (prefers-reduced-motion: reduce) {
    .nav-drop-panel,
    .nav-drop-wrap,
    .nav-drop-toggle::after { transition: none !important; }
}

/* ═══════════════════════════════════════════════════════════════════
   BUTTONS
   ═══════════════════════════════════════════════════════════════════ */

body.site-public .btn-primary {
    background: var(--grad-cta);
    background-size: 200% 200%;
    border-color: transparent;
    color: #fff;
    box-shadow: var(--shadow-brand);
    animation: grad-drift 18s var(--ease) infinite;
}
body.site-public .btn-primary:hover {
    color: #fff;
    transform: translateY(-1px);
    box-shadow: 0 16px 42px rgba(124, 58, 237, 0.55);
}
body.site-public .btn-secondary,
body.site-public .btn-outline {
    background: var(--glass);
    border: 1px solid var(--hair-2);
    color: var(--text);
}
body.site-public .btn-secondary:hover,
body.site-public .btn-outline:hover {
    background: var(--glass-2);
    border-color: rgba(255, 255, 255, 0.34);
    color: #fff;
}
body.site-public .btn-ghost { color: var(--text-2); }
body.site-public .btn-ghost:hover { background: var(--glass); color: #fff; }

/* ═══════════════════════════════════════════════════════════════════
   HERO
   ═══════════════════════════════════════════════════════════════════ */

body.site-public .hero { background: transparent; }
body.site-public .hero h1 { color: #fff; letter-spacing: -0.035em; }
body.site-public .hero-sub { color: var(--text-2); }

/* The gradient heading — the one place the showy three-stop run is
   safe, because every stop is light on near-black. */
body.site-public .gradient-text {
    background: var(--grad-accent);
    background-size: 200% 200%;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
    animation: grad-drift 14s var(--ease) infinite;
}

body.site-public .hero-badge {
    background: var(--glass);
    border: 1px solid var(--hair-2);
    color: var(--text);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
}
body.site-public .hero-badge .dot {
    background: #22d3ee;
    box-shadow: 0 0 0 0 rgba(34, 211, 238, 0.65);
    animation: dot-ping 2.4s var(--ease) infinite;
}
@keyframes dot-ping {
    0%        { box-shadow: 0 0 0 0   rgba(34, 211, 238, 0.55); }
    70%, 100% { box-shadow: 0 0 0 9px rgba(34, 211, 238, 0);    }
}

body.site-public .hero-trust { color: var(--text-3); }
body.site-public .hero-orb--1 { background: rgba(124, 58, 237, 0.42); }
body.site-public .hero-orb--2 { background: rgba(232, 121, 249, 0.30); }

/* ── Two-column hero with the floating cards ─────────────────────
   The shape wordly.lk uses: argument on the left, three drifting glass
   cards on the right. */
.hero-grid {
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
    gap: var(--space-xl);
    align-items: center;
    text-align: left;
}
.hero-copy .hero-cta,
.hero-copy .hero-trust { justify-content: flex-start; }

.hero-visual { position: relative; min-height: 560px; }

.floating-card {
    position: absolute;
    padding: 1.25rem 1.375rem;
    border-radius: var(--radius-lg);
    background: rgba(255, 255, 255, 0.07);
    border: 1px solid var(--hair-2);
    box-shadow: var(--shadow-lg);
    -webkit-backdrop-filter: blur(16px);
    backdrop-filter: blur(16px);
    animation: card-float 8s var(--ease) infinite;
    /* Hover lifts it clear of the drift, so it reads as a real object
       rather than a picture of one. */
    transition: transform 0.3s var(--ease), border-color 0.3s var(--ease);
}
.floating-card:hover {
    border-color: rgba(167, 139, 250, 0.55);
    animation-play-state: paused;
    transform: translateY(-6px) scale(1.02);
}

/* Offsets, sizes and phases all differ — three cards moving in step
   read as one block sliding about rather than three things floating. */
.float-1 { top: 8px;    left: 0;     width: 300px; animation-delay: 0s; }
.float-2 { top: 216px;  right: -8px; width: 268px; animation-delay: -2.7s; }
.float-3 { bottom: 12px; left: 22px; width: 286px; animation-delay: -5.4s; }

@keyframes card-float {
    0%, 100% { transform: translateY(0)     rotate(0deg); }
    50%      { transform: translateY(-16px) rotate(-0.6deg); }
}

.float-icon {
    display: grid;
    place-items: center;
    width: 46px; height: 46px;
    margin-bottom: 0.875rem;
    border-radius: 14px;
    background: var(--grad-accent);
    background-size: 200% 200%;
    box-shadow: 0 8px 22px rgba(124, 58, 237, 0.38);
    animation: grad-drift 16s var(--ease) infinite;
}
.float-icon .icon { width: 22px; height: 22px; color: #17071f; }
.float-title {
    font-size: 1rem; font-weight: 700;
    color: #fff; margin-bottom: 0.3rem; letter-spacing: -0.01em;
}
.float-desc {
    margin: 0;
    font-size: 0.8125rem; line-height: 1.55;
    color: var(--text-2);
}

/* ⚠️ REMOVED below 980px, not stacked. Three absolutely-positioned
   cards need ~560px of height to sit in, and on a phone that is most of
   a viewport spent on decoration before anyone reaches the buttons.
   The hero goes back to being centred, which is what it was. */
@media (max-width: 980px) {
    .hero-grid { grid-template-columns: 1fr; text-align: center; }
    .hero-copy .hero-cta,
    .hero-copy .hero-trust { justify-content: center; }
    .hero-visual { display: none; }
}

@media (prefers-reduced-motion: reduce) {
    .floating-card, .float-icon { animation: none !important; }
}

/* ═══════════════════════════════════════════════════════════════════
   CARDS — glass
   ═══════════════════════════════════════════════════════════════════ */

body.site-public .card,
body.site-public .service-card,
body.site-public .feature-card,
body.site-public .auth-box {
    background: var(--glass);
    border: 1px solid var(--hair);
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
    box-shadow: var(--shadow-md);
    transition: transform 0.28s var(--ease), border-color 0.28s var(--ease),
                background 0.28s var(--ease), box-shadow 0.28s var(--ease);
}
body.site-public .service-card:hover,
body.site-public .feature-card:hover {
    background: var(--glass-2);
    border-color: rgba(167, 139, 250, 0.45);
    box-shadow: var(--shadow-xl);
    transform: translateY(-4px);
}

body.site-public .service-card h3,
body.site-public .feature-card h3,
body.site-public .section-title { color: #fff; }
body.site-public .service-card p,
body.site-public .feature-card p,
body.site-public .section-sub { color: var(--text-2); }

body.site-public .service-icon,
body.site-public .feature-icon,
body.site-public .step-number {
    background: var(--grad-accent);
    background-size: 200% 200%;
    color: #17071f;
    border: none;
    box-shadow: 0 10px 28px rgba(124, 58, 237, 0.40);
    animation: grad-drift 16s var(--ease) infinite;
}
.service-icon .icon, .feature-icon .icon { width: 1.5em; height: 1.5em; color: #17071f; }
.feature-icon .icon { width: 1.35em; height: 1.35em; }

body.site-public .service-tag,
body.site-public .eyebrow {
    background: var(--brand-tint);
    border: 1px solid rgba(167, 139, 250, 0.32);
    color: var(--brand-light);
}
body.site-public .service-features li { color: var(--text-2); }
body.site-public .service-link { color: var(--brand); transition: gap 0.2s var(--ease); }
body.site-public .service-link:hover { color: #fff; }

.hero-trust .icon {
    width: 1em; height: 1em;
    margin-right: 0.3em;
    vertical-align: -0.12em;
    color: var(--brand);
}
.hero-trust span { display: inline-flex; align-items: center; }

/* ═══════════════════════════════════════════════════════════════════
   HOMEPAGE SERVICE GRID

   Ten cards across two sections, so they must be COMPACT and they must
   line up. An earlier cut printed each service's full lead paragraph
   plus every pricing chip as a bullet — five cards of wildly different
   heights, each effectively a small article.

   `margin-top: auto` on the link is what lines the "How it works"
   links up across a row whose copy is not the same length — the same
   device the checker's two provider cards use for their prices.
   ═══════════════════════════════════════════════════════════════════ */

.services-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: var(--space-md);
    align-items: stretch;
}
.services-grid .service-card { display: flex; flex-direction: column; height: 100%; }
.services-grid .service-card > p {
    font-size: 0.9375rem; line-height: 1.6;
    color: var(--text-2); margin: 0 0 1rem;
}
.services-grid .service-features {
    margin: 0 0 1.25rem; padding: 0; list-style: none;
    display: flex; flex-wrap: wrap; gap: 0.375rem;
}
.services-grid .service-features li {
    padding: 0.25rem 0.6rem;
    border-radius: var(--radius-pill);
    background: var(--brand-tint);
    border: 1px solid rgba(167, 139, 250, 0.26);
    color: var(--brand-light);
    font-size: 0.75rem; font-weight: 600; line-height: 1.5;
}
.services-grid .service-features li::before { content: none; }
.services-grid .service-link { margin-top: auto; }

/* ═══════════════════════════════════════════════════════════════════
   CTA BAND + FOOTER
   ═══════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════
   THE CLOSING CTA — a BOX, not a band, with a sky in it

   It used to run the full width of the viewport at 5.5rem of vertical
   padding, which on a wide screen put one short heading, one line and
   one button across 1900px. A call to action is the one thing on a
   page that should look like a single object you can point at; spread
   edge to edge it reads as another section of page.

   `width: min(760px, calc(100% - 2rem))` does both jobs in one line —
   a real maximum on a desktop, and a 1rem gutter on a phone, where a
   card touching both edges is the commonest small-screen mistake.
   ═══════════════════════════════════════════════════════════════════ */
body.site-public .cta-band {
    position: relative;
    overflow: hidden;
    width: min(760px, calc(100% - 2rem));
    margin-inline: auto;
    padding: 4rem 2rem;
    background:
        radial-gradient(700px 320px at 50% 0%, rgba(232, 121, 249, 0.22), transparent 70%),
        linear-gradient(135deg, #1c0f3a 0%, #0b0b14 100%);
    border: 1px solid var(--hair-2);
    border-radius: var(--radius-xl);
    box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45);
}
/* The band's own .container would add a second max-width and a second
   set of side padding inside a box that already has both. */
body.site-public .cta-band .container {
    max-width: none;
    padding: 0;
}
body.site-public .cta-band h2 { color: #fff; }
/* ⚠️ NOT --text-2 here, which every other section on this site
   uses for body copy. The nebula drifts UNDER this line, and against
   its brightest lobe --text-2 measures 4.04:1 — under the floor, and
   only while the animation happens to be there, which is the worst
   kind of contrast bug to try to spot by looking. White at 0.88 is
   6.82:1 against that same peak. */
body.site-public .cta-band p  { color: rgba(255, 255, 255, 0.88); }

/* ⚠️ index.css paints its OWN nebula on .cta-band::before — a third
   light wash stacked on the two this box already has. Two nebulae do
   not read as a richer one; they read as a washed-out top half, and
   the text contrast here was measured against a stack that has one.
   .cta-sky::before is the nebula now. */
body.site-public .cta-band::before { content: none; }

@media (max-width: 560px) {
    body.site-public .cta-band { padding: 3rem 1.25rem; }
}

/* ── The sky ────────────────────────────────────────────────────────
   Three parallax star layers, a drifting nebula, two shooting stars
   and one slow orbit. All CSS: no canvas, no library, no second
   requestAnimationFrame loop competing with the page's own particle
   field for the same frame.

   ⚠️ EVERY LAYER TILES, and the drift distance EQUALS the tile. That
   is what makes it seamless — animate a star field by any other
   distance and it visibly jumps back at the end of each cycle. If you
   change a tile size, change its keyframe to match.

   The layers move at different speeds on purpose: the big bright stars
   are "near" and travel fastest. Equal speeds read as a flat sheet of
   dots sliding past, which is the thing that makes a starfield look
   cheap.

   Star positions are GENERATED, from a seeded sequence — an evenly
   spaced field reads as a texture swatch, and hand-typed coordinates
   drift into rows without anybody meaning them to.
   ─────────────────────────────────────────────────────────────────── */
.cta-sky {
    position: absolute;
    inset: 0;
    overflow: hidden;
    border-radius: inherit;
    pointer-events: none;
    z-index: 0;
}
body.site-public .cta-band .container { position: relative; z-index: 1; }

.cta-sky .stars {
    position: absolute;
    /* Oversized and offset, so a layer that has drifted by one whole
       tile still covers the box instead of exposing a corner. */
    inset: -50%;
    background-repeat: repeat;
}

.cta-sky .stars-1 {
    background-image:
        radial-gradient(0.94px 0.94px at 110.4px 169px, rgba(255,255,255,0.48) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(0.68px 0.68px at 155.3px 13.2px, rgba(255,255,255,0.36) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.36px 1.36px at 61.3px 178.8px, rgba(255,255,255,0.42) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(0.76px 0.76px at 124.8px 22.7px, rgba(255,255,255,0.43) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(0.79px 0.79px at 36.2px 146.6px, rgba(255,255,255,0.52) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.08px 1.08px at 25.4px 14px, rgba(255,255,255,0.33) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.25px 1.25px at 33.9px 133.5px, rgba(255,255,255,0.33) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.06px 1.06px at 122.4px 128.2px, rgba(255,255,255,0.51) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.14px 1.14px at 98.4px 160.1px, rgba(255,255,255,0.44) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(0.82px 0.82px at 154.2px 177.4px, rgba(255,255,255,0.41) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.05px 1.05px at 34px 74.5px, rgba(255,255,255,0.43) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.04px 1.04px at 165.7px 66px, rgba(255,255,255,0.42) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(0.63px 0.63px at 16.3px 19.5px, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.06px 1.06px at 160.5px 79.7px, rgba(255,255,255,0.37) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.2px 1.2px at 2px 59.7px, rgba(255,255,255,0.44) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.12px 1.12px at 142.3px 177.2px, rgba(255,255,255,0.35) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.24px 1.24px at 31.7px 26.5px, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(0.73px 0.73px at 57.5px 25.7px, rgba(255,255,255,0.49) 0%, rgba(255,255,255,0) 100%);
    background-size: 180px 180px;
    animation: cta-drift-180 120s linear infinite;
}
.cta-sky .stars-2 {
    background-image:
        radial-gradient(1.48px 1.48px at 189px 209.4px, rgba(255,255,255,0.42) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.74px 1.74px at 82.5px 24.1px, rgba(255,255,255,0.46) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.53px 1.53px at 143px 162.9px, rgba(255,255,255,0.64) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.95px 1.95px at 54.7px 47.1px, rgba(255,255,255,0.45) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.59px 1.59px at 246.1px 101.8px, rgba(255,255,255,0.66) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.24px 1.24px at 81.6px 30.2px, rgba(255,255,255,0.75) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.6px 1.6px at 63.5px 184.8px, rgba(255,255,255,0.73) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.79px 1.79px at 123.5px 230.6px, rgba(255,255,255,0.52) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.85px 1.85px at 151.5px 26.4px, rgba(255,255,255,0.48) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.69px 1.69px at 130px 229.6px, rgba(255,255,255,0.59) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.71px 1.71px at 221.3px 228.9px, rgba(255,255,255,0.58) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(2.08px 2.08px at 62.6px 15.3px, rgba(255,255,255,0.6) 0%, rgba(255,255,255,0) 100%);
    background-size: 260px 260px;
    animation: cta-drift-260 78s linear infinite;
}
.cta-sky .stars-3 {
    background-image:
        radial-gradient(3.08px 3.08px at 209.7px 318.8px, rgba(255,255,255,0.56) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.59px 1.59px at 146.9px 174.4px, rgba(255,255,255,0.67) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(2.02px 2.02px at 236.4px 299.8px, rgba(255,255,255,0.74) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.44px 1.44px at 69px 247.8px, rgba(255,255,255,0.74) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(2.15px 2.15px at 259.9px 23.8px, rgba(255,255,255,0.82) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.34px 1.34px at 236.6px 182.8px, rgba(255,255,255,0.85) 0%, rgba(255,255,255,0) 100%),
        radial-gradient(1.88px 1.88px at 288.1px 162px, rgba(255,255,255,0.68) 0%, rgba(255,255,255,0) 100%);
    background-size: 340px 340px;
    animation: cta-drift-340 48s linear infinite;
}

@keyframes cta-drift-180 {
    from { background-position: 0 0; }
    to   { background-position: -180px -180px; }
}
@keyframes cta-drift-260 {
    from { background-position: 0 0; }
    to   { background-position: -260px -260px; }
}
@keyframes cta-drift-340 {
    from { background-position: 0 0; }
    to   { background-position: -340px -340px; }
}

/* A second, slower wash over the top of the static one — the nebula
   breathes rather than sitting still. Kept low: it is behind the
   heading, and the text contrast was measured against its PEAK
   stacked on the background's own wash — #fff 7.86:1 and body text
   6.82:1 — not against its average. The violet lobe was 0.30 and came
   down to 0.24 for exactly that reason. */
.cta-sky::before {
    content: '';
    position: absolute;
    inset: -25%;
    background:
        radial-gradient(38% 46% at 26% 30%, rgba(167, 139, 250, 0.24), transparent 68%),
        radial-gradient(34% 40% at 76% 66%, rgba(34, 211, 238, 0.18), transparent 70%);
    animation: cta-nebula 26s var(--ease) infinite alternate;
}
@keyframes cta-nebula {
    from { transform: translate3d(-2%, -1%, 0) scale(1);    opacity: 0.85; }
    to   { transform: translate3d( 3%,  2%, 0) scale(1.12); opacity: 1; }
}

/* ── Shooting stars ─────────────────────────────────────────────────
   A streak is a HEAD with a tail behind it, so the gradient runs from
   transparent to white and the element is anchored at its bright end.
   Long gaps between runs (the animation is mostly its own delay): one
   every few seconds is a night sky, one a second is a screensaver. */
.cta-sky .shoot {
    position: absolute;
    width: 160px;
    height: 1.5px;
    border-radius: 999px;
    background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.85) 100%);
    filter: drop-shadow(0 0 6px rgba(196, 181, 253, 0.9));
    opacity: 0;
    transform: rotate(18deg);
}
.cta-sky .shoot-1 { top: 18%; left: -20%; animation: cta-shoot 9s var(--ease) infinite; }
.cta-sky .shoot-2 { top: 62%; left: -20%; animation: cta-shoot 9s var(--ease) 5.2s infinite; }
@keyframes cta-shoot {
    0%       { opacity: 0; transform: translate3d(0, 0, 0) rotate(18deg); }
    3%       { opacity: 1; }
    16%      { opacity: 0; }
    /* 130% of the box, so it leaves properly instead of fading out
       somewhere in the middle of the sky. */
    16.01%, 100% { opacity: 0; transform: translate3d(130vw, 42vw, 0) rotate(18deg); }
}

/* ── One orbit ──────────────────────────────────────────────────────
   A ring with a single travelling dot. Deliberately ONE: the effect is
   "there is something out there", and three rings is a diagram. */
.cta-sky .orbit {
    position: absolute;
    top: 50%; left: 50%;
    width: 520px; height: 520px;
    margin: -260px 0 0 -260px;
    border: 1px solid rgba(196, 181, 253, 0.13);
    border-radius: 50%;
    animation: cta-orbit 44s linear infinite;
}
.cta-sky .orbit::after {
    content: '';
    position: absolute;
    top: -3px; left: 50%;
    width: 6px; height: 6px;
    margin-left: -3px;
    border-radius: 50%;
    background: #c4b5fd;
    box-shadow: 0 0 12px 2px rgba(196, 181, 253, 0.75);
}
@keyframes cta-orbit {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

/* ⚠️ REDUCED MOTION IS HANDLED HERE, NOT BY main.css.
   main.css crushes every animation to 0.001ms, and an INFINITE loop at
   that duration is a flicker rather than a pause — the same trap
   guide.css documents. These are stopped outright and parked somewhere
   that reads correctly: the stars stay (a still starfield is the point
   of the panel), the nebula sits at its mid position, and the shooting
   stars are removed entirely, because a streak frozen half way across
   the sky is a smear nobody can explain. */
@media (prefers-reduced-motion: reduce) {
    .cta-sky .stars,
    .cta-sky::before,
    .cta-sky .orbit { animation: none !important; }
    .cta-sky::before { transform: translate3d(0, 0, 0) scale(1.05); opacity: 0.92; }
    .cta-sky .shoot  { display: none; }
}



/* ═══════════════════════════════════════════════════════════════════
   FORMS — login, register, calculator
   ═══════════════════════════════════════════════════════════════════ */

body.site-public .form-group label { color: var(--text); }
body.site-public .form-control,
body.site-public select.form-control,
body.site-public textarea.form-control {
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid var(--hair-2);
    color: #fff;
}
body.site-public .form-control::placeholder { color: var(--text-3); }
body.site-public .form-control:focus {
    background: rgba(255, 255, 255, 0.08);
    border-color: var(--brand);
    box-shadow: 0 0 0 4px rgba(167, 139, 250, 0.20);
}
/* A native dropdown paints its own list from the page colours; without
   this the options come out white-on-white on some platforms. */
body.site-public select.form-control option { background: #12121f; color: #fff; }

body.site-public .auth-container { background: transparent; }
body.site-public .auth-header h1 { color: #fff; }
body.site-public .auth-header p,
body.site-public .auth-footer,
body.site-public .auth-footer p { color: var(--text-2); }
body.site-public .auth-footer a { color: var(--brand); }
body.site-public .auth-footer a:hover { color: #fff; }
body.site-public .field-hint, body.site-public small { color: var(--text-3); }

body.site-public .alert-error {
    background: rgba(239, 68, 68, 0.14);
    border: 1px solid rgba(239, 68, 68, 0.40);
    color: #fca5a5;
}
body.site-public .alert-success {
    background: rgba(16, 185, 129, 0.14);
    border: 1px solid rgba(16, 185, 129, 0.40);
    color: #6ee7b7;
}
body.site-public .alert-info {
    background: var(--brand-tint);
    border: 1px solid rgba(167, 139, 250, 0.34);
    color: var(--brand-light);
}
body.site-public .spinner { border-color: rgba(255,255,255,0.25); border-top-color: #fff; }

/* ═══════════════════════════════════════════════════════════════════
   SERVICE PAGES
   ═══════════════════════════════════════════════════════════════════ */

.svc-hero { padding: 4.5rem 0 3rem; text-align: center; }
.svc-hero .eyebrow { margin-bottom: 1rem; }
.svc-hero h1 {
    font-size: clamp(2rem, 4.5vw, 3.1rem);
    font-weight: 800; letter-spacing: -0.035em; line-height: 1.1;
    margin: 0 0 1rem; color: #fff;
}
.svc-hero p {
    max-width: 680px; margin: 0 auto;
    font-size: 1.1rem; line-height: 1.7; color: var(--text-2);
}
.svc-mark {
    display: inline-flex; align-items: center; justify-content: center;
    width: 72px; height: 72px; margin-bottom: 1.25rem;
    border-radius: 20px;
    background: var(--grad-accent);
    background-size: 200% 200%;
    box-shadow: 0 16px 40px rgba(124, 58, 237, 0.42);
    animation: grad-drift 16s var(--ease) infinite;
}
.svc-mark .icon { width: 34px; height: 34px; color: #17071f; }

.svc-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-md); }

.svc-steps { display: grid; gap: var(--space-md); }
.svc-step { display: flex; gap: 1rem; align-items: flex-start; }
.svc-step p { color: var(--text-2); }
.svc-step-n {
    flex: none; display: grid; place-items: center;
    width: 42px; height: 42px;
    border-radius: var(--radius-sm);
    background: var(--brand-tint);
    border: 1px solid rgba(167, 139, 250, 0.30);
    color: var(--brand-light);
    font-weight: 800;
}

.svc-price { display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: center; margin-top: 1.25rem; }
.svc-price-chip {
    padding: 0.45rem 0.9rem;
    border-radius: var(--radius-pill);
    background: var(--brand-tint);
    border: 1px solid rgba(167, 139, 250, 0.30);
    color: var(--brand-light);
    font-size: 0.875rem; font-weight: 600;
}

/* Every service page ends here. The pages exist to explain a service
   to somebody who is NOT signed in, so each has to end by telling them
   how to start — otherwise it is a leaflet with no address on it. */
.svc-cta {
    text-align: center;
    padding: 3.25rem 2rem;
    border-radius: var(--radius-xl);
    background:
        radial-gradient(60% 150% at 50% 0%, rgba(232, 121, 249, 0.26), transparent 70%),
        var(--grad-cta);
    background-size: auto, 200% 200%;
    color: #fff;
    box-shadow: var(--shadow-lg);
    animation: grad-drift 18s var(--ease) infinite;
}
.svc-cta h2 {
    font-size: clamp(1.5rem, 3vw, 2.1rem);
    font-weight: 800; letter-spacing: -0.02em; margin: 0 0 0.6rem; color: #fff;
}
.svc-cta p {
    margin: 0 auto 1.75rem; max-width: 560px; font-size: 1.03rem;
    color: rgba(255, 255, 255, 0.90);
}
.svc-cta-row { display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; }
/* ⚠️ THESE TWO SELECTORS CARRY `body.site-public` FOR A REASON —
   do not shorten them back to `.svc-cta .btn-primary`.

   This is the WHITE button that sits on the purple CTA band. Written
   without the body prefix it scores 0,3,0, and
   `body.site-public .btn-primary` scores 0,3,1 — so the generic rule
   won `color` and this one won `background`. The resting button was
   the purple gradient on a purple band, and on hover it became
   **white text on a #f5f3ff background: 1.1:1**, a button whose label
   vanished when you pointed at it.

   Neither rule was wrong on its own, which is exactly why it survived
   a read-through. Measured, not eyeballed: 8.98:1 at rest, 9.99:1 on
   hover. */
body.site-public .svc-cta .btn-primary {
    background: #fff; color: #5b21b6; border-color: #fff; animation: none;
    box-shadow: 0 10px 28px rgba(0, 0, 0, 0.30);
}
body.site-public .svc-cta .btn-primary:hover {
    background: #f5f3ff; color: #4c1d95;
    box-shadow: 0 14px 34px rgba(0, 0, 0, 0.34);
}
body.site-public .svc-cta .btn-outline {
    background: rgba(255,255,255,0.10);
    border: 1.5px solid rgba(255, 255, 255, 0.55);
    color: #fff;
}
body.site-public .svc-cta .btn-outline:hover {
    background: rgba(255,255,255,0.20); border-color: #fff; color: #fff;
}

.svc-inline-cta {
    display: flex; align-items: center; justify-content: space-between;
    gap: 1rem; flex-wrap: wrap;
    padding: 1.25rem 1.5rem;
    border-radius: var(--radius-lg);
    background: var(--glass);
    border: 1px solid var(--hair);
}
.svc-inline-cta p { margin: 0; color: var(--text-2); font-weight: 500; }

/* ═══════════════════════════════════════════════════════════════════
   HARD OVERRIDES

   Five rules in index.css and auth.css paint a LITERAL light colour
   rather than reading a token, so redefining tokens cannot reach them.
   Listed explicitly so the light site still works if `site-public` is
   ever removed. Each needs at least the specificity of what it beats —
   `.auth-box .form-control` is (0,2,0), so `body.site-public` alone
   would lose.
   ═══════════════════════════════════════════════════════════════════ */

body.site-public .auth-box .form-control {
    background: rgba(255, 255, 255, 0.05);
    border-color: var(--hair-2);
    color: #fff;
}
body.site-public .auth-box .form-control:focus {
    background: rgba(255, 255, 255, 0.08);
    border-color: var(--brand);
    box-shadow: 0 0 0 4px rgba(167, 139, 250, 0.20);
}
body.site-public .auth-box label { color: var(--text); }

/* ── The section bands ──────────────────────────────────────────────
   index.css paints four full-width section backgrounds, and three of
   them bake a literal #ffffff into a gradient:

       .hero      radial(var(--lav-2)) over linear(var(--lav) → #ffffff)
       .services  #ffffff
       .features  linear(#ffffff → var(--lav))     ← the one that was missed
       .how       var(--lav)

   The var() stops follow the token override; **the #ffffff stops do
   not**, so `.features` rendered as a white band across the middle of
   the page starting at "Why Wordly" — the section backgrounds after
   the hero were literally white.

   All four are forced transparent so the fixed wash and the particle
   field show through as ONE continuous surface, which is the whole
   point of the glass treatment. Listing them individually rather than
   with a blanket `section { background: none }` on purpose: .cta-band
   is also a section and its background is the entire point of it.

   ⚠️ A NEW full-width band in index.css needs a line here. The token
   override cannot reach a literal, and nothing will error — the page
   will just have a white stripe in it. */
body.site-public .hero,
body.site-public .services,
body.site-public .features,
body.site-public .how { background: transparent; }
body.site-public .cta-band .btn { background: #fff; color: #5b21b6; }
body.site-public .cta-band .btn:hover { background: #f5f3ff; color: #4c1d95; }

/* ═══════════════════════════════════════════════════════════════════
   REDUCED MOTION
   Every animation here is decoration. Somebody who has asked for less
   motion gets the same page, still.
   ═══════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
    body.site-public .btn-primary,
    body.site-public .gradient-text,
    body.site-public .service-icon,
    body.site-public .feature-icon,
    body.site-public .step-number,
    body.site-public .hero-badge .dot,
    .svc-mark, .svc-cta { animation: none !important; }
    body.site-public .hero-orb { animation: none !important; }
    /* Reveal targets are released immediately rather than left hidden —
       reveal.js also bails, but a stylesheet that can hide content must
       never depend on a script to unhide it. */
    html.js-reveal .reveal,
    html.js-reveal .reveal-stagger > * { opacity: 1 !important; transform: none !important; }
}

/* ═══════════════════════════════════════════════════════════════════
   THE GUIDED QUOTE

   /pricing asks one question at a time in a transcript. It LOOKS like
   a chat and it is a decision tree — see the header of pricing.js.

   Two earlier attempts are worth not repeating. A <select> hid every
   option until opened, on the one page whose job is "what do you sell
   and what does it cost". A grid of tiles plus a form showed every
   field at once, most of them irrelevant to whatever had been picked,
   and the price only arrived after all of them were filled.

   Built mobile-first: the widths below are max-widths and percentages,
   so nothing here has a floor wider than a 390px screen.
   ═══════════════════════════════════════════════════════════════════ */

.quote-box {
    max-width: 760px;
    margin: 0 auto;
    border-radius: var(--radius-xl);
    background: var(--glass);
    border: 1px solid var(--hair);
    box-shadow: var(--shadow-lg);
    -webkit-backdrop-filter: blur(16px);
    backdrop-filter: blur(16px);
    overflow: hidden;
}

.quote-head {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    padding: 0.875rem 1.125rem;
    border-bottom: 1px solid var(--hair);
    background: rgba(255, 255, 255, 0.04);
}
.quote-dot {
    width: 9px; height: 9px;
    border-radius: 50%;
    background: #22d3ee;
    animation: dot-ping 2.4s var(--ease) infinite;
}
.quote-head-title { font-weight: 700; font-size: 0.9375rem; color: #fff; }
.quote-head-note {
    margin-left: auto;
    font-size: 0.75rem;
    color: var(--text-3);
    /* The one thing worth saying in a header this small. */
    white-space: nowrap;
}

.quote-thread {
    padding: 1.25rem;
    display: flex;
    flex-direction: column;
    gap: 0.875rem;
    /* Tall enough that the conversation has somewhere to happen, capped
       so a long one does not push the footer off a laptop. It scrolls
       itself rather than the page. */
    min-height: 420px;
    max-height: 70vh;
    overflow-y: auto;
    scrollbar-gutter: stable;
}

/* ── Bubbles ─────────────────────────────────────────────────────── */

.q-row { display: flex; gap: 0.625rem; align-items: flex-start; animation: q-in 0.32s var(--ease); }
@keyframes q-in {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: none; }
}

.q-avatar {
    flex: none;
    display: grid; place-items: center;
    width: 32px; height: 32px;
    border-radius: 10px;
    background: var(--grad-accent);
    background-size: 200% 200%;
    animation: grad-drift 16s var(--ease) infinite;
}
.q-avatar .icon { width: 16px; height: 16px; color: #17071f; }

.q-bubble {
    max-width: 80%;
    padding: 0.7rem 0.95rem;
    border-radius: 16px;
    font-size: 0.9375rem;
    line-height: 1.55;
}
.q-bot .q-bubble {
    background: rgba(255, 255, 255, 0.07);
    border: 1px solid var(--hair);
    border-top-left-radius: 5px;
    color: var(--text);
}
/* The customer's own answers, right-aligned so the transcript reads as
   two voices rather than one column of boxes. */
.q-you { justify-content: flex-end; }
.q-you .q-bubble {
    background: var(--grad-cta);
    border-top-right-radius: 5px;
    color: #fff;
    font-weight: 600;
    box-shadow: var(--shadow-brand);
}

/* ── Answer controls ─────────────────────────────────────────────── */

.q-answer-row { flex-direction: column; align-items: stretch; gap: 0.5rem; }
.q-group {
    margin: 0.35rem 0 0;
    font-size: 0.7rem; font-weight: 700;
    letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--text-3);
}
.q-choices { display: flex; flex-wrap: wrap; gap: 0.5rem; }

.q-choice {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.6rem 0.9rem;
    font: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    text-align: left;
    color: var(--text);
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid var(--hair-2);
    border-radius: var(--radius-pill);
    cursor: pointer;
    /* 44px is the Apple HIG minimum. A row of pills that are each 30px
       tall is the commonest thing to miss on a phone. */
    min-height: 44px;
    transition: background 0.18s var(--ease), border-color 0.18s var(--ease),
                transform 0.18s var(--ease);
}
.q-choice:hover {
    background: rgba(167, 139, 250, 0.16);
    border-color: var(--brand);
    transform: translateY(-1px);
}
.q-choice:active { transform: translateY(0); }
.q-choice:focus-visible {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 4px rgba(167, 139, 250, 0.22);
}
.q-choice-hint {
    font-size: 0.7rem; font-weight: 500;
    color: var(--text-3);
    padding-left: 0.4rem;
    border-left: 1px solid var(--hair-2);
}
.q-choice-icon {
    display: grid; place-items: center;
    width: 24px; height: 24px;
    border-radius: 7px;
    background: var(--grad-accent);
}
.q-choice-icon .icon { width: 13px; height: 13px; color: #17071f; }

.q-number { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.q-number input {
    /* flex-basis rather than a fixed width, so it shrinks on a phone
       instead of pushing the row sideways. */
    flex: 1 1 8rem;
    min-width: 0;
    min-height: 44px;
}
.q-suffix { font-size: 0.875rem; color: var(--text-3); }
.q-number .btn { min-height: 44px; }

/* ── The quote ───────────────────────────────────────────────────── */

.q-quote-row .q-bubble {
    max-width: 100%;
    width: 100%;
    padding: 0;
    background: none;
    border: none;
}
.q-quote {
    text-align: center;
    padding: 1.5rem 1.25rem;
    border-radius: var(--radius-lg);
    background:
        radial-gradient(70% 140% at 50% 0%, rgba(232, 121, 249, 0.24), transparent 70%),
        var(--grad-cta);
    background-size: auto, 200% 200%;
    color: #fff;
    box-shadow: var(--shadow-lg);
    animation: grad-drift 18s var(--ease) infinite;
}
.q-quote-label {
    display: block;
    font-size: 0.7rem; font-weight: 700;
    letter-spacing: 0.1em; text-transform: uppercase;
    color: rgba(255, 255, 255, 0.86);
}
.q-quote-value {
    margin: 0.35rem 0 0.6rem;
    font-size: clamp(1.9rem, 8vw, 2.6rem);
    font-weight: 800; letter-spacing: -0.03em;
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
}
.q-unit { font-size: 0.45em; font-weight: 700; letter-spacing: 0; }
.q-quote-note {
    margin: 0 auto 1.1rem;
    max-width: 44ch;
    font-size: 0.8125rem; line-height: 1.6;
    color: rgba(255, 255, 255, 0.90);
}
.q-quote-cta .btn {
    background: #fff; color: #5b21b6; border-color: #fff;
    animation: none; box-shadow: 0 10px 26px rgba(0, 0, 0, 0.30);
}
.q-quote-cta .btn:hover { background: #f5f3ff; color: #4c1d95; }
.q-quote-small {
    margin: 0.75rem 0 0;
    font-size: 0.7rem; line-height: 1.5;
    color: rgba(255, 255, 255, 0.78);
}

@media (max-width: 560px) {
    .quote-thread { padding: 1rem; min-height: 360px; max-height: none; }
    /* Bubbles get more of the line on a narrow screen — 80% of 390px
       leaves a sentence four words wide. */
    .q-bubble { max-width: 88%; font-size: 0.9rem; }
    .q-choice { width: 100%; }
    .q-choices { flex-direction: column; }
    .quote-head-note { display: none; }
}

@media (prefers-reduced-motion: reduce) {
    .q-row, .q-avatar, .q-quote, .q-choice-icon, .quote-dot { animation: none !important; }
}

/* ═══════════════════════════════════════════════════════════════════
   FOOTER

   Four columns, grouped the way the site is. It used to be a centred
   logo and then EVERY link on the site in one flat row — twelve of
   them, wrapping into an unreadable block. A footer is for somebody
   who did not find what they wanted higher up, and a wall of
   equal-weight links is the opposite of scannable.

   ⚠️ On a phone the CONTACT column comes first, which is why the brand
   block is `order: -1` rather than simply first in the source — on a
   narrow screen the thing people scroll to the bottom for is a way to
   get hold of you, not a sitemap.
   ═══════════════════════════════════════════════════════════════════ */

body.site-public .site-footer {
    margin-top: var(--space-xxl);
    padding: var(--space-xl) 0 var(--space-lg);
    background: rgba(11, 11, 20, 0.72);
    border-top: 1px solid var(--hair);
    color: var(--text-3);
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
}

.footer-grid {
    display: grid;
    /* The brand column is wider — it carries a paragraph and the
       contact details, the others carry five short links each. */
    grid-template-columns: minmax(0, 1.5fr) repeat(3, minmax(0, 1fr));
    gap: var(--space-lg);
    align-items: start;
    text-align: left;
}

/* ── Brand column ────────────────────────────────────────────────── */

body.site-public .footer-logo {
    display: inline-block;
    margin-bottom: 0.875rem;
}
/* Sized by HEIGHT with width:auto — logo.png and its white twin are
   WORDMARKS, not square marks, and forcing one into a box squashes it.
   Same rule the auth pages follow. */
body.site-public .footer-logo img { height: 34px; width: auto; display: block; }

.footer-tagline {
    margin: 0 0 1.125rem;
    max-width: 34ch;
    font-size: 0.875rem;
    line-height: 1.65;
    color: var(--text-2);
}

.footer-contact { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }
.footer-contact a {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--text-2);
    /* 44px tap target — these are the two links on the page most likely
       to be reached for on a phone. */
    min-height: 44px;
    transition: color 0.15s var(--ease);
}
.footer-contact a:hover { color: #fff; }
.footer-contact .icon {
    width: 16px; height: 16px;
    flex: none;
    color: var(--brand);
}

/* ── Link columns ────────────────────────────────────────────────── */

.footer-col h3 {
    margin: 0 0 0.875rem;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-3);
}
.footer-col ul { list-style: none; margin: 0; padding: 0; }
.footer-col li + li { margin-top: 0.125rem; }
.footer-col a {
    display: block;
    padding: 0.4rem 0;
    font-size: 0.875rem;
    color: var(--text-2);
    transition: color 0.15s var(--ease), transform 0.15s var(--ease);
}
.footer-col a:hover { color: #fff; transform: translateX(3px); }

/* ── Bottom bar ──────────────────────────────────────────────────── */

.footer-bottom {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-top: var(--space-lg);
    padding-top: var(--space-md);
    border-top: 1px solid var(--hair);
}
/* One line now — the opening hours that used to sit on the right
   are on the contact card and in the footer's own contact column, and
   a bottom bar repeating them was the third copy. */
body.site-public .footer-copy {
    margin: 0;
    font-size: 0.8125rem;
    color: var(--text-3);
}

/* ── Narrow ──────────────────────────────────────────────────────── */

@media (max-width: 900px) {
    .footer-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    /* The brand block spans both columns rather than being squeezed
       into half a phone-width beside a link list. */
    .footer-brand { grid-column: 1 / -1; }
}

@media (max-width: 560px) {
    .footer-grid { grid-template-columns: 1fr; gap: var(--space-md); }
    /* Contact first. See the note at the top of this block. */
    .footer-brand { order: -1; }
    .footer-tagline { max-width: none; }
    .footer-bottom { justify-content: flex-start; }
}

/* ═══════════════════════════════════════════════════════════════════
   LEGAL PAGES  (/terms, /refund-policy)

   These are the two pages on the site somebody reads because they are
   about to spend money, or because something has gone wrong. Both
   cases are badly served by a wall of justified 13px grey text, which
   is what a legal page usually is — so this is set at 17px with a
   ~68-character measure and generous leading, and every clause is
   numbered so support and the customer can refer to the same thing.

   A contents list sits at the top because the second reader of this
   page is looking for ONE clause, not the document.
   ═══════════════════════════════════════════════════════════════════ */

.legal-wrap {
    max-width: 780px;
    margin: 0 auto;
}

/* The contents list. Anchors, so a support reply can link straight to
   the clause being quoted rather than saying "see section 4". */
.legal-toc {
    border: 1px solid var(--hair);
    border-radius: var(--radius-xl, 18px);
    padding: 1.25rem 1.5rem;
    margin-bottom: 2.5rem;
    background: var(--card-bg, #fff);
}
.legal-toc h2 {
    font-size: 0.75rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-3);
    margin: 0 0 0.75rem;
}
.legal-toc ol {
    margin: 0;
    padding-left: 1.25rem;
    display: grid;
    gap: 0.375rem;
    font-size: 0.9375rem;
}
.legal-toc a { color: var(--ink); text-decoration: none; }
.legal-toc a:hover { color: var(--brand); text-decoration: underline; }

.legal-body { font-size: 1.0625rem; line-height: 1.75; color: var(--ink); }

.legal-body section { margin-bottom: 2.75rem; }
/* scroll-margin so an anchor jump does not tuck the heading under the
   sticky navbar — the one thing that makes a contents list useless. */
.legal-body section { scroll-margin-top: 96px; }

.legal-body h2 {
    font-size: 1.375rem;
    font-weight: 800;
    line-height: 1.3;
    margin: 0 0 0.875rem;
    color: var(--ink);
}
.legal-body h3 {
    font-size: 1.0625rem;
    font-weight: 700;
    margin: 1.5rem 0 0.5rem;
    color: var(--ink);
}
.legal-body p { margin: 0 0 1rem; color: var(--muted, var(--text-2)); }
.legal-body ul, .legal-body ol { margin: 0 0 1rem; padding-left: 1.375rem; }
.legal-body li { margin-bottom: 0.5rem; color: var(--muted, var(--text-2)); }
.legal-body strong { color: var(--ink); font-weight: 700; }
.legal-body a { color: var(--brand); font-weight: 600; }

/* A clause that costs money if it is missed. Used sparingly — four
   times across both documents — because a page where everything is
   highlighted highlights nothing. */
.legal-callout {
    border: 1px solid var(--hair);
    border-left: 3px solid var(--brand);
    border-radius: var(--radius-lg, 14px);
    padding: 1rem 1.25rem;
    margin: 0 0 1.25rem;
    background: rgba(139, 92, 246, 0.06);
}
.legal-callout.is-warning {
    /* Amber-500, not amber-700. Measured on #0b0b14: the deeper shade
       lands at 3.9:1 and reads as mud on a dark ground, which is the
       opposite of what a warning stripe is for. This one is 9.12:1. */
    border-left-color: #f59e0b;
    background: rgba(245, 158, 11, 0.08);
}
.legal-callout p:last-child { margin-bottom: 0; }

.legal-meta {
    font-size: 0.875rem;
    color: var(--text-3);
    margin-top: 3rem;
    padding-top: 1.5rem;
    border-top: 1px solid var(--hair);
}

@media (max-width: 640px) {
    .legal-body { font-size: 1rem; }
    .legal-body h2 { font-size: 1.1875rem; }
    .legal-toc { padding: 1rem 1.125rem; }
}

/* ═══════════════════════════════════════════════════════════════════
   THE RECENT-PURCHASES STRIP

   It sits in the gap the hero used to leave above Services. That gap
   is now SMALLER as well as filled — a strip dropped into 192px of
   empty page would have been a third thing floating in it.

   ⚠️ THE LOOP IS CSS ON A DUPLICATED TRACK. ticker.js writes the rows
   out twice and this translates the track left by exactly -50%, so the
   restart lands on an identical pixel. Any other distance visibly
   jumps — the same rule the CTA starfield follows.
   ═══════════════════════════════════════════════════════════════════ */

.ticker { padding: 0 0 1.5rem; }

.ticker-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    justify-content: center;
    margin-bottom: 0.75rem;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-3);
}
.ticker-dot {
    width: 7px; height: 7px;
    border-radius: 50%;
    background: #22d3ee;
    box-shadow: 0 0 0 0 rgba(34, 211, 238, 0.6);
    animation: ticker-ping 2.4s var(--ease) infinite;
}
@keyframes ticker-ping {
    0%        { box-shadow: 0 0 0 0   rgba(34, 211, 238, 0.55); }
    70%, 100% { box-shadow: 0 0 0 7px rgba(34, 211, 238, 0);    }
}

.ticker-window {
    position: relative;
    overflow: hidden;
    border: 1px solid var(--hair);
    border-radius: var(--radius-pill);
    background: var(--glass);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    padding: 0.6rem 0;
    /* Fades the rows out at both ends instead of cutting them off at a
       hard edge, which is what makes a marquee look like an overflow
       bug rather than a feed. */
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
    mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}

.ticker-track {
    display: flex;
    align-items: center;
    /* width: max-content is what lets the track be wider than its
       window — without it the flex line wraps and there is nothing to
       scroll. */
    width: max-content;
    gap: 2.5rem;
    padding-left: 2.5rem;
    /* The fallback only applies if ticker.js could not measure the
       track; it is set to match the slower pace rather than the old
       one, so a measurement failure does not silently speed it up. */
    animation: ticker-slide var(--ticker-duration, 70s) linear infinite;
}
/* Pausing on hover is the difference between a feed you can read and
   one you can only glimpse. */
.ticker-window:hover .ticker-track { animation-play-state: paused; }

@keyframes ticker-slide {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(-50%, 0, 0); }
}

.ticker-item {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    white-space: nowrap;
    font-size: 0.875rem;
    color: var(--text-2);
}
.ticker-item b { color: var(--ink); font-weight: 600; }
/* Flag emoji render as a two-letter box on Windows, which still reads
   as a country — there is no flag sprite in icons.js, and shipping 40
   SVG flags for a decorative strip is not a trade worth making. */
.ticker-flag { font-size: 1.05rem; line-height: 1; }
.ticker-verb { color: var(--text-3); }
.ticker-what { color: var(--brand); font-weight: 600; }
.ticker-ago  { color: var(--text-3); font-size: 0.8125rem; }
.ticker-ago::before { content: '·'; margin-right: 0.5rem; }

@media (prefers-reduced-motion: reduce) {
    /* main.css crushes animations to 0.001ms and an INFINITE loop at
       that duration is a flicker, not a pause. Stopped outright and
       parked at the start, where the newest purchase is — which is the
       one worth reading anyway. The window still scrolls by hand. */
    .ticker-track { animation: none; transform: none; }
    .ticker-dot   { animation: none; }
    .ticker-window { overflow-x: auto; }
}

@media (max-width: 560px) {
    .ticker-item { font-size: 0.8125rem; }
    .ticker-track { gap: 1.75rem; padding-left: 1.75rem; }
}
