
        /* ============ Secondary (ghost) CTA ============ */
        .zio-cta-ghost {
            border: 1px solid rgba(255,255,255,.18);
            color: #fff;
            background: rgba(255,255,255,.02);
            transition: background .2s ease, border-color .2s ease, transform .22s cubic-bezier(.34,1.56,.64,1);
        }
        .zio-cta-ghost:hover {
            background: rgba(255,255,255,.07);
            border-color: rgba(255,255,255,.32);
            transform: translateY(-2px);
        }

        /* ============ Claim-your-link control ============
           Glass pill matching the hero design system. Scoped CSS only (no new
           Tailwind utilities) so it renders even when the build/watch isn't
           running in an isolated env. */
        .zio-claim-form { margin-bottom: 1rem; }
        .zio-claim-label {
            display: block;
            font-size: .7rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
            color: #94a3b8; margin-bottom: .5rem;
        }
        .zio-claim {
            display: flex; align-items: stretch; gap: .25rem;
            max-width: 30rem; margin-inline: auto;
            padding: .3rem;
            background: rgba(255,255,255,.04);
            border: 1px solid rgba(255,255,255,.12);
            border-radius: 9999px;
            backdrop-filter: blur(12px);
            -webkit-backdrop-filter: blur(12px);
            transition: border-color .2s ease, background .2s ease;
        }
        .zio-claim:focus-within {
            border-color: rgba(96,165,250,.55);
            background: rgba(255,255,255,.06);
        }
        .zio-claim-prefix {
            display: flex; align-items: center;
            padding-inline: .6rem 0; padding-left: .9rem;
            font-size: .9rem; color: #94a3b8; white-space: nowrap; user-select: none;
        }
        .zio-claim-input {
            flex: 1 1 auto; min-width: 0;
            background: transparent; border: 0; outline: none;
            color: #fff; font-size: .95rem; padding: .55rem .25rem;
        }
        .zio-claim-input::placeholder { color: #64748b; }
        .zio-claim-btn {
            flex: 0 0 auto;
            display: inline-flex; align-items: center; gap: .4rem;
            padding: .55rem 1.15rem;
            border: 0; border-radius: 9999px;
            color: #fff; font-size: .9rem; font-weight: 700; white-space: nowrap; cursor: pointer;
        }
        /* Inline status mark (spinner / check / cross) sitting before the button. */
        .zio-claim-mark {
            flex: 0 0 auto;
            display: none; align-items: center; justify-content: center;
            width: 1.1rem; height: 1.1rem; margin-inline: .15rem .35rem;
            align-self: center;
            font-size: .85rem; line-height: 1;
        }
        .zio-claim[data-state="checking"] .zio-claim-mark,
        .zio-claim[data-state="available"] .zio-claim-mark,
        .zio-claim[data-state="error"] .zio-claim-mark { display: inline-flex; }
        .zio-claim[data-state="available"] .zio-claim-mark::before { content: '\2713'; color: #1ed760; font-weight: 800; }
        .zio-claim[data-state="error"] .zio-claim-mark::before { content: '\2715'; color: #fb7185; font-weight: 800; }
        .zio-claim[data-state="checking"] .zio-claim-mark::before {
            content: ''; width: .9rem; height: .9rem; border-radius: 50%;
            border: 2px solid rgba(148,163,184,.35); border-top-color: #60a5fa;
            animation: zioClaimSpin .6s linear infinite;
        }
        @keyframes zioClaimSpin { to { transform: rotate(360deg); } }
        .zio-claim[data-state="available"] { border-color: rgba(30,215,96,.5); }
        .zio-claim[data-state="error"] { border-color: rgba(251,113,133,.5); }

        /* Verdict line + suggestion chips. */
        .zio-claim-status {
            min-height: 1.1rem;
            margin: .55rem .25rem 0;
            font-size: .82rem; font-weight: 600; line-height: 1.3;
            color: #94a3b8;
        }
        .zio-claim-status[data-state="available"] { color: #34d399; }
        .zio-claim-status[data-state="error"] { color: #fb7185; }
        .zio-claim-status[data-state="checking"] { color: #94a3b8; }
        .zio-claim-suggest {
            display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
            margin: .5rem .25rem 0;
        }
        .zio-claim-suggest-label { font-size: .78rem; color: #94a3b8; }
        .zio-claim-suggest-list { display: inline-flex; flex-wrap: wrap; gap: .35rem; }
        .zio-claim-suggest-btn {
            display: inline-flex; align-items: center;
            padding: .25rem .6rem;
            font-size: .8rem; font-weight: 600; color: #c7d2fe;
            background: rgba(96,165,250,.1);
            border: 1px solid rgba(96,165,250,.3);
            border-radius: 9999px; cursor: pointer;
            transition: background .18s ease, border-color .18s ease, transform .18s cubic-bezier(.34,1.56,.64,1);
        }
        .zio-claim-suggest-btn:hover {
            background: rgba(96,165,250,.18);
            border-color: rgba(96,165,250,.5);
            transform: translateY(-1px);
        }
        @media (prefers-reduced-motion: reduce) {
            .zio-claim[data-state="checking"] .zio-claim-mark::before { animation: none; }
            .zio-claim-suggest-btn { transition: none; }
            .zio-claim-suggest-btn:hover { transform: none; }
            .pulse-dot { animation: none; }
        }
        .pulse-dot { animation: zioStatPulse 2.2s ease-in-out infinite; }
        @keyframes zioStatPulse {
            0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(61,107,255,.55); }
            50% { opacity: .75; box-shadow: 0 0 0 4px rgba(61,107,255,0); }
        }
        @media (max-width: 1023.98px) {
            .zio-claim-status, .zio-claim-suggest { text-align: center; justify-content: center; }
        }
        html.light-mode .zio-claim-status { color: #64748b; }
        html.light-mode .zio-claim-status[data-state="available"] { color: #059669; }
        html.light-mode .zio-claim-status[data-state="error"] { color: #e11d48; }
        html.light-mode .zio-claim-suggest-label { color: #64748b; }
        html.light-mode .zio-claim-suggest-btn {
            color: #4338ca; background: rgba(99,102,241,.08); border-color: rgba(99,102,241,.28);
        }
        html.light-mode .zio-claim-suggest-btn:hover { background: rgba(99,102,241,.15); border-color: rgba(99,102,241,.45); }
        @media (max-width: 380px) {
            .zio-claim { flex-wrap: wrap; border-radius: 1.1rem; }
            .zio-claim-input { flex-basis: 100%; }
            .zio-claim-btn { flex: 1 1 100%; justify-content: center; }
        }
        @media (min-width: 1024px) {
            .zio-claim { margin-inline: 0; }
            .zio-claim-label { text-align: left; }
        }
        html.light-mode .zio-claim {
            background: #ffffff; border-color: #e2e8f0;
        }
        html.light-mode .zio-claim:focus-within { border-color: #60a5fa; }
        html.light-mode .zio-claim-prefix { color: #64748b; }
        html.light-mode .zio-claim-input { color: #0f172a; }
        html.light-mode .zio-claim-input::placeholder { color: #94a3b8; }
        html.light-mode .zio-claim-label { color: #64748b; }

        /* ============ Hero column order ============
           At ≥lg the Zio universe visual sits on the LEFT and the copy on the
           RIGHT (copy keeps the wider 1.05fr share for the headline). Below lg
           the grid is a single column in DOM order (copy first) so the headline
           still leads on mobile. Done in scoped CSS so no new Tailwind utilities
           are needed (no rebuild). */
        @media (min-width: 1024px) {
            .zio-hero-grid { grid-template-columns: 1fr 1.05fr; }
            .zio-hero-visual { order: 1; }
            .zio-hero-copy { order: 2; }
        }

        /* Room for Zio's speech bubble, which the grid gap cannot give him.
           The bubble is absolutely positioned at `bottom: 100%` of the orbit,
           so it hangs ABOVE this column's box and the gap between the two
           stacked rows measures from the box, not from the bubble. Stacked,
           that put the bubble 5px INTO the proof badges at 390px and 12px at
           430px -- reserving the space here is what makes the gap mean what it
           says.

           32px, because that is the smallest value that clears the badges at
           every width from 360px to the lg breakpoint (measured: 36px of
           clearance at 360, 27 at 390, 20 at 430 and 600, 53 at 767). Not
           needed at lg and up, where the two columns sit side by side and
           there is nothing above the bubble to run into. */
        @media (max-width: 1023px) {
            .zio-hero-visual { padding-top: 32px; }
        }

        /* ============ Orbital Zio visual ============ */
        /* ---- Hero grid ----
           A square grid drawn in two hairline gradients, with a scatter of
           single cells that glow up and fade out. It is masked away toward
           the edges so it never meets the page border as a hard line, which
           is what keeps a texture from reading as a table. Both the grid and
           the tiles are drawn from tokens, so the whole thing inverts for
           dark mode by redefining two colours. */
        /* The field holds the grid, the glowing tiles and the feature icons, pinned
           to the section so all three share one origin. It sits BELOW the copy
           in paint order (the copy's own wrapper is positioned and comes
           later), so a tile that wanders near the text passes behind it. */
        .zio-field {
            position: absolute; inset: 0; z-index: 0; pointer-events: none;
            /* The tiles size off the cell, not off the old orbit box, so a
               tile is always the same fraction of a cell as the grid it sits
               in — three quarters, which leaves a clear margin of ground on
               every side of it. */
            --node: calc(var(--grid) * .5);
            /* How faint an icon is at the top of its fade. One token, so the
               keyframes never carry a colour decision of their own. */
            --zio-node-op: .34;
        }
        .zio-field, .zio-field * { pointer-events: none; }

        /* ---------- the ribbon ----------
           The SVG is in home/partials/hero-ribbon.blade.php, and its viewBox
           is mapped straight onto this box, so the composition is designed in
           the coordinates it lands in. Nothing here filters or blurs: the
           softness is a mask, and the strands do the rest. */
        .zio-ribbon {
            position: absolute; inset: 0;
            z-index: 0; pointer-events: none;
            /* Two masks, intersected.

               The radial feathers the ribbon where it runs out and leaves it
               solid where it exits top right, so the copy column stays on
               clean white with no straight cut anywhere.

               The linear clears the top ~130px. The navbar is transparent
               until the page scrolls, so without this the nav links sit on
               the most saturated part of the sweep and stop being readable —
               the ribbon has to get out of their way, not the other way
               round. Stripe's own ribbon is pale where its nav crosses it;
               this arrives at the same place by fading rather than by luck.

               Both prefixes, and both composite spellings: Safari still wants
               -webkit-, and it names the operation differently. */
            -webkit-mask-image: radial-gradient(96% 104% at 92% 26%, #000 52%, rgba(0,0,0,.78) 76%, transparent 99%),
                                linear-gradient(to bottom, transparent 0, rgba(0,0,0,.30) 68px, #000 138px);
                    mask-image: radial-gradient(96% 104% at 92% 26%, #000 52%, rgba(0,0,0,.78) 76%, transparent 99%),
                                linear-gradient(to bottom, transparent 0, rgba(0,0,0,.30) 68px, #000 138px);
            -webkit-mask-composite: source-in;
                    mask-composite: intersect;
        }
        .zio-ribbon svg { width: 100%; height: 100%; display: block; }
        .rb-g { fill: none; stroke-linecap: round; }
        /* The whole sheet breathes, very slowly, as one object. Two groups
           drifting against each other at periods that do not divide is enough
           for the fold to open and close; animating individual strands would
           be hundreds of animations for an effect nobody can see. */
        .rb-body  { animation: rbBody  34s ease-in-out infinite alternate; }
        .rb-fold  { animation: rbFold  41s ease-in-out infinite alternate; }
        .rb-sheen { animation: rbSheen 29s ease-in-out infinite alternate; }
        @keyframes rbBody  { to { transform: translate3d(-1.4%, 1.1%, 0) rotate(-1.1deg); } }
        @keyframes rbFold  { to { transform: translate3d(1.8%, -1.4%, 0) rotate(1.4deg); } }
        @keyframes rbSheen { to { transform: translate3d(-2.2%, 1.8%, 0); } }
        html:not(.light-mode) .zio-ribbon { opacity: .58; }
        /* On a phone the hero stacks and the copy runs the full width, so a
           ribbon spanning the whole box would lie straight under the body
           text -- which is exactly what it did. Anchoring it to the top strip
           instead keeps it a corner flourish: the viewBox now maps onto a
           short wide box, so the sweep stays a sweep instead of stretching
           into a vertical curtain down the page. */
        @media (max-width: 1023px) {
            .zio-ribbon {
                inset: 0 0 auto 0;
                height: min(46vh, 380px);
                opacity: .62;
                -webkit-mask-image: radial-gradient(104% 118% at 88% 16%, #000 40%, rgba(0,0,0,.6) 70%, transparent 96%);
                        mask-image: radial-gradient(104% 118% at 88% 16%, #000 40%, rgba(0,0,0,.6) 70%, transparent 96%);
                -webkit-mask-composite: source-over;
                        mask-composite: add;
            }
        }
        @media (prefers-reduced-motion: reduce) {
            .rb-body, .rb-fold, .rb-sheen { animation: none !important; }
        }

        /* ---------- the closing strip ---------- */
        /* The strip is a ROW OF THE LATTICE, not a bar laid over it.

           It had one rule along its top, an arbitrary 20px of padding either
           side of the text, and an opaque page-coloured ground that hid the
           lattice's vertical lines behind it. So it floated: a band whose
           height agreed with nothing on the page, open at the bottom, sitting
           on top of the grid rather than in it.

           Three things make it a row instead. Its height is exactly one cell,
           so it is the same size as everything else the lattice measures. It
           is ruled top AND bottom, at the lattice's own weight and colour --
           the missing bottom border is what left it looking unfinished. And
           it redraws the lattice's vertical lines across itself, at the same
           `--grid` size and the same `50%` phase, so they continue straight
           through the band and the items sit between them. */
        .zio-strip {
            position: absolute; left: 0; right: 0; bottom: 0;
            z-index: 3;
            height: var(--grid);
            display: flex; align-items: center;
            border-top: 1px solid var(--fs-rule, rgba(255,255,255,.14));
            border-bottom: 1px solid var(--fs-rule, rgba(255,255,255,.14));
            background-color: var(--fs-page, #fff);
            background-image: linear-gradient(to right, var(--strip-line, rgba(15,23,42,.05)) 1px, transparent 1px);
            background-size: var(--grid) 100%;
            background-position: 50% 0;
            overflow: hidden;
            /* Items enter and leave rather than being chopped at the edge. */
            -webkit-mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
                    mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
        }
        html:not(.light-mode) .zio-strip { --strip-line: rgba(255,255,255,.055); }

        /* ...and the last piece: the hero's own height has to be a whole
           number of cells, or the row is the right size in the wrong place.

           The lattice tiles from the TOP of the hero, so its horizontal lines
           fall at 0, g, 2g and so on. The strip is pinned to the BOTTOM. With
           the hero at 100svh -- 950px against an 81px cell -- the last line
           landed at 891 and the strip's top edge at 869: a cell-tall band
           sitting 22px off the grid it is meant to belong to.

           Rounding the hero's min-height down to a whole number of cells puts
           a line on its bottom edge, and therefore on the strip's top edge
           too. Guarded on `round()` like the rail snapping in
           surfaces.blade.php, and scoped to the breakpoint where the strip is
           actually pinned -- below `lg` it goes static and none of this
           applies. It costs less than one cell of hero height. */
        @media (min-width: 1024px) {
            @supports (height: round(down, 10px, 3px)) {
                #hero { min-height: round(down, 100svh, var(--grid)); }
            }
        }
        .zio-strip-track { display: flex; width: max-content; animation: zioStrip 46s linear infinite; }
        .zio-strip:hover .zio-strip-track { animation-play-state: paused; }
        /* Two identical runs side by side, translated by exactly half the
           track — which is one whole run — so the seam never shows. */
        @keyframes zioStrip { to { transform: translateX(-50%); } }
        .zio-strip-run { display: flex; align-items: center; gap: 52px; padding-right: 52px; }
        .zio-strip-item {
            display: inline-flex; align-items: center; gap: 9px;
            white-space: nowrap;
            font-size: 14px; font-weight: 600; letter-spacing: -.005em;
            color: var(--fs-strip, #6B7396);
        }
        html:not(.light-mode) .zio-strip-item { color: rgba(255,255,255,.55); }
        .zio-strip-item i { font-size: 13px; opacity: .65; }
        @media (max-width: 1023px) { .zio-strip { position: static; margin-top: 40px; } }
        @media (prefers-reduced-motion: reduce) { .zio-strip-track { animation: none; } }

        /* The lattice, after Stripe's. Theirs is not a graph-paper field —
           it is a small number of structural rules that say where the page's
           columns are, drawn faintly and running the full height without
           fading out. So: one line every FOUR cells, at about half the weight
           it was, and the soft-edged mask replaced by a plain fade at the far
           right only. The cell itself is unchanged, so the tiles and the
           navbar still land on it; the lattice just stops shouting. */
        .zio-grid {
            position: absolute; inset: 0;
            pointer-events: none;
            --line: rgba(15,23,42,.05);
            --tile: rgba(61,107,255,.055);
            background-image:
                linear-gradient(to right, var(--line) 1px, transparent 1px),
                linear-gradient(to bottom, var(--line) 1px, transparent 1px);
            /* 50% across is what pins a cell CENTRE to the viewport centre —
               the whole reason the tiles and the navbar can line up to this
               without measuring anything. Vertically it tiles from the top. */
            background-position: 50% 0;
            background-size: var(--grid) var(--grid);
            -webkit-mask-image: linear-gradient(to right, #000 0%, #000 72%, transparent 100%);
                    mask-image: linear-gradient(to right, #000 0%, #000 72%, transparent 100%);
        }
        html:not(.light-mode) .zio-grid {
            --line: rgba(255,255,255,.055);
            --tile: rgba(120,150,255,.09);
        }
        .zio-tile {
            position: absolute;
            left: calc(50% + var(--c) * var(--grid) - var(--grid) / 2);
            top:  calc(var(--r) * var(--grid));
            width: var(--grid); height: var(--grid);
            background: radial-gradient(closest-side, var(--tile), transparent 92%);
            opacity: 0;
            animation: zioTile var(--d) ease-in-out infinite;
            animation-delay: var(--delay);
        }
        @keyframes zioTile {
            0%, 100% { opacity: 0; }
            18%      { opacity: 1; }
            46%      { opacity: .55; }
            70%      { opacity: 0; }
        }
        /* Below the two-column breakpoint the hero stacks and there is no
           clear left-hand field to scatter into — a tile would land on the
           headline. The grid stays; the tiles come back at lg. */
        @media (max-width: 1023px) { .zio-field .zio-node { display: none; } }

        .zio-orbit-wrap { display: flex; align-items: center; justify-content: center; width: 100%; }
        .zio-orbit {
            --size: clamp(300px, 40vw, 500px);
            /* Node tiles scale WITH the orbit (proportional, not a fixed px) so the
               radial clearance between the three rings holds at every breakpoint. */
            --node: clamp(38px, calc(var(--size) * 0.092), 50px);
            /* Three concentric node-orbit radii (fractions of --size). Each icon's
               CENTER sits precisely on its dashed ring. The ~0.125 gap between rings
               exceeds the node fraction (~0.092), so tiles never collide. */
            --r1: calc(var(--size) * 0.300);
            --r2: calc(var(--size) * 0.425);
            --r3: calc(var(--size) * 0.550);
            position: relative;
            width: var(--size);
            height: var(--size);
        }

        .zio-glow {
            position: absolute; inset: -8%;
            border-radius: 50%;
            background: radial-gradient(circle at 50% 45%, rgba(61,107,255,.28), rgba(110,97,255,.12) 45%, transparent 70%);
            filter: blur(8px);
            z-index: 0;
            animation: zioGlowPulse 7s ease-in-out infinite;
        }
        @keyframes zioGlowPulse { 0%,100% { opacity: .85; transform: scale(1); } 50% { opacity: 1; transform: scale(1.06); } }

        /* ---- Where the feature tiles live ----
           They used to orbit on three rotating rings. They now sit in cells of
           the page grid and hop between them.

           The maths is only this: the grid is drawn with
           background-position-x:50%, which puts a cell CENTRE on the viewport
           centre, so cell centres run at 50% + k*cell across and (k+.5)*cell
           down. A tile placed at exactly those coordinates is dead centre in
           a cell at every viewport width, with no measuring and no script.

           Movement is one shared keyframe reading each tile's own --x1..--y3,
           which are corners of its own 2x2 square. Home cells sit on an even
           lattice, so no two squares overlap and seventeen tiles can wander
           for as long as you leave the page open without ever colliding. */
        .zio-node {
            position: absolute;
            /* Both coordinates are in WHOLE grid modules now, not in --cell.
               That is the whole fix: the squares are drawn at --grid but the
               nodes were placed on the 58px --cell lattice, so a node could
               only land in a square's centre by coincidence -- and with
               --grid four cells wide, it never did. The horizontal cell
               centres fall on 50% + k*--grid exactly (the grid is phased so a
               cell centre is on the viewport centre), and the vertical ones
               on (k + .5)*--grid, which is where these two lines put them. */
            left: calc(50% + var(--x) * var(--grid));
            top:  calc((var(--y) + .5) * var(--grid));
            width: var(--node); height: var(--node);
            margin: calc(var(--node) / -2) 0 0 calc(var(--node) / -2);
            /* Decoration. Nothing in the field is a target. */
            pointer-events: none;
            opacity: 0;
            transform: translate(0, 0);
            /* Two animations on ONE period. zioPlace moves the icon between
               four cells; zioShow fades it in and out four times. They are
               phase-locked so every move happens while the icon is at zero,
               which is what makes it read as appearing somewhere new rather
               than sliding across the field. */
            animation: zioPlace var(--dur) step-end var(--delay) infinite,
                       zioShow  var(--dur) linear   var(--delay) infinite;
        }
        /* step-end, so the position SNAPS at 25/50/75/100% instead of
           travelling. At each of those instants zioShow is holding zero, and
           a hop is a whole square, so the icon is centred in a cell at every
           moment it is actually visible. */
        @keyframes zioPlace {
            0%   { transform: translate(0, 0); }
            25%  { transform: translate(calc(var(--x1) * var(--grid)), calc(var(--y1) * var(--grid))); }
            50%  { transform: translate(calc(var(--x2) * var(--grid)), calc(var(--y2) * var(--grid))); }
            75%  { transform: translate(calc(var(--x3) * var(--grid)), calc(var(--y3) * var(--grid))); }
            100% { transform: translate(0, 0); }
        }
        /* Each icon is up for four windows of about 6.5% of its own cycle, so
           it shows roughly a quarter of the time. Seventeen icons at that duty
           puts four or five on screen at once, and because the periods (29s to
           59s in 5s steps) share no common factor the set never repeats -- so
           the scatter keeps looking arbitrary without a line of JS. */
        @keyframes zioShow {
            0%,    1.5%  { opacity: 0; }
            3.2%,  6.8%  { opacity: var(--zio-node-op); }
            8.6%, 26.5%  { opacity: 0; }
            28.2%, 31.8% { opacity: var(--zio-node-op); }
            33.6%, 51.5% { opacity: 0; }
            53.2%, 56.8% { opacity: var(--zio-node-op); }
            58.6%, 76.5% { opacity: 0; }
            78.2%, 81.8% { opacity: var(--zio-node-op); }
            83.6%, 100%  { opacity: 0; }
        }


        .zio-node-thumb { width: 100%; height: 100%; object-fit: contain; display: block; }
        /* Visually hidden, NOT display:none -- the difference matters, because
           a screen reader skips display:none entirely and this list is the
           only text seventeen wordless icons have. */
        .zio-toollist {
            position: absolute; width: 1px; height: 1px;
            padding: 0; margin: -1px; overflow: hidden;
            clip-path: inset(50%); white-space: nowrap; border: 0;
        }

        .zio-core {
            position: absolute; top: 50%; left: 50%;
            transform: translate(-50%, -50%);
            z-index: 3;
            display: flex; flex-direction: column; align-items: center;
            pointer-events: none;
        }
        .zio-mascot-halo {
            position: absolute; top: 38%; left: 50%;
            width: calc(var(--size) * 0.5); height: calc(var(--size) * 0.5);
            transform: translate(-50%, -50%);
            border-radius: 50%;
            background: radial-gradient(circle, rgba(61,107,255,.45), transparent 68%);
            filter: blur(10px);
            z-index: -1;
            animation: zioHalo 5s ease-in-out infinite;
        }
        @keyframes zioHalo { 0%,100% { opacity: .55; transform: translate(-50%,-50%) scale(1); } 50% { opacity: .9; transform: translate(-50%,-50%) scale(1.12); } }
        /* The float lives on the face, not the artwork, so the eyelids and the
           mouth ride along with it instead of drifting off the eyes. */
        .zio-face {
            position: relative;
            /* Published as its own custom property so the few places that need
               a real length rather than a percentage — the lash line's
               thickness — can scale with the artwork. */
            --fw: calc(var(--size) * 0.42);
            width: var(--fw);
            aspect-ratio: 1 / 1;
            animation: zioFloat 6.5s ease-in-out infinite;
            transform-origin: 50% 80%;
        }
        .zio-mascot {
            display: block;
            width: 100%;
            height: auto;
            /* No glow. The old blue drop-shadow spread a haze roughly 80px
               wide around the mascot, which on a white page is not white:
               the pixels beside his head measured #D1DBFF. */
        }
        /* The rise is a fraction of the artwork, not a fixed 12px, because Zio
           is now drawn at two sizes an order of magnitude apart. At the hero's
           --size this still works out to the 12px it always was; at the hub's
           124px it is 3px, which reads as the same gentle bob rather than a
           small head jumping out of its card. */
        @keyframes zioFloat {
            0%,100% { transform: translateY(0) rotate(-1.5deg) scale(1); }
            50%     { transform: translateY(calc(var(--size, 500px) * -0.024)) rotate(1.5deg) scale(1.02); }
        }

        /* ---- Antennae ----
           Each is its own layer on the same canvas, pinned at inset:0, so it
           needs no positioning of its own — only a pivot.

           They sit BEHIND the body, and that is the whole trick. In the
           artwork an antenna is drawn resting along the head's dome for its
           entire arc, so there is no single point where it "joins": cutting
           it out leaves a long edge that follows the dome exactly. In front
           of the body, the first degree of sway slides that edge off the
           dome and the antenna reads as a sticker peeling away. Behind it,
           the head's own silhouette is the boundary, and it never moves. The
           layers also carry 90px of head with them below the cut, so the
           edge is buried too deep for a few degrees to ever bring it out.

           Cutting them out took two goes. The first cut took, per column,
           everything above where the head's silhouette began — but where a
           stalk merges into the head there is no gap between them, so that
           column's silhouette starts partway UP the stalk. The stalk got
           split lengthwise, half to the antenna and half to the body, and
           the two halves came apart the moment it swayed. The cut now
           follows a running median of the silhouette, which ignores those
           narrow spikes and recovers the head's actual dome, so each antenna
           comes away whole.

           They sway on purposely mismatched durations, so the pair never
           falls into a mechanical lockstep. */
        .zio-ant {
            position: absolute; inset: 0;
            background-repeat: no-repeat;
            background-size: 100% 100%;
            pointer-events: none;
            z-index: 0;
        }
        .zio-mascot { position: relative; z-index: 1; }
        .zio-ant--l {
            background-image: url('../../branding/zio-antenna-l.png');
            transform-origin: 33% 26%;
            animation: zioAntL 3.6s ease-in-out infinite;
        }
        .zio-ant--r {
            background-image: url('../../branding/zio-antenna-r.png');
            transform-origin: 68% 22%;
            animation: zioAntR 4.3s ease-in-out infinite;
        }
        @keyframes zioAntL {
            0%,100% { transform: rotate(-2.6deg); }
            50%     { transform: rotate(2.2deg); }
        }
        @keyframes zioAntR {
            0%,100% { transform: rotate(2.4deg); }
            50%     { transform: rotate(-2.2deg); }
        }

        /* ---- Blink ----
           The lid is a patch of head colour that wipes down over the eye, and
           the whole trick is that it must not read AS a patch. Two things do
           that. It is masked with a radial gradient, so it has no edge at all
           — it is fully opaque over the iris and dissolves into the head well
           before its own boundary. And it is half again as wide as the iris,
           so the dissolve happens over skin rather than over the eye. What is
           left to see is the lash line, which is the part that says "shut". */
        /* Above the body, which is itself above the antennae (z-index 1). */
        .zio-lid, .zio-mouth-gate { z-index: 2; }
        .zio-lid {
            position: absolute;
            clip-path: inset(0 0 100% 0);
            animation: zioBlink 5.6s ease-in-out infinite;
            will-change: clip-path;
        }
        .zio-lid::before {
            content: ''; position: absolute; inset: 0;
            background: linear-gradient(var(--c-top), var(--c-bot));
            -webkit-mask-image: radial-gradient(closest-side, #000 72%, transparent 100%);
                    mask-image: radial-gradient(closest-side, #000 72%, transparent 100%);
        }
        /* Iris on the 758px master: left x 170-292 / y 173-291, right
           x 409-542 / y 201-323. Each lid is 1.5x that box, centred on it. */
        .zio-lid--l { left: 18.4%; top: 18.9%; width: 24.1%; height: 23.4%;
                      --c-top: #569CFF; --c-bot: #5C7AFF; }
        .zio-lid--r { left: 49.6%; top: 22.5%; width: 26.3%; height: 24.1%;
                      --c-top: #A3C4FF; --c-bot: #7C72FF;
                      animation-delay: .07s; }
        /* The lash: a circle showing only its lower border, which draws the
           closed eye as an arc rather than a straight line. */
        .zio-lid::after {
            content: ''; position: absolute;
            left: 20%; right: 20%; top: 30%; height: 42%;
            border: 0 solid #0E1A55;
            border-bottom-width: calc(var(--fw) * .016);
            border-radius: 50%;
        }
        @keyframes zioBlink {
            0%, 88%, 100% { clip-path: inset(0 0 100% 0); }
            91%, 94%      { clip-path: inset(0 0 0 0); }
            97%           { clip-path: inset(0 0 100% 0); }
        }

        /* ---- Mouth ----
           The gate hides the whole mouth between lines, which lets the painted
           smile underneath be Zio's resting face; while a line is up the mouth
           opens and closes over it. It opens by growing its HEIGHT rather than
           scaling, because a scale would stretch the tongue inside it with it.
           The painted smile stays visible along the top edge and reads as the
           upper lip. */
        .zio-mouth-gate {
            position: absolute;
            left: 39.9%; top: 41.2%;
            width: 9.9%; height: 3.0%;
            animation: zioMouthGate 4s linear infinite;
        }
        .zio-mouth {
            display: block; position: absolute; inset: 0 0 auto 0;
            height: 100%;
            background: #1B0E3C;
            border-radius: 42% 42% 50% 50% / 22% 22% 78% 78%;
            overflow: hidden;
            animation: zioChatter .36s ease-in-out infinite;
        }
        .zio-tongue {
            position: absolute; left: 20%; right: 20%; bottom: -14%;
            height: 62%;
            background: #FF6E9E;
            border-radius: 50% 50% 45% 45% / 65% 65% 35% 35%;
        }
        @keyframes zioChatter {
            0%, 100% { height: 100%; }
            50%      { height: 205%; }
        }
        /* Synced to the bubbles, not merely near them. A bubble's slot is 4s
           (16s / four lines): it finishes appearing at 2.5% of the 16s cycle
           and starts leaving at 21% — 0.4s and 3.36s inside its own slot. The
           gate runs on the same 4s and opens at 11%, shuts at 84%: Zio starts
           talking as the line lands and has finished the sentence by the time
           it fades. Move one of these and the other has to move with it. */
        @keyframes zioMouthGate {
            0%, 9%    { opacity: 0; }
            11%, 83%  { opacity: 1; }
            85%, 100% { opacity: 0; }
        }

        /* ---- Speech bubbles ----
           Four lines share one slot above Zio's head and take turns: each runs
           the same 16s animation offset by four seconds, so exactly one is
           visible and the hero never reflows. */
        .zio-says {
            position: absolute;
            left: 50%; bottom: calc(100% - 2px);
            transform: translateX(-50%);
            width: min(21.5rem, 76vw);
            aspect-ratio: 600 / 300;
            z-index: 5;
        }
        /* A real ellipse, the way a comic book draws one -- curved along the
           top AND the bottom. border-radius:999px could never do this: on a
           box wider than it is tall that is a stadium, flat across the middle
           of both long edges, which is why it kept reading as a pill however
           large the radius went.

           Fixed aspect rather than shrink-to-fit, because an ellipse crops its
           own corners: text has to be laid inside the curve, not measured
           around it. Every line therefore gets the same bubble, which is also
           how a comic panel does it. */
        .zio-bubble {
            position: absolute; inset: 0;
            opacity: 0;
            transform: translateY(6px) scale(.96);
            transform-origin: 50% 88%;
            animation: zioSay var(--zio-cycle, 16s) ease-in-out infinite;
            animation-delay: calc(var(--i) * 4s);
        }
        .zio-bubble-shape {
            position: absolute; inset: 0;
            width: 100%; height: 100%;
            overflow: visible;
        }
        .zio-bubble-shape path {
            fill: var(--fs-chip, #17162A);
            /* Not --fs-rule. That token is a hairline meant to separate two
               surfaces of nearly the same colour, and at this size on white it
               vanished -- the bubble read as a faint dent in the page. A drawn
               bubble needs a drawn line, so this one is its own colour. */
            stroke: #c2c8de;
            /* The box stretches to the text, which would thicken the outline
               with it; non-scaling-stroke keeps 2px meaning 2px at every
               width. */
            stroke-width: 2px;
            vector-effect: non-scaling-stroke;
        }
        html:not(.light-mode) .zio-bubble-shape path { stroke: rgba(255,255,255,.22); }
        /* 43.3% is the ellipse's centre in the viewBox (cy 130 of 300), not
           the box's -- the tail owns the bottom fifth. */
        .zio-bubble-text {
            position: absolute; left: 50%; top: 43.3%;
            transform: translate(-50%, -50%);
            width: 78%;
            text-align: center; text-wrap: balance;
            font-size: clamp(13.5px, 1.12vw, 16.5px); font-weight: 650; line-height: 1.34;
            color: #fff;
        }
        html.light-mode .zio-bubble-text { color: #0F172A; }
        /* Has to live AFTER the .zio-says rule above, not up with the other
           mobile overrides in the ribbon block: same specificity, so order is
           what decides, and from up there it lost every time. On a phone Zio
           stands below the copy and the bubble hangs off his head into the
           stat row, so it gives back the width it gained on desktop. */
        @media (max-width: 1023px) {
            .zio-says { width: min(15.5rem, 60vw); }
        }
        /* No translate(-50%) any more: the bubble is inset:0 inside .zio-says
           and the parent does the centring, so these only carry the lift. */
        /* Percentages cannot be a CSS variable, so this one set of keyframes
           is emitted for the number of lines the page actually has. See the
           note beside `$zioCount` where the markup is built. */
        @keyframes zioSay {
            0%                                          { opacity: 0; transform: translateY(6px) scale(.96); }
            5%, 80%              { opacity: 1; transform: translateY(0) scale(1); }
            95%, 100%                        { opacity: 0; transform: translateY(-4px) scale(.98); }
        }
        .zio-core-label {
            margin-top: 6px;
            display: inline-flex; align-items: center; gap: 6px;
            padding: 5px 12px;
            border-radius: 999px;
            font-size: 11px; font-weight: 700;
            background: rgba(255,255,255,.08);
            border: 1px solid rgba(255,255,255,.14);
            backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
            color: #fff; white-space: nowrap;
        }
        .zio-core-label i { color: var(--c2); font-size: 10px; }

        /* ---- No-JS fallback list (only rendered inside <noscript>) ---- */
        .zio-noscript {
            list-style: none;
            margin: 22px auto 0;
            padding: 0;
            display: grid;
            gap: 8px;
            width: 100%;
            max-width: 460px;
        }
        .zio-noscript-item {
            display: flex; align-items: flex-start; gap: 11px;
            padding: 11px 13px;
            border-radius: 14px;
            background: rgba(255,255,255,.06);
            border: 1px solid rgba(255,255,255,.12);
            backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
        }
        .zio-noscript-ic { width: 34px; height: 34px; object-fit: contain; flex: 0 0 auto; }
        .zio-noscript-text { display: flex; flex-direction: column; text-align: left; }
        .zio-noscript-title { font-size: 13px; font-weight: 800; color: #fff; line-height: 1.25; }
        .zio-noscript-title::before {
            content: ''; display: inline-block;
            width: 7px; height: 7px; margin-right: 7px;
            border-radius: 50%; background: var(--ac, var(--c2));
            vertical-align: middle;
        }
        .zio-noscript-desc { margin-top: 2px; font-size: 11.5px; font-weight: 500; color: rgba(214,222,255,.82); line-height: 1.4; }
        .zio-noscript-tag {
            align-self: flex-start;
            margin-top: 6px;
            padding: 2px 8px;
            border-radius: 999px;
            font-size: 9.5px; font-weight: 800; letter-spacing: .02em;
            color: color-mix(in srgb, var(--ac, var(--c2)) 75%, white 25%);
            background: color-mix(in srgb, var(--ac, var(--c2)) 16%, transparent);
            border: 1px solid color-mix(in srgb, var(--ac, var(--c2)) 38%, transparent);
        }

        html.light-mode .zio-noscript-item {
            background: #ffffff; border-color: #e2e8f0;
            box-shadow: 0 10px 24px -14px rgba(15,23,42,.35);
        }
        html.light-mode .zio-noscript-title { color: #0f172a; }
        html.light-mode .zio-noscript-desc { color: #475569; }
        html.light-mode .zio-noscript-tag {
            color: color-mix(in srgb, var(--ac, var(--c2)) 60%, black 40%);
            background: color-mix(in srgb, var(--ac, var(--c2)) 12%, white 88%);
            border-color: color-mix(in srgb, var(--ac, var(--c2)) 30%, white 70%);
        }

        /* ---- Tablet (single-column, sm→below-lg): the orbit is stacked under the
               copy in a wide column, so the vw-based size leaves it looking small and
               lost. Bump --size here so it fills the column with more presence. Every
               sub-element (mascot, node radius) derives from --size, so they scale in
               lockstep. Does NOT touch the ≥lg two-column desktop layout. ---- */
        @media (min-width: 640px) and (max-width: 1023.98px) {
            .zio-orbit { --size: clamp(360px, 54vw, 460px); }
        }

        /* ---- Narrow single-column phones (anything below the ≥640px tablet block).
               Across this whole range `--size` sits pinned at its 300px floor
               (40vw < 300px until ~750px), but the DEFAULT `--node` clamps up to its
               38px MIN — i.e. ~0.127 of --size, which EXCEEDS the ~0.125 inter-ring
               gap, so the inner/middle rings kiss at the shared 3- and 9-o'clock
               nodes (and the outer ring pokes past the box on ~320px screens). Pull
               all three radii in and shrink the tiles so every icon stays fully
               visible, clears the central mascot, and never collides. Bounded at
               639.98px so it dovetails exactly with the ≥640px tablet block (which
               grows --size enough that the default radii breathe again). The
               ≥640px tablet / ≥lg desktop sizing is untouched. ---- */
        @media (max-width: 639.98px) {
            .zio-orbit {
                --node: clamp(32px, calc(var(--size) * 0.088), 42px);
                --r1: calc(var(--size) * 0.275);
                --r2: calc(var(--size) * 0.390);
                --r3: calc(var(--size) * 0.500);
            }
        }

        /* ---- Light mode ---- */
        html.light-mode .zio-glow {
            background: radial-gradient(circle at 50% 45%, rgba(61,107,255,.18), rgba(110,97,255,.08) 45%, transparent 70%);
        }
        html.light-mode .zio-pulse { border-color: rgba(37,66,199,.28); }
        html.light-mode .zio-ring { border-color: rgba(37,66,199,.26); }
        html.light-mode .zio-ring--r1 { border-color: rgba(37,66,199,.14); }
        html.light-mode .zio-ring--r2 { border-color: rgba(37,66,199,.20); }
        /* The icons are decoration now, so all light mode has to say
           about them is how faint they are. */
        html.light-mode .zio-field { --zio-node-op: .30; }
        html.light-mode .zio-mascot-halo { background: radial-gradient(circle, rgba(61,107,255,.28), transparent 68%); }
        html.light-mode .zio-core-label {
            background: #ffffff; border-color: #e2e8f0; color: #0f172a;
            box-shadow: 0 6px 16px -10px rgba(15,23,42,.3);
        }
        html.light-mode .zio-cta-ghost {
            border-color: #cbd5e1; color: #0f172a; background: #ffffff;
        }
        html.light-mode .zio-cta-ghost:hover { background: #f1f5f9; border-color: #94a3b8; }

        /* ---- Reduced motion: freeze the orbit + ambient layers (nodes stay
               placed + upright, everything visible, popovers still work) ---- */
        @media (prefers-reduced-motion: reduce) {
            .zio-rotor, .zio-face, .zio-mascot, .zio-mascot-halo,
            .zio-glow, .zio-pulse, .zio-node, .zio-node-thumb,
            .zio-lid, .zio-mouth, .zio-mouth-gate, .zio-bubble, .zio-ant, .zio-tile {
                animation: none !important;
            }
            /* The grid stays; only its blinking stops, on a low steady value
               so the scatter still reads as texture rather than vanishing. */
            .zio-tile { opacity: .5 !important; }
            /* With the animation stopped, every icon would otherwise hold at
               opacity 0 and the field would be empty. They settle at their
               home cell, at the faint token, all seventeen at once -- the
               scatter without the blinking. */
            .zio-node { opacity: var(--zio-node-op) !important; transform: translate(0, 0) !important; }
            .zio-node-thumb { transform: scale(1) !important; }
            .zio-pulse { opacity: 0 !important; }
            /* Zio holds still: eyes open, antennae level, resting smile, and
               the first line left on screen rather than a cycle nobody asked
               to watch. */
            .zio-lid { clip-path: inset(0 0 100% 0) !important; }
            .zio-ant { transform: none !important; }
            .zio-mouth-gate { opacity: 0 !important; }
            .zio-bubble { opacity: 0 !important; transform: none !important; }
            .zio-bubble:first-child { opacity: 1 !important; }
        }
    