/* ==========================================================================
   Digitalsom design system
   Derived from the Variant design (digitalsomo-design.html). Every page of
   the site is built from the tokens and components in this one file.
   ========================================================================== */

/* ---------- Tokens ---------- */
:root {
    --bg: #F9FAFA;
    --surface: #FFFFFF;
    --ink: #082C2B;
    --ink-body: rgba(8, 44, 43, 0.75);
    --ink-dim: rgba(8, 44, 43, 0.68);
    --ink-mute: rgba(8, 44, 43, 0.35); /* non-text only: borders, decoration */
    --ink-line: rgba(8, 44, 43, 0.2);
    --ink-faint: rgba(8, 44, 43, 0.08);
    --ink-wash: rgba(8, 44, 43, 0.05);
    --accent: #6D6D6D;
    --accent-wash: rgba(109, 109, 109, 0.12);
    --accent-ring: rgba(109, 109, 109, 0.5);
    --panel-top: #082C2B;
    --panel-bottom: #0C3D3B;
    --code-bg: #0C1A1A;
    --bg-glass: rgba(249, 250, 250, 0.9);

    /* Text and lines on the dark green surfaces (panel, CTA band, mobile bar, code panel). */
    --on-dark: #fff;
    --on-dark-body: rgba(255, 255, 255, 0.75);
    --on-dark-dim: rgba(255, 255, 255, 0.6);
    --on-dark-line: rgba(255, 255, 255, 0.3);
    --on-dark-faint: rgba(255, 255, 255, 0.08);
    --on-dark-wash: rgba(255, 255, 255, 0.03);

    --focus: var(--ink);
    --focus-on-dark: var(--on-dark);

    --shadow-pop: 0 16px 32px -16px rgba(8, 44, 43, 0.18);
    --shadow-card: 0 12px 24px -10px rgba(8, 44, 43, 0.08);
    --shadow-focus: 0 0 0 3px rgba(8, 44, 43, 0.08);

    /* Mono micro type floor: no mono label goes below --fs-micro. */
    --fs-micro: 0.65rem;
    --fs-caption: 0.72rem;

    --sans: 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

    /* Glass material, taken from the Variant glass UI and translated to the light palette.
       Fills are white alphas, lines and shadows are ink alphas: no new hues. */
    --glass-fill: linear-gradient(135deg, rgba(255, 255, 255, 0.58) 0%, rgba(255, 255, 255, 0.22) 100%);
    --glass-fill-strong: linear-gradient(135deg, rgba(255, 255, 255, 0.82) 0%, rgba(255, 255, 255, 0.58) 100%);
    --glass-fill-subtle: linear-gradient(135deg, rgba(255, 255, 255, 0.46) 0%, rgba(255, 255, 255, 0.14) 100%);
    --glass-sheen: radial-gradient(120% 90% at 15% 0%, rgba(255, 255, 255, 0.9) 0%, rgba(255, 255, 255, 0.25) 30%, transparent 55%);
    --glass-line: rgba(255, 255, 255, 0.7);      /* the bright rim of the glass */
    --glass-divider: rgba(8, 44, 43, 0.09);      /* lines inside a glass surface */
    --glass-outline: 0 0 0 1px rgba(8, 44, 43, 0.07);
    --glass-edge: rgba(255, 255, 255, 0.95);
    --glass-dark-fill: linear-gradient(135deg, rgba(255, 255, 255, 0.07) 0%, rgba(255, 255, 255, 0.01) 100%);
    --glass-dark-sheen: radial-gradient(circle at 20% 0%, rgba(255, 255, 255, 0.09) 0%, transparent 50%);
    --blur-sm: blur(12px) saturate(140%);
    --blur-md: blur(20px) saturate(150%);
    --blur-lg: blur(28px) saturate(160%);
    /* Three elevation levels: 1 strips and small tiles, 2 cards, 3 feature cards, hover and menus. */
    --glass-shadow-1: inset 0 1px 0 0 var(--glass-edge), var(--glass-outline), 0 8px 18px -10px rgba(8, 44, 43, 0.18);
    --glass-shadow-2: inset 0 1px 0 0 var(--glass-edge), inset 0 0 0 1px rgba(255, 255, 255, 0.25), var(--glass-outline), 0 22px 44px -20px rgba(8, 44, 43, 0.32);
    --glass-shadow-3: inset 0 1px 0 0 var(--glass-edge), inset 0 0 0 1px rgba(255, 255, 255, 0.3), var(--glass-outline), 0 32px 64px -22px rgba(8, 44, 43, 0.4);
    --glass-shadow-dark: inset 0 1px 1px 0 rgba(255, 255, 255, 0.08), inset 0 0 0 1px rgba(255, 255, 255, 0.03), 0 24px 48px -16px rgba(8, 44, 43, 0.45);
    /* Soft blurred light behind the content column, so the glass has something to refract. */
    --ambient-1: rgba(12, 61, 59, 0.34);
    --ambient-2: rgba(12, 61, 59, 0.14);
    --ambient-3: rgba(8, 44, 43, 0.26);
    --ambient-4: rgba(12, 61, 59, 0.18);

    --r-xs: 3px;
    --r-sm: 6px;
    --r-md: 8px;
    --r-lg: 12px;
    --r-glass-sm: 16px; /* small glass: compact cards, strips, menus */
    --r-glass-lg: 24px; /* large glass: cards, feature cards, CTA band */

    /* Content column: the inner width plus the side gutter that the header, hero and footer share. */
    --content-w: 1120px;
    --gutter: 2.5rem;
    --head-h: 4.5rem;

    --dur-fast: 150ms;
    --dur-base: 200ms;
    --dur-slow: 320ms;
    --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
    --ease: cubic-bezier(0.4, 0, 0.2, 1);
    --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
    /* Springs for the menus (CSS linear() curves, the Framer Motion feel without a library). */
    --spring: linear(0, 0.06 2%, 0.23 5%, 0.48 9%, 0.72 14%, 0.88 19%, 0.97 24%, 1.02 30%, 1.035 36%, 1.03 42%, 1.01 52%, 0.998 65%, 1);
    --spring-snappy: linear(0, 0.12 2%, 0.42 6%, 0.75 11%, 0.95 16%, 1.04 21%, 1.06 26%, 1.045 32%, 1.01 42%, 0.997 55%, 1);
    --dur-spring: 520ms;
    --dur-glide: 420ms;
    --dur-morph: 640ms;   /* the header pill-to-bar morph */
    --ease-expand: cubic-bezier(0.22, 1, 0.36, 1); /* strips: fast start, soft landing, no bounce */
    --stagger: 28ms;
    --dur-reveal: 760ms;     /* scroll reveals ([data-reveal], see Scroll reveal) */
    --stagger-reveal: 80ms;  /* the beat between the children of [data-reveal="stagger"] */
    --dur-ambient: 1600ms;   /* ambient scenery fading in or out (the footer's fog) */
    --dur-text-reveal: 600ms; /* one word's rise in the text reveal ([data-text-reveal]) */
    --blur-snapshot: blur(40px) saturate(150%); /* the header's frost over page-transition snapshots */
}
@supports not (transition-timing-function: linear(0, 1)) {
    :root { --spring: var(--ease-out); --spring-snappy: var(--ease-out); }
}

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; line-height: 1.5; } /* scroll-padding-top: see In-page jumps */
body {
    margin: 0;
    background: var(--bg);
    color: var(--ink);
    font-family: var(--sans);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}
/* The ambient light the glass sits on (the Variant environment, in palette greens). */
body::before {
    content: ''; position: fixed; inset: 0; z-index: -1; pointer-events: none;
    background:
        radial-gradient(34rem 28rem at 82% 14%, var(--ambient-1), transparent 70%),
        radial-gradient(30rem 26rem at 52% 48%, var(--ambient-2), transparent 70%),
        radial-gradient(38rem 30rem at 86% 86%, var(--ambient-3), transparent 70%),
        radial-gradient(26rem 22rem at 40% 8%, var(--ambient-4), transparent 70%),
        var(--bg);
}
h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd, pre { margin: 0; font-size: inherit; font-weight: inherit; }
ul, ol { margin: 0; padding: 0; list-style: none; }
a { color: inherit; text-decoration: none; }
img, svg, video, canvas, iframe { display: block; max-width: 100%; }
img { height: auto; }
button, input, select, textarea { font: inherit; color: inherit; margin: 0; }
button { background: none; border: 0; padding: 0; cursor: pointer; }
strong { font-weight: 600; color: var(--ink); }
[hidden] { display: none !important; }

:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: 2px; }
.strip.is-open :focus-visible,
.cta-band :focus-visible { outline-color: var(--focus-on-dark); }
/* The skip link moves focus to <main tabindex="-1">; the whole column needs no ring. */
#main:focus { outline: none; }

.skip-link {
    position: absolute; left: 1rem; top: -4rem; z-index: 100;
    background: var(--ink); color: var(--on-dark); padding: 0.6rem 1rem; border-radius: var(--r-sm);
    font-family: var(--mono); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.1em;
}
.skip-link:focus { top: 1rem; }

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

::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--ink-faint); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--ink-line); }

/* ---------- Layout: sticky glass header, full-width hero, centred content column ---------- */
.layout-main { min-height: 100vh; background: transparent; position: relative; }

.content-wrap {
    padding: 5rem var(--gutter) 5rem;
    max-width: calc(var(--content-w) + 2 * var(--gutter));
    margin: 0 auto;
    display: flex; flex-direction: column;
    gap: 6rem;
}
/* Pages with a page head start their column closer to it. */
.page-head + .content-wrap { padding-top: 3rem; }

/* ---------- Brand ---------- */
.brand { display: inline-flex; align-items: center; gap: 0.75rem; }
.brand-mark { width: 2rem; height: 2rem; flex-shrink: 0; }
.brand-mark .mark-a { fill: currentColor; }
.brand-mark .mark-b { fill: currentColor; opacity: 0.55; }
.brand-name { font-weight: 600; font-size: 1.1rem; letter-spacing: -0.025em; }

/* ---------- Hero (home) ----------
   A full-width inset frame: the generated glass artwork behind, the headline bottom-left,
   and a three-part strip along the bottom. The sticky glass header frosts the art as it scrolls. */
.hero {
    position: relative; isolation: isolate;
    /* Full bleed, starting under the transparent header: the art fades into the page at the top
       (so the bar, brand and CTA sit on pale ground) and at the bottom (so there is no frame). */
    margin: calc(-1 * var(--head-h)) 0 0; overflow: hidden;
    /* Fills the first screen; the headline and strip sit at its foot, the art above them. */
    min-height: 100vh;
    display: flex; flex-direction: column; justify-content: flex-end;
    background: var(--bg);
}
@supports (height: 100dvh) { .hero { min-height: 100dvh; } }
/* overflow: hidden keeps the art's motion (settle, depth drift, light) inside the frame. */
.hero-art { position: absolute; inset: 0; z-index: -1; overflow: hidden; }
.hero-art img { width: 100%; height: 100%; object-fit: cover; object-position: 70% 30%; }
.hero-art::after {
    content: ''; position: absolute; inset: 0; z-index: 2; /* over the art's light layers (Hero motion) */
    background:
        linear-gradient(180deg, rgba(249, 250, 250, 0.9) 0%, rgba(249, 250, 250, 0.35) 14%, rgba(249, 250, 250, 0) 30%),
        linear-gradient(180deg, rgba(249, 250, 250, 0) 36%, rgba(249, 250, 250, 0.6) 72%, var(--bg) 100%);
}
/* Flush with the brand in the header: the same gutter, no centring. */
.hero-body, .hero-strip { width: 100%; }
.hero-body { padding: 10rem var(--gutter) 2rem; }
/* Regular weight and tight tracking, as in the reference: large but quiet. */
.hero-title {
    font-weight: 400; font-size: clamp(2.1rem, 4.4vw, 4rem); line-height: 1.06; letter-spacing: -0.035em;
    max-width: 18ch; text-wrap: balance; color: var(--ink);
}
.hero-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 1.5rem; margin-top: 1.75rem; }
.hero-strip {
    display: grid; grid-template-columns: minmax(0, 1.2fr) auto minmax(0, 1fr) auto; align-items: center; gap: 1.5rem 2rem;
    padding: 1.25rem var(--gutter) 1.5rem; border-top: 1px solid transparent; position: relative;
}
/* The strip's rule, drawn over the transparent border so it can draw across on arrival. */
.hero-strip::before {
    content: ''; position: absolute; left: 0; right: 0; top: -1px; height: 1px; pointer-events: none;
    background: var(--ink-faint); transform-origin: left center;
}
.hero-strip p { font-size: 0.85rem; line-height: 1.55; color: var(--ink-dim); max-width: 24rem; }
.hero-strip .hero-arrow { color: var(--ink); display: inline-flex; }
.hero-strip .hero-arrow svg { width: 1.1rem; height: 1.1rem; }
.hero-trust {
    justify-self: end; text-align: right;
    font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-dim); line-height: 1.7;
}
.hero-trust strong { color: var(--ink); font-weight: 600; }

/* ---------- Hero motion ----------
   One signature moment (the arrival), one living surface (a light on the glass) and precise
   responses on the actions, in the menus' springs and the shared icon motion (site.js, dsIcons).
   Arrival, about 1.2s from first paint. It is pure CSS, so it never waits for site.js, and
   nothing waits on it to be clickable: the art settles out of the page colour from a slight
   zoom; the title's words rise (the shared data-text-reveal, offset by --tr-delay in
   render_hero); the strip's rule draws across; the actions rise; the lead follows the rule
   in; the scroll cue comes last. Start states exist only under html.js-motion and only inside
   one-shot animations (backwards fill), so they always finish. The art resolves with opacity
   and scale rather than a blur (blurring the full-screen art every frame is too heavy), and it
   starts visible, so it is still the first thing painted.
   Containment: check_layout counts transformed children as overflow, so the art, its depth
   drift and its light stay inside .hero-art (overflow: hidden), and everything else rises or
   slides into place within its own box. */
.hero-art img { transform-origin: 70% 35%; }
.js-motion .hero-art img { animation: hero-art-in 900ms cubic-bezier(0.33, 1, 0.68, 1) backwards, hero-art-settle 1100ms var(--ease-out) backwards; }
.js-motion .hero-strip::before { animation: hero-draw 800ms var(--ease-expand) calc(var(--tr-lead, 0ms) + 280ms) backwards; }
.js-motion .hero-actions > * { animation: hero-rise 600ms var(--ease-out) calc(var(--tr-lead, 0ms) + 440ms) backwards; }
.js-motion .hero-actions > :nth-child(2) { animation-delay: calc(var(--tr-lead, 0ms) + 500ms); }
.js-motion .hero-strip > p { animation: hero-follow 560ms var(--ease-out) calc(var(--tr-lead, 0ms) + 560ms) backwards; }
.js-motion .hero-cue { animation: hero-rise 520ms var(--ease-out) calc(var(--tr-lead, 0ms) + 660ms) backwards; }
/* Everything after the art keeps step with the title (offsets above are from the moment it
   plays): while it waits for its fonts (head.js, at most 400ms) the rest waits too, paused in
   its start state, and a page transition's beat (--tr-lead, as in the page heads) holds both. */
.js-motion .hero:has(.hero-title.tr-split:not(.tr-play)) :is(.hero-actions > *, .hero-strip > p, .hero-cue),
.js-motion .hero:has(.hero-title.tr-split:not(.tr-play)) .hero-strip::before { animation-play-state: paused; }
.hero:has(.hero-title.tr-lead) { --tr-lead: 90ms; }
@keyframes hero-art-in { from { opacity: 0.12; } }
@keyframes hero-art-settle { from { scale: 1.06; } }
@keyframes hero-draw { from { transform: scaleX(0); } }
@keyframes hero-rise { from { opacity: 0; translate: 0 0.75rem; } }
@keyframes hero-follow { from { opacity: 0; translate: -0.75rem 0; } }

/* Depth: as the page scrolls, the art drifts down by an eighth of the scroll, so it sits
   behind the text. A scroll timeline scrubs it on the compositor; without one it rests. The
   gap this opens at the top of the art is always above the viewport. */
@supports (animation-timeline: scroll()) {
    .hero-art img { animation: hero-art-depth linear both; animation-timeline: scroll(root); animation-range: 0 100vh; }
    .js-motion .hero-art img {
        animation: hero-art-in 900ms cubic-bezier(0.33, 1, 0.68, 1) backwards, hero-art-settle 1100ms var(--ease-out) backwards, hero-art-depth linear both;
        animation-timeline: auto, auto, scroll(root); animation-range: normal, normal, 0 100vh;
    }
}
@keyframes hero-art-depth { to { translate: 0 12vh; } }

/* Light on the glass. Layers inside .hero-art, bottom to top: the picture, the mouse's light
   (.hero-light), the arrival pass (::before), the page-colour wash (::after). Both lights are
   white pools blended with soft-light, so they stay in the art's own greens, and both sit
   under the wash, so they show on the panes and fade out towards the headline.
   Arrival pass, every device: one soft pass of light down the staircase of panes while the
   art settles. Transform and opacity only, once. */
.hero-art::before {
    content: ''; position: absolute; z-index: 1; left: 0; top: 0; pointer-events: none;
    width: 64vmax; height: 44vmax; margin: -22vmax 0 0 -32vmax; border-radius: 50%;
    background: radial-gradient(closest-side, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.34) 45%, rgba(255, 255, 255, 0));
    mix-blend-mode: soft-light; opacity: 0;
}
.js-motion .hero-art::before { animation: hero-pass 1050ms var(--ease) 150ms backwards; }
@keyframes hero-pass {
    0% { opacity: 0; transform: translate(18vw, 2vh); }
    30%, 65% { opacity: 1; }
    100% { opacity: 0; transform: translate(96vw, 72vh); }
}
/* The mouse's light trails the pointer (site.js moves it with a transform and sets .is-lit
   while a mouse is over the hero); site.js also leans the picture a few pixels away from the
   pointer. Mouse devices only. */
.hero-light { display: none; }
@media (hover: hover) and (pointer: fine) {
    .hero-light {
        display: block; position: absolute; left: 0; top: 0; pointer-events: none;
        width: 40rem; height: 40rem; margin: -20rem 0 0 -20rem; border-radius: 50%;
        background: radial-gradient(closest-side, rgba(255, 255, 255, 0.85), rgba(255, 255, 255, 0.32) 42%, rgba(255, 255, 255, 0));
        mix-blend-mode: soft-light; opacity: 0;
        transition: opacity var(--dur-glide) var(--ease-out);
    }
    .hero.is-lit .hero-light { opacity: 1; will-change: transform; }
}

/* Primary action: the dark button catches the same light where the pointer is (site.js writes
   --lx/--ly) and gives a little under a press, then springs back; its arrow flies like every
   other (site.js, dsIcons). Keyboard focus gets the light too; touch gets the press. */
.hero-actions .btn-dark {
    position: relative; isolation: isolate; overflow: hidden;
    transition: background-color var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease), transform var(--dur-spring) var(--spring-snappy);
}
.hero-actions .btn-dark::before {
    content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
    background: radial-gradient(7rem circle at var(--lx, 50%) var(--ly, 50%), rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0) 70%);
    opacity: 0; transition: opacity var(--dur-slow) var(--ease-out);
}
.hero-actions .btn-dark:active { transform: scale(0.97); transition-duration: var(--dur-fast); }
.hero-actions .btn-dark:focus-visible::before { opacity: 1; }

/* "What we build": a spring underline draws in over the quiet rule, as in the header; its
   arrow flies (site.js, dsIcons). */
.hero-actions .btn-ghost { position: relative; display: inline-flex; align-items: center; gap: 0.5rem; }
.hero-actions .btn-ghost::after {
    content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px; pointer-events: none;
    background: var(--ink); transform: scaleX(0); transform-origin: left center;
    transition: transform var(--dur-spring) var(--spring);
}
.hero-actions .btn-ghost:hover { border-color: var(--ink-line); }
.hero-actions .btn-ghost:focus-visible { color: var(--ink); }
.hero-actions .btn-ghost:focus-visible::after, .hero-actions .btn-ghost:active::after { transform: scaleX(1); }
.ghost-icon { display: inline-flex; }
.ghost-icon svg { width: 0.75rem; height: 0.75rem; }
/* The actions' icons are sized before the icon library draws them, so nothing shifts. */
.hero-actions .arrow-icon { min-width: 0.875rem; min-height: 0.875rem; }
.ghost-icon { min-width: 0.75rem; min-height: 0.75rem; }

/* Scroll cue (wider screens): a small glass ring at the end of the strip. Its arrow drops out
   of the ring and back in a few times after the arrival, then rests; pointed at or focused,
   the ring lifts and the arrow flies down out of the ring and back in once (site.js, dsIcons;
   --fly-d is how far it travels before the fresh one comes in). As the page starts to move,
   the ring shrinks away (a scroll timeline). */
.hero-cue { grid-column: -2 / -1; justify-self: end; display: inline-flex; border-radius: 50%; }
.hero-cue:focus-visible { border-radius: 50%; }
.hero-cue-ring {
    display: grid; place-items: center; width: 2.75rem; height: 2.75rem; border-radius: 50%; overflow: hidden;
    color: var(--ink); background: var(--glass-sheen), var(--glass-fill-strong);
    border: 1px solid var(--glass-line); box-shadow: var(--glass-shadow-1);
    transition: box-shadow var(--dur-base) var(--ease), transform var(--dur-spring) var(--spring-snappy);
    --fly-d: 170%;
}
.hero-cue-ring svg { width: 0.95rem; height: 0.95rem; animation: hero-cue-idle 2600ms 1200ms 3; }
.hero-cue:focus-visible .hero-cue-ring { box-shadow: var(--glass-shadow-2); }
.hero-cue:active .hero-cue-ring { transform: scale(0.92); transition-duration: var(--dur-fast); }
@keyframes hero-cue-idle {
    0%, 70% { transform: none; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
    81% { transform: translateY(170%); }
    81.1% { transform: translateY(-170%); animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); }
    100% { transform: none; }
}
@supports (animation-timeline: scroll()) {
    .hero-cue-ring { animation: hero-cue-away linear both; animation-timeline: scroll(root); animation-range: 0 16vh; }
}
@keyframes hero-cue-away { to { opacity: 0; scale: 0.6; } }

/* Pointer devices only: hover mirrors keyboard focus. */
@media (hover: hover) and (pointer: fine) {
    .hero-actions .btn-dark:hover { opacity: 1; }
    .hero-actions .btn-dark:hover::before { opacity: 1; }
    .hero-actions .btn-ghost:hover::after { transform: scaleX(1); }
    .hero-cue:hover .hero-cue-ring { box-shadow: var(--glass-shadow-2); }
}

/* ---------- Product hero (home, 2026-10-19) ----------
   The headline beside the RAAD console (the mock from the showcase, now the first thing on
   the site) and a row of four figures in the strip where the scroll cue was. The glass art
   stays behind as texture. The body takes the hero's spare height and centres the pair; the
   strip keeps the foot. */
.hero.is-product .hero-body {
    flex: 1; display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); gap: 2rem 4rem; align-items: center;
    padding: calc(var(--head-h) + 3.5rem) var(--gutter) 2.5rem;
}
.hero.is-product .hero-title { max-width: 13ch; }
.hero-copy { min-width: 0; }
.hero-visual { position: relative; min-width: 0; }
/* The console's fit box carries 40px of right padding and 12px of bleed for its shadow (RAAD
   console mock), so it is sized to leave them room inside the column. */
.hero-visual .console-fit { width: calc(100% - 40px - var(--bleed)); }
.hero-strip.has-stats { grid-template-columns: minmax(0, 1fr) auto; }
.hero-stats { display: grid; grid-auto-flow: column; gap: 2.75rem; justify-self: end; }
.hero-stats span {
    display: flex; flex-direction: column; gap: 0.3rem;
    font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-dim); white-space: nowrap;
}
.hero-stats b { font-family: var(--sans); font-weight: 500; font-size: 1.5rem; letter-spacing: -0.02em; line-height: 1; color: var(--ink); }
/* Arrival: the console rises after the title, the figures after the strip's rule. */
.js-motion .hero-visual { animation: hero-rise 720ms var(--ease-out) calc(var(--tr-lead, 0ms) + 380ms) backwards; }
.js-motion .hero-stats > * { animation: hero-rise 560ms var(--ease-out) calc(var(--tr-lead, 0ms) + 600ms) backwards; }
.js-motion .hero-stats > :nth-child(2) { animation-delay: calc(var(--tr-lead, 0ms) + 660ms); }
.js-motion .hero-stats > :nth-child(3) { animation-delay: calc(var(--tr-lead, 0ms) + 720ms); }
.js-motion .hero-stats > :nth-child(4) { animation-delay: calc(var(--tr-lead, 0ms) + 780ms); }
.js-motion .hero:has(.hero-title.tr-split:not(.tr-play)) :is(.hero-visual, .hero-stats > *) { animation-play-state: paused; }
@media (max-width: 1100px) {
    .hero.is-product .hero-body { gap: 2rem 2.5rem; }
    .hero-stats { gap: 1.75rem; }
}
@media (max-width: 960px) {
    .hero.is-product .hero-body { grid-template-columns: 1fr; gap: 2.5rem; padding-top: calc(var(--head-h) + 2.5rem); }
    .hero.is-product .hero-title { max-width: none; }
    .hero-strip.has-stats { grid-template-columns: 1fr; }
    .hero-stats { justify-self: start; grid-auto-flow: row; grid-template-columns: repeat(4, auto); gap: 1.25rem 2rem; }
}
@media (max-width: 640px) {
    .hero-stats { grid-template-columns: 1fr 1fr; }
}

/* ---------- Page head (every other page) ----------
   No eyebrow above the title (owner's decision 2026-10-16). The head arrives in sequence,
   about 0.9s in all: the title's words, the lede's words (the shared text reveal, offsets
   below), then the actions. */
.page-head {
    max-width: calc(var(--content-w) + 2 * var(--gutter)); margin: 0 auto; padding: 3.25rem var(--gutter) 0;
    display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2rem 4rem; align-items: end;
}
.page-title { font-weight: 400; font-size: clamp(1.9rem, 3.3vw, 2.9rem); line-height: 1.08; letter-spacing: -0.035em; text-wrap: balance; max-width: 20ch; }
.page-lede { font-size: 1.15rem; line-height: 1.6; font-weight: 300; color: var(--ink-body); max-width: 36rem; margin-top: 1.25rem; }
.page-actions, .aside-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 1.25rem; }
.page-head .page-actions { padding-bottom: 0.35rem; }
/* The icons in the actions are sized before the icon library draws them (no shift). */
.page-head .arrow-icon { min-width: 0.875rem; min-height: 0.875rem; }

/* Arrival, in step with the text reveal. The title and lede are offset slightly, and
   long ledes keep a short total stagger (--tr-spread), so the head lands in about 0.9s. The
   actions follow the title: hidden while it waits for its first frame,
   animated once it plays (:has; without it they simply show). --tr-lead delays the lot while
   a page transition dissolves the old page (head.js adds .tr-lead). */
.page-head .page-title { --tr-delay: 70ms; }
.page-head .page-lede { --tr-delay: 160ms; --tr-spread: 140ms; }
/* Section headings below (build.py, reveal_headings) rise as they scroll in; one already on the
   first screen (.tr-first) waits for the head to land rather than racing its title. */
.page-head ~ .content-wrap .tr-first { --tr-delay: 420ms; }
.page-head:has(.page-title.tr-lead) { --tr-lead: 90ms; }
@supports selector(:has(*)) {
    @media (prefers-reduced-motion: no-preference) {
        .js-motion .page-head:has(.page-title:not(.tr-play, .tr-done)) .page-actions > * { opacity: 0; }
        .js-motion .page-head:has(.page-title:is(.tr-play, .tr-done)) .page-actions > * {
            animation: ph-rise var(--dur-spring) var(--ease-out) calc(var(--tr-lead, 0ms) + 300ms) backwards;
        }
        .js-motion .page-head:has(.page-title:is(.tr-play, .tr-done)) .page-actions > :nth-child(2) { animation-delay: calc(var(--tr-lead, 0ms) + 370ms); }
    }
}
@keyframes ph-rise { from { opacity: 0; transform: translateY(0.6rem); } }

/* ---------- Text reveal ([data-text-reveal]) ----------
   The one text-splitting utility (head.js runs it before the first paint). Put
   data-text-reveal on a text element and its words rise into view from under a mask, one after
   another in reading order: in the first frame for text on the first screen, on scroll into
   view further down. style="--tr-delay: 200ms" offsets the start; --tr-spread caps the total
   stagger, so a long paragraph ripples in rather than dragging. Only transform and opacity
   move, the masks add no layout (the padding that lets descenders through is taken back by
   negative margins), and the lines are pinned where the text broke them (.tr-lines). When the
   last word lands the element is restored to plain text. Until it plays, an element is held
   at opacity 0 (never visibility, so screen readers can still reach every heading). Hidden
   states exist only under html.js-motion and never under reduced motion; an element the
   script never picks up shows anyway after 2.5s. */
[data-text-reveal] { --tr-spread: 200ms; }
.tr-w { display: inline-block; overflow: clip; padding: 0.14em 0.08em 0.24em; margin: -0.14em -0.08em -0.24em; }
.tr-in { display: inline-block; }
.tr-lines { white-space: nowrap; }
.tr-sr {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
    clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; -webkit-user-select: none; user-select: none;
}
.tr-lead { --tr-lead: 90ms; }
@media (prefers-reduced-motion: no-preference) {
    .js-motion [data-text-reveal]:not(.tr-wait, .tr-play, .tr-done) { opacity: 0; animation: tr-failsafe 1ms 2.5s forwards; }
    .js-motion .tr-wait { opacity: 0; }
    .js-motion .tr-play .tr-in {
        animation: tr-rise var(--dur-text-reveal) var(--ease-out) backwards;
        animation-delay: calc(var(--tr-lead, 0ms) + var(--tr-delay, 0ms) + var(--tr-i, 0) * min(var(--stagger), var(--tr-spread) / var(--tr-n, 1)));
    }
}
@keyframes tr-rise {
    from { opacity: 0; transform: translateY(calc(100% + 0.35em)); }
    45% { opacity: 1; }
    to { opacity: 1; transform: none; }
}
@keyframes tr-failsafe { to { opacity: 1; } }
@media print {
    [data-text-reveal], .tr-in { opacity: 1 !important; transform: none !important; }
}

/* ---------- Page transitions ----------
   Moving between pages is a cross-document view transition (same-origin navigations in
   Chromium; from file:// and in other browsers pages simply load and the text reveal plays).
   The old page lifts away as it dissolves, over the new page, which settles the last 0.75rem
   into place beneath it. Three parts keep still, each for its own reason:
   - the header (site-head) stays put, so the bar never flickers between pages;
   - the arriving page head (page-head, named on the new page only, so it never pairs with the
     old one) does not settle: its words rise through the text reveal, held a beat by
     --tr-lead while the old page clears, and that is its only motion;
   - nothing else is named, so the rest of the page moves as one picture, cheaply.
   Names exist only during a transition (html.is-vt, is-vt-in; head.js): a view-transition-name
   makes an element a backdrop root, and at rest it would stop the header's glass blurring the
   page. Going back, or up from a page to its section, the motion runs the other way: the old
   page sinks and the new one settles down into place. Off under reduced motion. */
@media (prefers-reduced-motion: no-preference) {
    @view-transition { navigation: auto; }
}
.is-vt .site-head { view-transition-name: site-head; }
.is-vt-in .page-head { view-transition-name: page-head; }
::view-transition-old(root) { z-index: 1; mix-blend-mode: normal; animation: vt-leave 220ms var(--ease) both; }
::view-transition-new(root) { mix-blend-mode: normal; animation: vt-settle 560ms var(--ease-out) both; }
::view-transition-new(page-head) { animation: none; }
/* A named element is captured on its own, so the header's glass has nothing to blur in the
   snapshot; the group frosts the pages passing beneath it instead. Over a snapshot the bar's
   own blur reads lighter than it does live, hence the stronger --blur-snapshot. It is constant
   (never animated) and ends with the transition, when only the new page's plain top is
   behind it, so nothing pops. */
::view-transition-group(site-head) {
    animation-duration: 240ms; animation-timing-function: var(--ease);
    -webkit-backdrop-filter: var(--blur-snapshot); backdrop-filter: var(--blur-snapshot);
}
html:active-view-transition-type(back)::view-transition-old(root) { animation-name: vt-leave-back; }
html:active-view-transition-type(back)::view-transition-new(root) { animation-name: vt-settle-back; }
@keyframes vt-leave { to { opacity: 0; transform: translateY(-1rem); } }
@keyframes vt-leave-back { to { opacity: 0; transform: translateY(1rem); } }
@keyframes vt-settle { from { transform: translateY(0.75rem); } }
@keyframes vt-settle-back { from { transform: translateY(-0.75rem); } }

/* ---------- In-page jumps ----------
   Anchor links (What we build to #products, Back to top, the skip link) glide to their target
   when motion is welcome, and the target always lands clear of the sticky header: the scroll
   stops 1rem below the bar, whatever height the bar has at that width (--head-h). */
html { scroll-padding-top: calc(var(--head-h) + 1rem); }
@media (prefers-reduced-motion: no-preference) {
    html { scroll-behavior: smooth; }
}

.status-dot { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
.status-dot.pulse { animation: pulse-status 2.5s ease-in-out infinite; }
@keyframes pulse-status {
    0%, 100% { opacity: 1; box-shadow: 0 0 0 0 var(--accent-ring); }
    50% { opacity: 0.5; box-shadow: 0 0 0 4px transparent; }
}

/* ---------- Buttons & links ---------- */
.btn {
    display: inline-flex; align-items: center; gap: 0.5rem;
    font-family: var(--mono); font-size: 0.7rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.05em;
    padding: 0.875rem 1.5rem; border-radius: var(--r-sm);
    transition: background-color var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
    cursor: pointer; white-space: nowrap;
}
.btn-light { background: var(--on-dark); color: var(--ink); }
.btn-light:hover { background: var(--bg); }
.btn-dark { background: var(--ink); color: var(--on-dark); }
.btn-dark:hover { opacity: 0.9; }
.btn-outline { border: 1px solid var(--ink-faint); color: var(--ink); background: var(--surface); }
.btn-outline:hover { border-color: var(--ink); }
.btn-sm { padding: 0.625rem 1.25rem; font-weight: 500; letter-spacing: 0.1em; border-radius: var(--r-xs); }

.btn-ghost-light {
    border-bottom: 1px solid var(--on-dark-line); color: var(--on-dark-body);
    padding-bottom: 0.125rem;
    font-family: var(--mono); font-size: 0.7rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.1em;
    transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.btn-ghost-light:hover { color: var(--on-dark); border-color: var(--on-dark); }
/* The same quiet link on light surfaces (page heads, the hero). */
.btn-ghost {
    border-bottom: 1px solid var(--ink-line); color: var(--ink-dim);
    padding-bottom: 0.125rem;
    font-family: var(--mono); font-size: 0.7rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.1em;
    transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.btn-ghost:hover { color: var(--ink); border-color: var(--ink); }

.arrow-icon { display: inline-flex; }
/* Icon motion (site.js, dsIcons): an arrow's slot clips it as it flies out and a fresh one
   comes in. site.js marks the slot .fly (its .arrow-icon, .ext-arrow or .ghost-icon, or a
   .fly-slot it wraps round a bare arrow). clip, not hidden, so the slot keeps its baseline. */
.fly { overflow: clip; }
.fly-slot { display: inline-flex; }
.arrow-icon svg, .icon-sm { width: 0.875rem; height: 0.875rem; }
.icon-xs, .arrow-icon svg.icon-xs { width: 0.75rem; height: 0.75rem; }

.link-mono {
    display: inline-flex; align-items: center; gap: 0.5rem;
    font-family: var(--mono); font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: 0.05em;
    color: var(--ink);
}
.link-underline {
    display: inline-flex; align-items: center; gap: 0.375rem;
    font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.05em; color: var(--ink);
    border-bottom: 1px solid var(--ink-line); padding-bottom: 0.125rem;
    transition: border-color var(--dur-fast) var(--ease);
}
.link-underline:hover { border-color: var(--ink); }
.section-more { display: inline-flex; margin-top: 1.5rem; }
.link-inline { color: var(--ink); border-bottom: 1px solid var(--ink-line); transition: border-color var(--dur-fast) var(--ease); }
.link-inline:hover { border-color: var(--ink); }

/* ---------- Site header: brand, primary navigation, call to action ---------- */
.glass-nav {
    position: sticky; top: 0; z-index: 60;
    border-bottom: 1px solid var(--glass-divider);
}
.site-head {
    display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 2rem;
    min-height: var(--head-h); padding: 0.85rem var(--gutter);
}
.site-head .brand { color: var(--ink); }
.site-head .nav-list { justify-self: center; }
.head-actions { display: flex; align-items: center; gap: 1rem; }
/* One glass layer, two shapes. On load it is the pill behind the centre links (site.js
   measures the pill and writes --pill-l/-t/-w/-h). Once the page scrolls (.is-scrolled) the
   same layer stretches to the full bar, corners squaring off as it goes: a layout morph in
   the Framer Motion sense, driven by CSS springs. The layer is a pseudo-element so the glass
   dropdowns inside the bar still blur the page underneath. */
.glass-nav { border-bottom: 0; }
.glass-nav::before {
    content: ''; position: absolute; z-index: -1; pointer-events: none;
    left: var(--pill-l, 50%); top: var(--pill-t, 0.85rem); width: var(--pill-w, 0px); height: var(--pill-h, 2.8rem);
    border-radius: 1.6rem;
    background: var(--glass-sheen), var(--glass-fill);
    -webkit-backdrop-filter: var(--blur-md); backdrop-filter: var(--blur-md);
    border: 1px solid var(--glass-line); box-shadow: var(--glass-shadow-1);
    transition:
        left var(--dur-morph) var(--spring), width var(--dur-morph) var(--spring),
        top var(--dur-morph) var(--spring), height var(--dur-morph) var(--spring),
        border-radius var(--dur-morph) var(--ease-out), border-color var(--dur-slow) var(--ease),
        box-shadow var(--dur-slow) var(--ease);
}
.glass-nav.is-scrolled::before, .glass-nav.is-menu-open::before {
    left: 0; top: 0; width: 100%; height: 100%; border-radius: 0;
    border-color: transparent; box-shadow: inset 0 -1px 0 0 var(--glass-divider);
    /* Out to the bar, the side edges land on --ease-expand: a spring would carry them past
       the viewport for a moment (a passing horizontal scrollbar), and off-screen it shows
       nothing anyway. The spring stays on the vertical, which overshoots harmlessly, and on
       the way back to the pill, which overshoots inwards. */
    transition:
        left var(--dur-morph) var(--ease-expand), width var(--dur-morph) var(--ease-expand),
        top var(--dur-morph) var(--spring), height var(--dur-morph) var(--spring),
        border-radius var(--dur-morph) var(--ease-out), border-color var(--dur-slow) var(--ease),
        box-shadow var(--dur-slow) var(--ease);
}
/* The first measurement lands without a morph (site.js holds .no-morph for one style pass),
   so the layer never grows out of the centre as a page loads. Until then, and wherever
   site.js does not run, the nav draws the same pill itself: the header is right from the
   first frame and stays still from page to page. */
.glass-nav.no-morph::before { transition: none; }
.nav-pill::before {
    content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
    border-radius: 1.6rem;
    background: var(--glass-sheen), var(--glass-fill);
    -webkit-backdrop-filter: var(--blur-md); backdrop-filter: var(--blur-md);
    border: 1px solid var(--glass-line); box-shadow: var(--glass-shadow-1);
}
.glass-nav.is-measured .nav-pill::before { display: none; }
@media (min-width: 961px) {
    .glass-nav:not(.is-measured)::before { visibility: hidden; }
}
/* First page of a visit only (html.head-intro: the inline script at the top of the header
   sets it, site.js clears it): the layer is painted as the full bar, then gathers into the
   pill on the scroll morph's own spring. Later pages keep the header still. */
@media (min-width: 961px) and (prefers-reduced-motion: no-preference) {
    html.head-intro .glass-nav::before {
        visibility: visible; left: 0; top: 0; width: 100%; height: 100%; border-radius: 0;
        border-color: transparent; box-shadow: inset 0 -1px 0 0 var(--glass-divider);
    }
    html.head-intro .nav-pill::before { display: none; }
}
.nav-pill {
    position: relative; justify-self: center;
    display: flex; align-items: center;
    padding: 0.5rem 1.6rem;
}
.nav-list { display: flex; align-items: center; gap: 2.25rem; }
.nav-link {
    display: inline-flex; align-items: center; gap: 0.35rem;
    font-family: var(--mono); font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--ink-dim); transition: color var(--dur-fast) var(--ease); padding: 0.25rem 0;
    position: relative;
}
.nav-link::after {
    content: ''; position: absolute; left: 0; right: 0; bottom: -0.2rem; height: 1px;
    background: var(--ink); transform: scaleX(0); transform-origin: left center;
    transition: transform var(--dur-spring) var(--spring);
}
/* Keyboard focus draws the same spring underline as the pointer does. */
.nav-link:hover::after, .nav-link:focus-visible::after, .nav-link[aria-expanded="true"]::after,
.nav-link[aria-current="page"]::after, .nav-item.is-active > .nav-link::after { transform: scaleX(1); }
.nav-link:hover, .nav-link:focus-visible, .nav-link[aria-expanded="true"] { color: var(--ink); }
.nav-link[aria-current="page"], .nav-item.is-active > .nav-link { color: var(--ink); }
/* The chevron morphs into chevron-up while its menu is open (site.js, dsIcons.toggle, which
   marks it .is-morph). Without morphicons it folds over like a hinge instead. */
.nav-link .chev { width: 0.7rem; height: 0.7rem; transition: transform var(--dur-spring) var(--spring-snappy); }
.nav-link[aria-expanded="true"] .chev:not(.is-morph) { transform: scaleY(-1); }

.nav-item { position: relative; }
.nav-menu {
    position: absolute; top: calc(100% + 0.9rem); left: -1.25rem; min-width: 17rem;
    background: var(--glass-sheen), var(--glass-fill-strong);
    -webkit-backdrop-filter: var(--blur-lg); backdrop-filter: var(--blur-lg);
    border: 1px solid var(--glass-line); border-radius: var(--r-glass-sm);
    box-shadow: var(--glass-shadow-3);
    padding: 0.5rem; display: block;
    max-height: calc(100vh - 6rem); overflow-y: auto;
    /* Closed menus are invisible and out of the tab order. They spring open from the toggle. */
    visibility: hidden; opacity: 0; transform: translateY(-8px) scale(0.96); transform-origin: 1.75rem 0; pointer-events: none;
    transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out), visibility 0s linear var(--dur-fast);
}
.nav-menu.is-open {
    visibility: visible; opacity: 1; transform: none; pointer-events: auto;
    transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-spring) var(--spring), visibility 0s;
}
/* Items rise in one after another (site.js sets --i on each). */
.nav-menu a {
    position: relative; z-index: 1;
    display: flex; flex-direction: column; gap: 0.15rem;
    padding: 0.7rem 0.85rem; border-radius: var(--r-md);
    opacity: 0; transform: translateY(6px);
    transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out), background-color var(--dur-fast) var(--ease);
}
.nav-menu.is-open a {
    opacity: 1; transform: none;
    transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-spring) var(--spring), background-color var(--dur-fast) var(--ease);
    transition-delay: calc(40ms + var(--i, 0) * var(--stagger)), calc(40ms + var(--i, 0) * var(--stagger)), 0s;
}
.nav-menu a:hover, .nav-menu a:focus-visible { background: var(--ink-wash); box-shadow: inset 0 1px 0 0 var(--glass-edge); }
.nav-menu a[aria-current="page"] { background: var(--ink-faint); }
/* The glass highlight that glides between items (Framer Motion's shared-layout effect). */
.nav-menu-pill {
    position: absolute; top: 0; left: 0; z-index: 0; pointer-events: none;
    width: var(--w, 0); height: var(--h, 0); transform: translate(var(--x, 0), var(--y, 0));
    border-radius: var(--r-md); background: var(--ink-wash); box-shadow: var(--glass-shadow-1);
    opacity: 0;
    transition: transform var(--dur-glide) var(--spring), width var(--dur-glide) var(--spring), height var(--dur-glide) var(--spring), opacity var(--dur-fast) var(--ease);
}
.nav-menu-pill.is-snap { transition: opacity var(--dur-fast) var(--ease); }
.nav-menu.has-pill.pill-on .nav-menu-pill { opacity: 1; }
.nav-menu.has-pill a:hover, .nav-menu.has-pill a:focus-visible { background: transparent; box-shadow: none; }
.nav-menu-title { display: flex; align-items: center; gap: 0.375rem; font-size: 0.85rem; font-weight: 500; color: var(--ink); }
.nav-menu-desc { font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink-dim); transition: color var(--dur-fast) var(--ease); }
/* An item's arrow shows while it is pointed at or focused. It flies like every other arrow
   (dsIcons); the fade waits for the fresh one, so the arrow is seen coming in. */
.nav-menu-go { width: 0.75rem; height: 0.75rem; opacity: 0; transition: opacity var(--dur-fast) var(--ease); }
.nav-menu a:hover .nav-menu-go, .nav-menu a:focus-visible .nav-menu-go { opacity: 1; transition-delay: calc(var(--dur-spring) * 0.38); }
.nav-menu a:hover .nav-menu-desc, .nav-menu a:focus-visible .nav-menu-desc { color: var(--ink-body); }
.nav-menu hr { border: 0; border-top: 1px solid var(--ink-faint); margin: 0.4rem 0.5rem; }
/* The shared stage (site.js adds .nav-stage and marks the nav .has-stage): one glass panel
   behind whichever menu is open. It springs open from the toggle with its menu; when the
   pointer or focus moves on to the next menu it glides and reshapes to it while the links
   cross-fade in place (.is-switching), instead of one panel closing as another opens.
   Without it, each menu keeps its own glass. */
.nav-stage {
    position: absolute; top: 0; left: 0; z-index: 0; pointer-events: none;
    width: var(--sw, 17rem); height: var(--sh, 0px);
    border-radius: var(--r-glass-sm);
    background: var(--glass-sheen), var(--glass-fill-strong);
    -webkit-backdrop-filter: var(--blur-lg); backdrop-filter: var(--blur-lg);
    border: 1px solid var(--glass-line); box-shadow: var(--glass-shadow-3);
    visibility: hidden; opacity: 0;
    transform: translate(var(--sx, 0px), var(--sy, 0px)) translateY(-8px) scale(0.96); transform-origin: 1.75rem 0;
    transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out), visibility 0s linear var(--dur-fast);
}
.nav-stage.is-open {
    visibility: visible; opacity: 1; transform: translate(var(--sx, 0px), var(--sy, 0px));
    transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-spring) var(--spring),
        width var(--dur-spring) var(--spring), height var(--dur-spring) var(--spring), visibility 0s;
}
.nav-stage.is-snap { transition: none; }
.nav-pill.has-stage .nav-menu {
    background: none; -webkit-backdrop-filter: none; backdrop-filter: none;
    border-color: transparent; box-shadow: none;
}
.nav-pill.is-switching .nav-menu { transform: none; transition: opacity var(--dur-fast) var(--ease-out), visibility 0s linear var(--dur-fast); }
.nav-pill.is-switching .nav-menu.is-open { transition: opacity var(--dur-base) var(--ease-out), visibility 0s; }

/* The call to action, the same dark button as the hero's: a press sinks it a touch and the
   release springs back. While it is pointed at or focused, the envelope folds into a paper
   plane (site.js, dsIcons). */
.head-actions .btn {
    transition: background-color var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease), transform var(--dur-spring) var(--spring-snappy);
}
.head-actions .btn:active { transform: scale(0.97); transition: transform var(--dur-fast) var(--ease-out); }

/* ---------- Mobile navigation ---------- */
/* Below 960px the header keeps the brand and swaps the link list for this toggle. */
/* A small glass pill, the same material as the desktop link pill, tall enough to tap (44px). */
.menu-toggle {
    display: none; align-items: center; justify-content: center; gap: 0.5rem;
    min-height: 2.75rem; padding: 0 0.9rem 0 1.1rem;
    font-family: var(--mono); font-size: 0.7rem; letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--ink); border: 1px solid var(--glass-line); border-radius: 999px;
    background: var(--glass-sheen), var(--glass-fill-strong); box-shadow: var(--glass-shadow-1);
}
.menu-toggle [data-label] { min-width: 3.5em; text-align: right; } /* "Menu" and "Close" without a width jump: five mono characters with their tracking */
.menu-toggle .ico-close { display: none; }
.menu-toggle[aria-expanded="true"] .ico-menu { display: none; }
.menu-toggle[aria-expanded="true"] .ico-close { display: block; }
/* With site.js the two icons become one that morphs between menu and x (dsMorph), the label
   rolls up as it changes, and a tap presses the pill in. */
.menu-toggle { transition: transform var(--dur-spring) var(--spring-snappy); }
.menu-toggle:active { transform: scale(0.96); transition: transform var(--dur-fast) var(--ease-out); }
/* Two names for one roll, so each change restarts it without forcing a reflow. */
.menu-toggle [data-label].roll-a { animation: ds-label-roll-a var(--dur-slow) var(--ease-out); }
.menu-toggle [data-label].roll-b { animation: ds-label-roll-b var(--dur-slow) var(--ease-out); }
@keyframes ds-label-roll-a { from { opacity: 0; transform: translateY(0.6em); } }
@keyframes ds-label-roll-b { from { opacity: 0; transform: translateY(0.6em); } }
.mobile-panel {
    display: flex; flex-direction: column; position: fixed; inset: 0; z-index: 55; overflow-y: auto;
    background: var(--glass-sheen), var(--glass-fill-strong);
    -webkit-backdrop-filter: var(--blur-lg); backdrop-filter: var(--blur-lg);
    padding: calc(var(--head-h) + 0.75rem) var(--gutter) max(1.5rem, env(safe-area-inset-bottom));
    visibility: hidden; opacity: 0; pointer-events: none;
    transition: opacity var(--dur-fast) var(--ease-out), visibility 0s linear var(--dur-fast);
}
.mobile-panel.is-open { visibility: visible; opacity: 1; pointer-events: auto; transition-delay: 0s; }
/* Four groups on a two-column grid: Products and Work share the first row, Services runs
   full width, Company lists its links in two columns. Rows are 44px tap targets. */
.mobile-panel nav {
    display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas: "products work" "services services" "company company";
}
.mobile-panel .group { border-top: 1px solid var(--ink-faint); padding: 1.1rem 0 1rem; min-width: 0; }
.mobile-panel .group.is-products { grid-area: products; border-top: 0; padding-top: 0.5rem; padding-right: 1rem; }
.mobile-panel .group.is-work { grid-area: work; border-top: 0; padding-top: 0.5rem; padding-left: 1.25rem; border-left: 1px solid var(--ink-faint); }
.mobile-panel .group.is-services { grid-area: services; }
.mobile-panel .group.is-company { grid-area: company; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); column-gap: 1rem; }
.mobile-panel .group.is-company .group-label { grid-column: 1 / -1; }
.mobile-panel .group-label {
    font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--ink-dim); margin-bottom: 0.35rem;
}
.mobile-panel .group a { display: flex; align-items: center; min-height: 2.75rem; padding: 0.35rem 0; font-size: 1.05rem; font-weight: 500; line-height: 1.25; letter-spacing: -0.01em; }
.mobile-panel .group a[aria-current="page"] { text-decoration: underline; text-underline-offset: 4px; }
/* The call to action and the phone number sit at the foot of the panel. */
.mobile-cta { margin-top: auto; padding-top: 1.5rem; border-top: 1px solid var(--ink-faint); display: flex; flex-direction: column; align-items: flex-start; gap: 1.25rem; }
.mobile-cta .btn { width: 100%; justify-content: center; min-height: 3rem; }
.mobile-cta .btn-ghost { font-size: 0.72rem; }
/* The bloom (site.js adds .has-bloom and --bloom, the menu pill's box as an inset of the
   panel): the panel grows out of the pill in the same glass and folds back into it, and its
   groups follow one after another, top to bottom. */
.mobile-panel.has-bloom {
    opacity: 1; clip-path: var(--bloom, inset(0 0 100% 0));
    transition: clip-path var(--dur-slow) var(--ease), visibility 0s linear var(--dur-slow);
}
.mobile-panel.has-bloom.is-open {
    clip-path: inset(0 round 0);
    transition: clip-path var(--dur-spring) var(--ease-expand), visibility 0s;
}
.mobile-panel.has-bloom nav > .group, .mobile-panel.has-bloom .mobile-cta {
    opacity: 0; transform: translateY(10px);
    transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
}
.mobile-panel.has-bloom.is-open nav > .group, .mobile-panel.has-bloom.is-open .mobile-cta {
    opacity: 1; transform: none;
    transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-spring) var(--spring);
    transition-delay: calc(90ms + var(--i, 0) * 45ms);
}
.mobile-panel .group.is-work { --i: 1; }
.mobile-panel .group.is-services { --i: 2; }
.mobile-panel .group.is-company { --i: 3; }
.mobile-panel .mobile-cta { --i: 4; }
/* Touch feedback: a tapped row gets the desktop menus' soft wash under the finger, and the
   call to action gives a little under a press. */
.mobile-panel .group a { position: relative; }
.mobile-panel .group a::before {
    content: ''; position: absolute; inset: 0.15rem -0.6rem; z-index: -1; pointer-events: none;
    border-radius: var(--r-md); background: var(--ink-wash); box-shadow: inset 0 1px 0 0 var(--glass-edge);
    opacity: 0; transition: opacity var(--dur-slow) var(--ease-out);
}
.mobile-panel .group a:active::before { opacity: 1; transition-duration: var(--dur-fast); }
/* These presses replace the browser's own tap flash (a blue outside the palette). */
.site-head a, .site-head button, .mobile-panel a { -webkit-tap-highlight-color: transparent; }
.mobile-cta .btn { transition: background-color var(--dur-fast) var(--ease), transform var(--dur-spring) var(--spring-snappy); }
.mobile-cta .btn:active { transform: scale(0.97); transition: transform var(--dur-fast) var(--ease-out); }

/* ---------- Glass material ----------
   .glass is the material on its own; the components below apply the same recipe at a level.
   Level 1: .glass.is-l1   Level 2: .glass   Level 3: .glass.is-l3   Dark: .glass.is-dark */
.glass {
    background: var(--glass-sheen), var(--glass-fill);
    -webkit-backdrop-filter: var(--blur-md); backdrop-filter: var(--blur-md);
    border: 1px solid var(--glass-line); border-radius: var(--r-glass-lg);
    box-shadow: var(--glass-shadow-2);
}
.glass.is-l1 { background: var(--glass-fill-subtle); -webkit-backdrop-filter: var(--blur-sm); backdrop-filter: var(--blur-sm); border-radius: var(--r-glass-sm); box-shadow: var(--glass-shadow-1); }
.glass.is-l3 { background: var(--glass-sheen), var(--glass-fill-strong); -webkit-backdrop-filter: var(--blur-lg); backdrop-filter: var(--blur-lg); box-shadow: var(--glass-shadow-3); }
.glass.is-dark { background: var(--glass-dark-sheen), var(--glass-dark-fill); border-color: var(--on-dark-faint); box-shadow: var(--glass-shadow-dark); color: var(--on-dark); }

/* ---------- Section primitives ---------- */
.section-label {
    display: block; font-family: var(--mono); font-size: 0.68rem;
    letter-spacing: 0.2em; text-transform: uppercase; font-weight: 400; color: var(--ink-dim); margin-bottom: 2rem;
}
.section-label.is-centred { text-align: center; }
/* A plain centred line where an eyebrow would be too much. */
.section-note { text-align: center; font-size: 0.9rem; color: var(--ink-dim); margin-bottom: 2rem; }
.section-head .section-title { margin-bottom: 0; }
/* Section opener in the wide column: headline left, supporting line and link right. */
.section-open { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 22rem); gap: 1.5rem 4rem; align-items: end; margin-bottom: 3rem; }
.section-open .section-title { font-weight: 400; font-size: clamp(1.6rem, 2.5vw, 2.25rem); letter-spacing: -0.035em; margin-bottom: 0; max-width: 20ch; }
.section-open .section-label { margin-bottom: 1rem; }
.section-open-aside { display: flex; flex-direction: column; align-items: flex-start; gap: 1rem; }
.section-open-aside p { font-size: 0.88rem; line-height: 1.6; color: var(--ink-dim); }
.section-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; }
.section-head .section-label { margin-bottom: 0; }
.section-title { font-weight: 600; font-size: 1.6rem; line-height: 1.2; letter-spacing: -0.035em; margin-bottom: 1rem; text-wrap: balance; }
.section-intro { font-size: 0.9rem; line-height: 1.625; color: var(--ink-dim); max-width: 32rem; margin-bottom: 2rem; }
/* Openers on arrival (Scroll reveal): the supporting line and link follow the title's words. */
.section-open-aside[data-reveal], .section-head > [data-reveal] { --reveal-delay: 180ms; }
.section-intro[data-reveal] { --reveal-delay: 120ms; }

.prose { max-width: 42rem; font-size: 1.05rem; line-height: 1.7; color: var(--ink-body); }
.prose > * + * { margin-top: 1.5rem; }
.prose h2 { font-weight: 600; font-size: 1.35rem; line-height: 1.3; letter-spacing: -0.03em; color: var(--ink); margin-top: 3rem; }
.prose h3 { font-weight: 600; font-size: 1.1rem; color: var(--ink); margin-top: 2.25rem; }
.prose ul { display: flex; flex-direction: column; gap: 0.65rem; }
.prose ul li { position: relative; padding-left: 1.25rem; }
.prose ul li::before { content: ''; position: absolute; left: 0; top: 0.72em; width: 5px; height: 5px; background: var(--accent); }
.prose.compact { font-size: 0.95rem; }
.prose.is-lead > p:first-child { font-size: 1.25rem; line-height: 1.6; color: var(--ink); letter-spacing: -0.01em; }

.lead-strong { color: var(--ink); font-weight: 600; }

/* ---------- Scroll reveal (shared by every section) ----------
   The one block-level reveal on the site (text has its own: [data-text-reveal]; never put
   both on one element). site.js adds .is-in once the element scrolls into view, or at once
   if it is already on screen:
     [data-reveal]            fades and rises into place
     [data-reveal="stagger"]  does that for each child in turn (site.js numbers them --i)
     [data-reveal="wipe"]     unveils left to right (the product lockups)
   Offset one with --reveal-delay, change the rise with --reveal-y. The hidden state exists
   only under html.js-motion and full motion, so without JS, with reduced motion and in print
   everything is simply there; if site.js never runs, a failsafe shows it all after 3 s. The
   reveal is an animation of opacity and the translate property, never transform or
   transition, so a component's own hover, transform and transitions are untouched (an
   element with its own animation needs a wrapper to carry data-reveal). It waits for
   site.js, so it is for content below the first screen; the first screen enters in CSS. */
@media (prefers-reduced-motion: no-preference) {
    html.js-motion [data-reveal]:not(.is-in):not([data-reveal="stagger"]),
    html.js-motion [data-reveal="stagger"]:not(.is-in) > * { opacity: 0; }
    html.js-motion [data-reveal].is-in:not([data-reveal="stagger"]),
    html.js-motion [data-reveal="stagger"].is-in > * {
        animation: ds-reveal var(--dur-reveal) var(--ease-out) var(--reveal-delay, 0ms) backwards;
    }
    html.js-motion [data-reveal="stagger"].is-in > * { animation-delay: calc(var(--reveal-delay, 0ms) + var(--i, 0) * var(--stagger-reveal)); }
    html.js-motion [data-reveal="wipe"].is-in:not([data-reveal="stagger"]) { animation-name: ds-reveal-wipe; animation-duration: 1100ms; }
    /* site.js arrived after the failsafe: show everything as it is, without replaying it
       (the reveals here and the arrivals the body components hang on them). */
    html.reveal-late [data-reveal], html.reveal-late [data-reveal="stagger"] > *,
    html.reveal-late .handoff::before, html.reveal-late .handoff::after, html.reveal-late .step-progress span::after,
    html.reveal-late .case-media img, html.reveal-late .case-masonry::before, html.reveal-late .pull-quote::before,
    html.reveal-late .cta-band::after { animation: none !important; }
    html.js-motion:not(.reveal-ready) [data-reveal]:not([data-reveal="stagger"]),
    html.js-motion:not(.reveal-ready) [data-reveal="stagger"] > * { animation: ds-reveal-failsafe 1ms 3s forwards; }
}
@keyframes ds-reveal { from { opacity: 0; translate: 0 var(--reveal-y, 1.1rem); } }
@keyframes ds-reveal-wipe {
    from { opacity: 0; clip-path: inset(-0.5rem 100% -0.5rem -0.5rem); filter: blur(3px); translate: -0.4rem 0; }
    to { clip-path: inset(-0.5rem -0.5rem -0.5rem -0.5rem); }
}
@keyframes ds-reveal-failsafe { to { opacity: 1; } }
@media print {
    html.js-motion [data-reveal]:not([data-reveal="stagger"]),
    html.js-motion [data-reveal="stagger"] > * { opacity: 1 !important; animation: none !important; }
}

/* ---------- Audience split (two-column list) ---------- */
.audience-split {
    display: grid; grid-template-columns: 1fr auto 1fr;
    border: 1px solid var(--glass-line); border-radius: var(--r-glass-sm); overflow: hidden; background: var(--glass-fill); -webkit-backdrop-filter: var(--blur-sm); backdrop-filter: var(--blur-sm); box-shadow: var(--glass-shadow-1);
}
.audience-col { padding: 1.75rem; }
.audience-divider { width: 1px; background: var(--ink-faint); align-self: stretch; }
.audience-label {
    font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--ink-dim); margin-bottom: 1rem;
}
.audience-list { display: flex; flex-direction: column; gap: 0.65rem; }
.audience-list li { display: flex; align-items: center; gap: 0.65rem; font-size: 0.85rem; color: var(--ink); }
.audience-list li::before { content: ''; width: 5px; height: 5px; background: var(--accent); flex-shrink: 0; }
.audience-list a:hover { opacity: 0.6; }

/* ---------- Partner strip ---------- */
.partner-strip {
    border-top: 1px solid var(--ink-faint); padding-top: 2rem;
    display: flex; align-items: baseline; gap: 1rem 3rem; flex-wrap: wrap;
}
.partner-strip-label {
    font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--ink-dim); flex-shrink: 0;
}
.partner-strip.is-flush { border-top: 0; padding-top: 0; }
/* Partner cards: a logo band above the role tag (connectivity / global IoT SIM partners). */
.partner-card .partner-logo { height: 2.1rem; margin-bottom: 1rem; display: flex; align-items: center; }
.partner-card .partner-logo img { height: 100%; width: auto; max-width: 75%; object-fit: contain; }
/* Convergence Group's artwork is a thin wordmark on a tall canvas: crop to the band, as .logo-cell does. */
.partner-card .partner-logo img[src*="client-cg"] { height: auto; width: 58%; aspect-ratio: 200 / 30; object-fit: cover; }
.partner-card .partner-logo.is-ciq img { height: 1.7rem; }
.partner-strip-list {
    display: flex; flex-wrap: wrap; gap: 0.75rem 2rem;
    font-family: var(--mono); font-size: 0.75rem; font-weight: 600; text-transform: uppercase; color: var(--ink); opacity: 0.6;
}

/* ---------- Tech access strip (key / value cells) ---------- */
.tech-access-strip {
    display: grid; grid-template-columns: repeat(3, 1fr);
    border: 1px solid var(--glass-line); border-radius: var(--r-glass-sm); overflow: hidden; background: var(--glass-fill); -webkit-backdrop-filter: var(--blur-sm); backdrop-filter: var(--blur-sm); box-shadow: var(--glass-shadow-1);
}
.tech-access-strip.cols-2 { grid-template-columns: repeat(2, 1fr); }
.tech-access-strip.cols-4 { grid-template-columns: repeat(4, 1fr); }
.tech-access-item {
    padding: 1.25rem; display: flex; flex-direction: column; gap: 0.35rem;
    border-right: 1px solid var(--ink-faint); border-bottom: 1px solid var(--ink-faint); margin-bottom: -1px;
}
.tech-access-item:last-child { border-right: none; }
.tech-access-strip:not(.cols-2):not(.cols-4) .tech-access-item:nth-child(3n) { border-right: none; }
.tech-access-strip.cols-2 .tech-access-item:nth-child(2n) { border-right: none; }
.tech-access-strip.cols-4 .tech-access-item:nth-child(4n) { border-right: none; }
a.tech-access-item { transition: background-color var(--dur-fast) var(--ease); }
a.tech-access-item:hover { background: var(--ink-wash); }
.tech-access-key {
    font-family: var(--mono); font-size: var(--fs-micro); font-weight: 600; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--ink);
}
.tech-access-val { font-family: var(--mono); font-size: var(--fs-micro); line-height: 1.5; color: var(--ink-dim); }
.tech-access-big { font-weight: 600; font-size: 1.9rem; letter-spacing: -0.04em; line-height: 1; color: var(--ink); }
.tech-access-big + .tech-access-val { font-size: var(--fs-caption); color: var(--ink-body); }

/* ---------- Cards ---------- */
.card-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
.card-grid.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.service-card {
    position: relative; display: flex; flex-direction: column;
    border: 1px solid var(--glass-line); border-radius: var(--r-glass-lg);
    background: var(--glass-sheen), var(--glass-fill);
    -webkit-backdrop-filter: var(--blur-md); backdrop-filter: var(--blur-md);
    box-shadow: var(--glass-shadow-2);
    padding: 2rem; overflow: hidden;
    transition: transform var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
.service-card:hover { transform: translateY(-2px); box-shadow: var(--glass-shadow-3); }
.service-card .icon-tile {
    width: 2.5rem; height: 2.5rem; border-radius: var(--r-lg); background: var(--glass-fill-subtle);
    border: 1px solid var(--glass-line); box-shadow: var(--glass-shadow-1);
    display: flex; align-items: center; justify-content: center; margin-bottom: 1.5rem; color: var(--ink);
}
.service-card .icon-tile svg { width: 1.25rem; height: 1.25rem; }
.service-card h3 { font-weight: 600; font-size: 1.1rem; margin-bottom: 0.75rem; letter-spacing: -0.015em; }
.service-card p { font-size: 0.85rem; line-height: 1.625; color: var(--ink-dim); margin-bottom: 1.5rem; }
.service-card .card-tag {
    font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--ink-dim); margin-bottom: 0.6rem;
}
.service-card .link-mono { margin-top: auto; }
.service-card .stretched::after { content: ''; position: absolute; inset: 0; }
.service-card.is-compact {
    padding: 1.5rem; border-radius: var(--r-glass-sm);
    background: var(--glass-fill-subtle); -webkit-backdrop-filter: var(--blur-sm); backdrop-filter: var(--blur-sm);
    box-shadow: var(--glass-shadow-1);
}
.service-card.is-compact:hover { box-shadow: var(--glass-shadow-2); }
/* Feature card: the large size. Spans the full row of a 2-column grid, or two of three columns. */
.service-card.is-feature {
    grid-column: span 2; padding: 2.75rem;
    background: var(--glass-sheen), var(--glass-fill-strong);
    -webkit-backdrop-filter: var(--blur-lg); backdrop-filter: var(--blur-lg);
    box-shadow: var(--glass-shadow-3);
}
.service-card.is-feature .icon-tile { width: 3rem; height: 3rem; border-radius: var(--r-glass-sm); }
.service-card.is-feature h3 { font-size: 1.5rem; letter-spacing: -0.03em; }
.service-card.is-feature p { font-size: 0.95rem; max-width: 34rem; color: var(--ink-body); }
.service-card.is-feature.case-card { padding: 0; }
.service-card.is-feature.case-card .case-media { aspect-ratio: 21 / 9; }
.service-card.is-feature.case-card .case-body { padding: 2.25rem 2.75rem 2.75rem; }
.service-card.is-compact p { margin-bottom: 0; }
.card-grid.is-list {
    grid-template-columns: 1fr; gap: 0;
    border: 1px solid var(--glass-line); border-radius: var(--r-glass-lg); overflow: hidden;
    background: var(--glass-sheen), var(--glass-fill);
    -webkit-backdrop-filter: var(--blur-md); backdrop-filter: var(--blur-md); box-shadow: var(--glass-shadow-2);
}
.card-grid.is-list .service-card {
    flex-direction: row; gap: 1.25rem; padding: 1.5rem;
    border: 0; border-radius: 0; border-bottom: 1px solid var(--glass-divider);
    background: none; -webkit-backdrop-filter: none; backdrop-filter: none; box-shadow: none;
    transition: background var(--dur-fast);
}
.card-grid.is-list .service-card:last-child { border-bottom: 0; }
.card-grid.is-list .icon-tile { margin-bottom: 0; flex-shrink: 0; }
.card-grid.is-list .service-card:hover { transform: none; box-shadow: none; background: var(--ink-wash); }

/* Team card. The portrait sits in a glass panel over a faint dot field, the same dots the
   halftone portraits are drawn in, so the picture and its ground read as one surface. A lone
   member gets the wide card (.is-solo): panel left, name right. Neither the card nor the
   portrait moves on hover (no lift, no zoom): site.js redraws the portrait's dots on a canvas
   (.team-dots) so they ease away from a mouse or pen and spring back. The img stays
   underneath, only made transparent (.is-live) once the canvas has drawn, so no-JS, file://
   and reduced motion keep the static picture. */
.team-card { gap: 0; }
.team-card.service-card:hover { transform: none; box-shadow: var(--glass-shadow-2); }
.team-portrait {
    position: relative; overflow: hidden; aspect-ratio: 1; margin-bottom: 1.5rem; border-radius: var(--r-md);
    background:
        radial-gradient(circle, rgba(12, 61, 59, 0.13) 1px, transparent 1.4px) 0 0 / 10px 10px,
        linear-gradient(160deg, rgba(255, 255, 255, 0.7), var(--ink-wash));
    border: 1px solid var(--glass-line); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
}
.team-portrait .team-photo { display: block; width: 100%; height: 100%; object-fit: cover; }
.team-portrait .team-dots { position: absolute; top: 0; left: 0; width: 100%; height: auto; aspect-ratio: 1; display: block; }
.team-portrait.is-live .team-photo { opacity: 0; }
.team-portrait.is-initials { display: grid; place-items: center; }
.team-portrait.is-initials span { font-family: var(--mono); font-size: 2rem; font-weight: 600; letter-spacing: 0.08em; color: var(--ink); }
.team-body { display: flex; flex-direction: column; }
.team-card h3 { font-size: 1.35rem; }
.team-links { display: flex; flex-wrap: wrap; gap: 0.75rem 1.5rem; margin-top: auto; padding-top: 1.25rem; }
.team-solo { max-width: none; }
.team-card.is-solo {
    display: grid; grid-template-columns: minmax(0, 29rem) minmax(0, 1fr); gap: 3rem; align-items: stretch; padding: 1.25rem;
}
.team-card.is-solo .team-portrait { margin: 0; align-self: start; }
.team-card.is-solo .team-body { padding: 1.75rem 1.5rem 1rem 0; }
.team-card.is-solo h3 {
    font-size: clamp(1.9rem, 3vw, 2.6rem); font-weight: 400; line-height: 1.05; letter-spacing: -0.035em; margin-top: 0.25rem;
}
.team-card.is-solo .team-body > p { margin-top: 1rem; max-width: 30rem; }
/* The bio is in the person's own voice, so it reads as a paragraph, not a card blurb. */
.team-card .team-bio { font-size: 1.02rem; line-height: 1.65; color: var(--ink-body); max-width: 40rem; }
.team-card .team-bio + .team-bio { margin-top: 0.85rem; }
/* Experience: always open under the bio, a small label then the career on a hairline grid
   (years, then the organisation with the role beneath it). */
.team-exp { margin-top: 1.75rem; max-width: 40rem; }
.team-exp-label {
    font-family: var(--mono); font-size: var(--fs-micro); font-weight: 400; letter-spacing: 0.2em; text-transform: uppercase;
    color: var(--ink-dim); margin: 0 0 0.6rem;
}
.team-timeline { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--glass-divider); }
.team-timeline li {
    display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); gap: 0.15rem 1.25rem; align-items: baseline;
    padding: 0.8rem 0; border-bottom: 1px solid var(--glass-divider); font-size: 0.92rem; line-height: 1.45;
}
.team-years { font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-dim); }
.team-org { color: var(--ink); font-weight: 500; }
.team-what { grid-column: 2; color: var(--ink-body); }
@media (max-width: 720px) {
    .team-card.is-solo { grid-template-columns: 1fr; gap: 1.5rem; }
    .team-card.is-solo .team-body { padding: 0 0.5rem 0.75rem; }
    .team-timeline li { grid-template-columns: 1fr; gap: 0.15rem; }
    .team-what { grid-column: 1; }
}

/* Case study card (image + meta) */
.case-card { padding: 0; }
.case-card .case-media { aspect-ratio: 16 / 9; background: var(--ink-wash); overflow: hidden; border-bottom: 1px solid var(--glass-divider); }
.case-card .case-media img { width: 100%; height: 100%; object-fit: cover; filter: saturate(0.85); }
.case-card .case-body { padding: 1.75rem 2rem 2rem; display: flex; flex-direction: column; flex: 1; }

/* ---------- Tags ---------- */
.tag {
    display: inline-block; padding: 0.25rem 0.625rem; border-radius: var(--r-xs);
    font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.1em; text-transform: uppercase; white-space: nowrap;
}
.cat-technical { background: var(--ink-faint); color: var(--ink); }
.cat-strategy { background: var(--accent-wash); color: var(--ink-body); }

/* ---------- Priority list (incident routing: escalation, support) ----------
   One glass row per priority: the level on the left, the text and its actions on the right.
   P1 sits a level up (glass level 2) with the phone call as the primary dark button; P2 to P4
   is level 1. Below 720px the level stacks above the text and the buttons go full width. */
.priority-list { display: flex; flex-direction: column; gap: 0.6rem; list-style: none; margin: 0; padding: 0; }
.priority-row {
    display: grid; grid-template-columns: 10rem minmax(0, 1fr); align-items: start; gap: 0.75rem 2rem;
    padding: 1.6rem 1.75rem; border: 1px solid var(--glass-line); border-radius: var(--r-glass-sm);
    background: var(--glass-fill-subtle); -webkit-backdrop-filter: var(--blur-sm); backdrop-filter: var(--blur-sm);
    box-shadow: var(--glass-shadow-1);
}
.priority-row.is-critical {
    background: var(--glass-sheen), var(--glass-fill); -webkit-backdrop-filter: var(--blur-md); backdrop-filter: var(--blur-md);
    box-shadow: var(--glass-shadow-2);
}
.priority-level {
    display: flex; align-items: center; gap: 0.5rem; padding-top: 0.2rem;
    font-family: var(--mono); font-size: 0.78rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-dim);
}
.priority-level b { color: var(--ink); font-weight: 600; }
.priority-row.is-critical .priority-level::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: var(--ink); flex-shrink: 0; }
.priority-text { font-size: 0.98rem; line-height: 1.65; font-weight: 300; color: var(--ink-body); max-width: 38rem; }
.priority-text a { color: var(--ink); border-bottom: 1px solid var(--ink-line); overflow-wrap: anywhere; transition: border-color var(--dur-fast) var(--ease); }
.priority-text a:hover { border-color: var(--ink); }
.priority-actions { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-top: 1.1rem; }

/* ---------- Testimonials ---------- */
.testimonial-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.testimonial-item {
    display: flex; flex-direction: column; justify-content: space-between;
    padding: 2rem; border: 1px solid var(--glass-line); border-radius: var(--r-glass-lg);
    background: var(--glass-sheen), var(--glass-fill);
    -webkit-backdrop-filter: var(--blur-md); backdrop-filter: var(--blur-md); box-shadow: var(--glass-shadow-2);
}
.testimonial-item.is-wide { grid-column: 1 / -1; background: var(--glass-sheen), var(--glass-fill-strong); box-shadow: var(--glass-shadow-3); }
.testimonial-quote { font-size: 0.92rem; line-height: 1.8; color: var(--ink); opacity: 0.8; margin-bottom: 1.5rem; }
.testimonial-person { display: flex; align-items: center; gap: 0.75rem; }
.testimonial-avatar {
    width: 32px; height: 32px; border-radius: var(--r-sm); background: var(--ink); color: var(--on-dark); flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    font-family: var(--mono); font-size: 0.7rem; font-weight: 600;
}
.testimonial-name { font-weight: 600; font-size: 0.8rem; }
.testimonial-role { font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink-dim); }

/* ---------- Index table (dated rows) ---------- */
.index-table { width: 100%; border-collapse: collapse; }
.index-table tr { border-top: 1px solid var(--ink-faint); }
.index-table tr:last-child { border-bottom: 1px solid var(--ink-faint); }
.index-table td { padding: 1.25rem 0; vertical-align: middle; }
.index-table .col-date { font-family: var(--mono); font-size: var(--fs-micro); color: var(--accent); padding-right: 1.5rem; width: 110px; white-space: nowrap; }
.index-table .col-title { font-size: 0.88rem; font-weight: 500; color: var(--ink); padding-right: 2rem; }
.index-table .col-title a { transition: opacity var(--dur-fast) var(--ease); }
.index-table .col-title a:hover { opacity: 0.6; }
.index-table .col-title small { display: block; margin-top: 0.25rem; font-size: 0.78rem; font-weight: 400; color: var(--ink-dim); }
.index-table .col-tag { text-align: right; width: 130px; }

/* ---------- Numbered steps ---------- */
.steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); border: 1px solid var(--glass-line); border-radius: var(--r-glass-sm); overflow: hidden; background: var(--glass-fill); -webkit-backdrop-filter: var(--blur-sm); backdrop-filter: var(--blur-sm); box-shadow: var(--glass-shadow-1); }
.steps.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.step { padding: 1.75rem 1.5rem; border-right: 1px solid var(--ink-faint); display: flex; flex-direction: column; gap: 0.6rem; }
.step:last-child { border-right: none; }
.step-num { font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.1em; color: var(--accent); }
.step-title { font-weight: 600; font-size: 1rem; letter-spacing: -0.015em; }
.step-text { font-size: 0.8rem; line-height: 1.6; color: var(--ink-dim); }

/* Method steps: steps with a progress bar, lead line and explanation */
.method-steps .step { padding: 2rem 1.75rem; gap: 0.75rem; }
.step-progress { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; margin-bottom: 0.75rem; }
.step-progress span { position: relative; overflow: hidden; height: 3px; border-radius: 2px; background: var(--ink-faint); }
/* The filled segments are drawn as a layer over the track, so they can fill on arrival. */
.step-progress span.on::after { content: ''; position: absolute; inset: 0; background: var(--ink); transform-origin: left center; }
.step-progress span:nth-child(2) { --seg: 1; }
.step-progress span:nth-child(3) { --seg: 2; }
/* Arrival (Scroll reveal, a stagger on the row): the steps rise in turn, then the bars fill
   one segment at a time across all three at once, as one progress line: step 01 stops at a
   third, step 02 at two thirds, and step 03 runs through to the end. */
@media (prefers-reduced-motion: no-preference) {
    html.js-motion.reveal-ready .method-steps[data-reveal]:not(.is-in) .step-progress span.on::after { transform: scaleX(0); }
    html.js-motion .method-steps[data-reveal].is-in .step-progress span.on::after {
        animation: step-fill 400ms var(--ease) calc(420ms + var(--seg, 0) * 360ms) backwards;
    }
}
@keyframes step-fill { from { transform: scaleX(0); } }
.method-steps .step-title { font-size: 1.15rem; }
.step-lead { font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink); }
.method-steps .step-text { font-size: 0.85rem; line-height: 1.7; margin-top: 0.25rem; }

/* Long-form numbered list (method documents) */
.method-list { counter-reset: method; display: flex; flex-direction: column; }
.method-item { display: grid; grid-template-columns: 3.5rem 1fr; gap: 1rem; padding: 1.5rem 0; border-top: 1px solid var(--ink-faint); }
.method-item:last-child { border-bottom: 1px solid var(--ink-faint); }
.method-item::before {
    counter-increment: method; content: counter(method, decimal-leading-zero);
    font-family: var(--mono); font-size: 0.7rem; color: var(--accent); padding-top: 0.2rem;
}
.method-item h3 { font-weight: 600; font-size: 1rem; margin-bottom: 0.4rem; letter-spacing: -0.015em; }
.method-item .method-objective { font-size: 0.88rem; line-height: 1.6; color: var(--ink-body); margin-bottom: 0.6rem; }
.method-item ul { display: flex; flex-direction: column; gap: 0.4rem; }
.method-item li { position: relative; padding-left: 1rem; font-size: 0.82rem; line-height: 1.6; color: var(--ink-dim); }
.method-item li::before { content: ''; position: absolute; left: 0; top: 0.65em; width: 4px; height: 4px; background: var(--accent); }

/* ---------- FAQ (details/summary) ---------- */
.faq { border-top: 1px solid var(--ink-faint); }
.faq details { border-bottom: 1px solid var(--ink-faint); }
.faq summary {
    list-style: none; cursor: pointer; padding: 1.35rem 0;
    display: flex; justify-content: space-between; align-items: center; gap: 1rem;
    font-size: 0.95rem; font-weight: 500;
}
.faq summary::-webkit-details-marker { display: none; }
/* The plus morphs into a minus as the answer opens (site.js, dsIcons.toggle, which marks it
   .is-morph). Without morphicons it turns into a cross instead. */
.faq-icon { width: 1rem; height: 1rem; margin: 0 0.25rem; flex-shrink: 0; color: var(--ink-dim); transition: transform var(--dur-base) var(--ease); }
.faq details[open] .faq-icon:not(.is-morph) { transform: rotate(45deg); }
.faq .faq-body { padding: 0 2.5rem 1.5rem 0; font-size: 0.9rem; line-height: 1.7; color: var(--ink-body); }

/* ---------- Detail rows (key facts) ---------- */
.fact-list { display: grid; grid-template-columns: 9rem 1fr; border-top: 1px solid var(--ink-faint); }
.fact-list dt, .fact-list dd { padding: 1rem 0; border-bottom: 1px solid var(--ink-faint); }
.fact-list dt { font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-dim); padding-top: 1.2rem; }
.fact-list dd { font-size: 0.92rem; color: var(--ink); }
.fact-list dd a { border-bottom: 1px solid var(--ink-line); }
.fact-list dd a:hover { border-color: var(--ink); }

/* ---------- Callout ---------- */
.callout {
    display: flex; gap: 1rem; align-items: flex-start;
    padding: 1.25rem 1.5rem; border: 1px solid var(--glass-line); border-left: 3px solid var(--ink);
    border-radius: var(--r-glass-sm); background: var(--glass-fill); -webkit-backdrop-filter: var(--blur-sm); backdrop-filter: var(--blur-sm); box-shadow: var(--glass-shadow-1);
    font-size: 0.88rem; line-height: 1.65; color: var(--ink-body);
}
.callout svg { width: 1rem; height: 1rem; flex-shrink: 0; margin-top: 0.2rem; color: var(--ink); }
.callout.is-muted { border-left-color: var(--accent); }

/* ---------- CTA band ---------- */
.cta-band {
    position: relative; overflow: hidden; border-radius: var(--r-glass-lg); color: var(--on-dark);
    background: var(--glass-dark-sheen), var(--glass-dark-fill), linear-gradient(180deg, var(--panel-top) 0%, var(--panel-bottom) 100%);
    border: 1px solid var(--on-dark-faint); box-shadow: var(--glass-shadow-dark);
    padding: 2.75rem; display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 2rem;
}
.cta-band h2 { font-weight: 700; font-size: 1.75rem; line-height: 1.15; letter-spacing: -0.04em; max-width: 26rem; margin-bottom: 0.9rem; }
.cta-band p { font-size: 0.92rem; font-weight: 300; line-height: 1.6; color: var(--on-dark-body); max-width: 28rem; }
.cta-band .aside-actions { flex-shrink: 0; max-width: 100%; }
/* Light. A soft band of light crosses the panel once as it scrolls into view (Scroll
   reveal), and again each time the pointer comes onto it or focus moves into it; the layer
   sits under the content (isolation). */
.cta-band { isolation: isolate; }
.cta-band::after {
    content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
    background: linear-gradient(105deg, transparent 32%, rgba(255, 255, 255, 0.05) 42%, rgba(255, 255, 255, 0.11) 50%, rgba(255, 255, 255, 0.05) 58%, transparent 68%);
    transform: translateX(-100%);
}
.cta-band:focus-within::after { transform: translateX(100%); transition: transform 1300ms var(--ease); }
@media (hover: hover) and (pointer: fine) {
    .cta-band:hover::after { transform: translateX(100%); transition: transform 1300ms var(--ease); }
}
@media (prefers-reduced-motion: no-preference) {
    html.js-motion .cta-band[data-reveal].is-in::after { animation: cta-sweep 1500ms var(--ease) 420ms backwards; }
}
@keyframes cta-sweep { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
.cta-band .btn {
    transition: background-color var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease),
        transform var(--dur-spring) var(--spring-snappy);
}
/* A press gives a little and springs back. Its arrow flies like every other (dsIcons). */
.cta-band .btn:active { transform: scale(0.97); transition-duration: var(--dur-fast); }

/* ---------- Media ---------- */
.figure { border: 1px solid var(--glass-line); border-radius: var(--r-glass-lg); overflow: hidden; background: var(--glass-sheen), var(--glass-fill); -webkit-backdrop-filter: var(--blur-md); backdrop-filter: var(--blur-md); box-shadow: var(--glass-shadow-2); }
.figure img { width: 100%; }
.figure.pad { padding: 2rem; }
.figure.pad img { margin: 0 auto; max-height: 360px; width: auto; }
.figure figcaption { font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-dim); padding: 0.85rem 1.25rem; border-top: 1px solid var(--glass-divider); }
.gallery { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.75rem; }
.gallery.cols-2 { grid-template-columns: repeat(2, 1fr); }
.gallery img { border-radius: var(--r-glass-sm); border: 1px solid var(--glass-line); box-shadow: var(--glass-shadow-1); aspect-ratio: 1; object-fit: cover; width: 100%; }

.logo-row { display: grid; grid-template-columns: repeat(6, 1fr); border: 1px solid var(--glass-line); border-radius: var(--r-glass-sm); overflow: hidden; background: var(--glass-fill); -webkit-backdrop-filter: var(--blur-sm); backdrop-filter: var(--blur-sm); box-shadow: var(--glass-shadow-1); }
.logo-cell { display: flex; align-items: center; justify-content: center; padding: 1.25rem 1rem; border-right: 1px solid var(--ink-faint); min-height: 5.5rem; }
.logo-cell:last-child { border-right: none; }
/* Client logos rest as one even tone (a silhouette at the accent grey's weight), so a pale
   mark (Channel 4) and a near-black one (Wifinity) read alike, and their sizes are evened out
   by eye: a small mark is allowed taller, heavy wide wordmarks are held lower. Pointing at
   one brings its own colours back. */
.logo-cell img {
    max-height: var(--logo-h, 2.6rem); max-width: 100%; width: auto; object-fit: contain;
    filter: brightness(0); opacity: 0.55;
    transition: filter var(--dur-slow) var(--ease-out), opacity var(--dur-slow) var(--ease-out);
}
.logo-cell img[src*="client-channel-4"] { --logo-h: 2.9rem; }
.logo-cell img[src*="client-midlands"] { --logo-h: 3rem; }
.logo-cell img[src*="client-cg"] { width: 88%; max-height: none; aspect-ratio: 200 / 30; object-fit: cover; } /* its wordmark sits in a thin band of a tall canvas: crop to the band */
.logo-cell img[src*="client-ach"] { --logo-h: 1.85rem; }
.logo-cell img[src*="client-wifinity"] { --logo-h: 2rem; }
@media (hover: hover) and (pointer: fine) {
    .logo-cell:hover img { filter: none; opacity: 1; }
}

.video-frame { position: relative; aspect-ratio: 16 / 9; border-radius: var(--r-glass-lg); overflow: hidden; border: 1px solid var(--glass-line); box-shadow: var(--glass-shadow-2); background: var(--code-bg); }
.video-frame.is-wide { aspect-ratio: 2 / 1; }
.video-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

.embed-frame { border: 1px solid var(--glass-line); border-radius: var(--r-glass-lg); overflow: hidden; background: var(--glass-fill-strong); box-shadow: var(--glass-shadow-2); }
.embed-frame iframe { width: 100%; height: 900px; border: 0; }

/* ---------- Quote ---------- */
/* The rule down the left is a layer rather than a border, so it can draw on arrival. */
.pull-quote { position: relative; padding: 0.25rem 0 0.25rem calc(1.5rem + 2px); }
.pull-quote::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px; background: var(--ink); transform-origin: 50% 0; }
/* Arrival (Scroll reveal, a stagger on the quote): the rule draws down, the words rise in
   behind it and the attribution follows. */
@media (prefers-reduced-motion: no-preference) {
    html.js-motion.reveal-ready .pull-quote[data-reveal]:not(.is-in)::before { transform: scaleY(0); }
    html.js-motion .pull-quote[data-reveal].is-in::before { animation: quote-rule 900ms var(--ease-out) backwards; }
    html.js-motion .pull-quote[data-reveal="stagger"].is-in > * { animation-delay: calc(200ms + var(--i, 0) * 160ms); }
}
@keyframes quote-rule { from { transform: scaleY(0); } }
.pull-quote p { font-size: 1.15rem; line-height: 1.6; letter-spacing: -0.015em; color: var(--ink); }
.pull-quote cite { display: block; margin-top: 0.9rem; font-style: normal; font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-dim); }

/* ---------- Forms ---------- */
.form { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
.field { display: flex; flex-direction: column; gap: 0.5rem; }
.field.full { grid-column: 1 / -1; }
.field label { font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-dim); }
.field label .req { color: var(--ink); }
.field input, .field select, .field textarea {
    width: 100%; background: var(--glass-fill-strong); border: 1px solid var(--glass-divider); border-radius: var(--r-sm);
    box-shadow: inset 0 1px 0 0 var(--glass-edge);
    padding: 0.85rem 1rem; font-size: 0.92rem; color: var(--ink);
    transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.field textarea { min-height: 9rem; resize: vertical; }
.field input:focus, .field select:focus, .field textarea:focus { outline: none; border-color: var(--ink); box-shadow: var(--shadow-focus); }
.field input::placeholder, .field textarea::placeholder { color: var(--ink-dim); }
.field-hint { font-size: 0.75rem; color: var(--ink-dim); }
.form-foot { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1rem; }
.form-status { font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.05em; color: var(--ink-dim); }

/* ---------- Footer ----------
   The footer is a full-bleed scene: a generated misty-hills landscape in the brand greens
   (assets/img/footer-scene.jpg, pale at the top, deep teal at the bottom) that the page body
   dissolves into. There is no edge: the scene layer is masked to nothing at its top, so the
   page background and ambient light show through and the hills fade in over the tall top
   padding. The content sits on the dark lower part in the on-dark palette, inside the usual
   content column. Without the image, a page-to-panel gradient stands in. */
.site-footer.is-scene {
    position: relative; isolation: isolate; overflow: hidden;
    width: 100%; max-width: none; margin: 0; padding: 0;
    color: var(--on-dark);
}
/* Two layers (render_footer): .footer-scene is the frame and carries the top mask, fixed to
   the footer; .footer-scene-art is the picture inside it, free to move on arrival (see Footer
   motion) without the faded top edge moving with it. */
.footer-scene {
    position: absolute; inset: 0; z-index: -2; pointer-events: none;
    -webkit-mask-image: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.4) 16%, rgba(0, 0, 0, 0.9) 32%, #000 42%);
    mask-image: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.4) 16%, rgba(0, 0, 0, 0.9) 32%, #000 42%);
}
.footer-scene-art {
    position: absolute; inset: 0;
    /* The scene is scaled by height and anchored at the bottom: only the lowest third of the
       picture is dark, so enlarging it gives the content a full dark band to sit on while
       the pale sky is what fades in at the top. */
    background:
        url(../img/footer-scene.jpg?v=f2351af6) 50% 100% / auto 225% no-repeat,
        linear-gradient(180deg, var(--bg) 0%, var(--panel-bottom) 60%, var(--panel-top) 100%);
    transform-origin: 50% 100%;
}
/* A scrim over the lower part so the white text keeps its contrast over the hills. */
.site-footer.is-scene::after {
    content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
    background: linear-gradient(180deg, transparent 16%, rgba(8, 44, 43, 0.5) 34%, rgba(8, 44, 43, 0.68) 100%);
}
.footer-inner { position: relative; max-width: calc(var(--content-w) + 2 * var(--gutter)); margin: 0 auto; padding: 18rem var(--gutter) 2.25rem; }
.footer-top { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: flex-start; gap: 2.5rem 4rem; }
.footer-brand { max-width: 20rem; }
.footer-brand .brand { color: var(--on-dark); }
.footer-brand .brand-name { font-size: 1.05rem; }
.footer-brand .brand-mark { width: 1.6rem; height: 1.6rem; }
.footer-tagline { font-size: 0.95rem; line-height: 1.5; color: var(--on-dark); margin: 1rem 0 1.25rem; }
.footer-loc { display: flex; align-items: center; gap: 0.5rem; font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.1em; text-transform: uppercase; color: var(--on-dark-body); }
.footer-cols { display: flex; flex-wrap: wrap; gap: 2rem 3.5rem; }
.footer-col { display: flex; flex-direction: column; gap: 0.55rem; min-width: 7rem; }
.footer-col-title { font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.1em; text-transform: uppercase; color: var(--on-dark-body); margin-bottom: 0.4rem; }
/* Links hug their text (align-self) so the spring underline below is as long as the word. */
.footer-col a { align-self: flex-start; font-size: 0.875rem; line-height: 1.4; color: var(--on-dark); }
.footer-bottom {
    margin-top: 2.75rem; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 1rem;
    font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.3em; text-transform: uppercase; color: var(--on-dark-body);
}
.footer-bottom > span { grid-column: 2; text-align: center; }
.footer-bottom > a { grid-column: 3; justify-self: end; display: inline-flex; align-items: center; gap: 0.375rem; transition: color var(--dur-fast) var(--ease); }
.footer-bottom a:hover { color: var(--on-dark); }
.site-footer.is-scene :focus-visible { outline-color: var(--focus-on-dark); }
/* The group strip and boilerplate are shared word for word with thecloud-iq.io and raad-iot.com. */
.group-strip {
    margin-top: 3rem;
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.75rem 2rem;
}
.group-strip-label { font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.1em; text-transform: uppercase; color: var(--on-dark-body); }
.group-strip-list { display: flex; flex-wrap: wrap; gap: 0.75rem 1.75rem; }
.group-strip-list a, .group-strip-list .is-current {
    font-family: var(--mono); font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--on-dark);
    display: inline-flex; align-items: center; gap: 0.3rem;
}
.group-strip-list .is-current { border-bottom: 1px solid var(--on-dark-line); }
.group-boiler { font-size: 0.8rem; line-height: 1.65; color: var(--on-dark); max-width: 52rem; margin-top: 1rem; }

/* ---------- Footer motion ----------
   One signature moment (the arrival) and a few precise responses, in the menus' springs.
   Arrival: as the footer scrolls into view the mist condenses and the hills settle. The
   picture starts slightly enlarged from its foot, so the far hills begin higher and travel
   further than the near ground (depth from a single image), and it reaches its resting
   composition exactly when the footer is fully in view. A view timeline on the footer scrubs
   it with the reader's own scroll; transform and opacity only, on the art layer alone (the
   frame's mask stays put). It only ever starts larger from the foot, so the ground under
   the text is never lighter than at rest. No view timelines, or reduced motion: the scene rests. */
@supports (animation-timeline: view()) {
    .site-footer.is-scene { view-timeline: --ds-footer block; }
    .footer-scene-art {
        animation: ds-footer-arrive linear both;
        animation-timeline: --ds-footer;
        animation-range: entry 0% entry 100%;
    }
}
@keyframes ds-footer-arrive {
    0% { opacity: 0.4; transform: scale(1.14); animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1); }
    30% { opacity: 1; }
    100% { opacity: 1; transform: none; }
}
/* The content then enters with the shared scroll reveal (data-reveal in render_footer): the
   columns wait one beat, so the top row reads left to right, brand first. */
.footer-cols { --reveal-delay: var(--stagger-reveal); }
/* Living scene: once the footer is on screen, thin bands of fog drift slowly over the far
   hills, the way the mist moves in the picture. The layer sits inside the masked frame (so
   its top dissolves like the rest) and has its own mask that keeps it in the 18rem above the
   content on every screen size, so it never passes behind text. It fades in and drifts only
   while the footer is visible (site.js sets .is-live); with reduced motion or without JS it
   does not exist, and the scene is exactly the still picture. */
@media (prefers-reduced-motion: no-preference) {
    html.js-motion .footer-scene::after {
        content: ''; position: absolute; top: 0; left: -25%; width: 150%; height: 18rem; pointer-events: none;
        background:
            radial-gradient(22% 11% at 18% 60%, rgba(255, 255, 255, 0.42), transparent 70%),
            radial-gradient(26% 8% at 50% 71%, rgba(255, 255, 255, 0.36), transparent 70%),
            radial-gradient(20% 12% at 82% 57%, rgba(255, 255, 255, 0.34), transparent 70%);
        -webkit-mask-image: linear-gradient(180deg, transparent 3rem, #000 8rem, #000 11rem, transparent 16rem);
        mask-image: linear-gradient(180deg, transparent 3rem, #000 8rem, #000 11rem, transparent 16rem);
        opacity: 0;
        animation: ds-footer-mist 56s ease-in-out infinite alternate paused;
        transition: opacity var(--dur-ambient) var(--ease-out);
    }
    html.js-motion .site-footer.is-live .footer-scene::after { opacity: 1; animation-play-state: running; }
}
@keyframes ds-footer-mist { from { transform: translateX(-8%); } to { transform: translateX(8%); } }

/* Links: the header's spring underline in the on-dark palette, grown from the left on hover
   (pointer devices), keyboard focus and press. */
.footer-col a, .group-strip-list a, .footer-bottom > a { position: relative; }
.footer-col a::after, .group-strip-list a::after, .footer-bottom > a::after {
    content: ''; position: absolute; left: 0; right: 0; bottom: -0.2rem; height: 1px; pointer-events: none;
    background: currentColor; transform: scaleX(0); transform-origin: left center;
    transition: transform var(--dur-spring) var(--spring);
}
.group-strip-list a::after { bottom: -1px; } /* level with the current site's rule */
.footer-col a:focus-visible::after, .footer-col a:active::after,
.group-strip-list a:focus-visible::after, .group-strip-list a:active::after,
.footer-bottom > a:focus-visible::after, .footer-bottom > a:active::after { transform: scaleX(1); }
.footer-bottom > a:focus-visible { color: var(--on-dark); }

/* Sister sites and Back to top: their arrows fly like every other (site.js, dsIcons), up and
   out of the slot (ext-arrow, arrow-icon), once per hover or focus. */
.ext-arrow { display: inline-flex; }

/* Copy the email address: an icon button after "Email us" (site.js reveals it, as it needs
   JS). Hover and keyboard focus preview the address in a small glass chip; a copy morphs the
   icon into a check and rolls the chip over to "Copied". Negative margins keep the row height. */
.footer-mail { display: inline-flex; align-items: center; gap: 0.3rem; align-self: flex-start; }
.footer-copy {
    position: relative; display: inline-grid; place-items: center;
    width: 1.5rem; height: 1.5rem; margin: -0.25rem 0; border-radius: var(--r-sm);
    color: var(--on-dark-body);
    transition: color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease), transform var(--dur-spring) var(--spring-snappy);
}
.footer-copy:active { transform: scale(0.88); transition-duration: var(--dur-fast); }
.footer-copy:focus-visible, .footer-copy.is-copied { color: var(--on-dark); background-color: var(--on-dark-faint); }
.footer-copy-tip {
    position: absolute; left: 50%; bottom: calc(100% + 0.45rem); pointer-events: none;
    display: grid; padding: 0.4rem 0.6rem; border-radius: var(--r-sm);
    background: var(--glass-sheen), var(--bg-glass); border: 1px solid var(--glass-line); box-shadow: var(--glass-shadow-1);
    -webkit-backdrop-filter: var(--blur-md); backdrop-filter: var(--blur-md); /* the line above blurs away, never shows through */
    color: var(--ink); font-family: var(--mono); font-size: var(--fs-micro); line-height: 1.2; letter-spacing: 0.04em; white-space: nowrap;
    opacity: 0; transform: translate(-50%, 0.35rem) scale(0.94); transform-origin: 50% 100%;
    transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
}
.footer-copy:focus-visible .footer-copy-tip, .footer-copy.is-copied .footer-copy-tip, .footer-copy.is-failed .footer-copy-tip {
    opacity: 1; transform: translate(-50%, 0) scale(1);
    transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-spring) var(--spring);
}
/* The chip's two lines share one cell; the copy rolls the address up and out, "Copied" up and in. */
.footer-copy-tip > span { grid-area: 1 / 1; text-align: center; transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-spring) var(--spring); }
.footer-copy-tip > span + span { opacity: 0; transform: translateY(0.6rem); text-transform: uppercase; letter-spacing: 0.1em; }
.footer-copy.is-copied .footer-copy-tip > span:first-child { opacity: 0; transform: translateY(-0.6rem); }
.footer-copy.is-copied .footer-copy-tip > span + span { opacity: 1; transform: none; }
/* Touch only: there is no hover preview, so the chip fits "Copied" (the address still shows if a copy fails). */
@media (hover: none) {
    .footer-copy:not(.is-failed) .footer-copy-tip > span:first-child { display: none; }
}

/* Pointer devices only: hover mirrors keyboard focus (touch gets the press and the copy state). */
@media (hover: hover) and (pointer: fine) {
    .footer-col a:hover::after, .group-strip-list a:hover::after, .footer-bottom > a:hover::after { transform: scaleX(1); }
    .footer-copy:hover { color: var(--on-dark); background-color: var(--on-dark-faint); }
    .footer-copy:hover .footer-copy-tip {
        opacity: 1; transform: translate(-50%, 0) scale(1);
        transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-spring) var(--spring);
    }
}

/* Footer scene on small screens: the scene is zoomed further so its dark ground covers the
   taller stack of content, the brand sits above two columns of links, and the copyright
   and back-to-top are stacked and centred. */
@media (max-width: 960px) {
    .site-footer.is-scene { padding: 0; }
    .footer-scene-art { background-size: auto 270%, auto; }
    /* The copy button keeps its look but gets a 44px tap target. */
    .footer-copy::before { content: ''; position: absolute; inset: -0.625rem; }
    .site-footer.is-scene::after { background: linear-gradient(180deg, transparent 10%, rgba(8, 44, 43, 0.55) 24%, rgba(8, 44, 43, 0.68) 100%); }
    .footer-inner { padding: 18rem var(--gutter) 2rem; }
    .footer-top { flex-direction: column; gap: 2.5rem; }
    .footer-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem 1.5rem; width: 100%; }
    .footer-col { min-width: 0; }
    .footer-bottom { grid-template-columns: 1fr; justify-items: center; text-align: center; }
    .footer-bottom > span, .footer-bottom > a { grid-column: 1; justify-self: center; }
}

/* ---------- Product showcase (what we build) ----------
   Not cards: a Z of full-width rows. Each row is the product's own artwork on a
   chevron-shaped pale backdrop beside a result line (grey lead, dark outcome), a bare stats
   row and an underlined small-caps link; even rows put the artwork on the right. */
.showcase { display: flex; flex-direction: column; gap: 3.5rem; --lockup-h: 2.5rem; }
/* The visual spans the row; the lockup and the copy sit beside it as one block, centred on
   it (the two 1fr rows share the spare height). */
.showcase-item {
    position: relative; display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
    grid-template-areas: "visual ." "visual lockup" "visual copy" "visual .";
    grid-template-rows: 1fr auto auto 1fr; gap: 0 5rem; align-items: center;
}
.showcase-item.is-even { grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); grid-template-areas: ". visual" "lockup visual" "copy visual" ". visual"; }
.showcase-copy { grid-area: copy; display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
.showcase-visual { grid-area: visual; position: relative; aspect-ratio: 4 / 3; overflow: visible; }
/* Product lockups: both head their copy column, on its left edge, at one height and one
   distance above the result line, so the two products read as a pair (on a phone they head
   the whole product, above its visual). Each keeps its own artwork, colours and proportions:
   RAAD's horizontal logo fills the height; Cloud-IQ's icon and wordmark keep the ratio of
   thecloud-iq.io's own header lockup (wordmark 0.64 of the icon, gap 0.43 of it), which keeps
   the wordmark above the brand's 120px minimum width. */
.showcase-lockup { grid-area: lockup; justify-self: start; display: flex; align-items: center; gap: calc(var(--lockup-h) * 0.34); height: var(--lockup-h); margin-bottom: 1.6rem; }
.showcase-lockup img { height: 100%; width: auto; }
.showcase-lockup:has(.is-wordmark) img:not(.is-wordmark) { height: 80%; }
.showcase-lockup .is-wordmark { height: 51.5%; }
/* Arrival (Scroll reveal, a stagger on each row): the lockup is unveiled left to right, mark
   first, then the visual and the copy rise in behind it. */
@media (prefers-reduced-motion: no-preference) {
    html.js-motion .showcase-item[data-reveal].is-in > .showcase-lockup { animation-name: ds-reveal-wipe; animation-duration: 1100ms; }
}
/* ---------- RAAD console mock ----------
   The live-map console from raad-iot.com's hero in its fully expanded state, ported as a
   static mock: rail, header, tabs, Esri tile map with the vehicles driving the original
   routes (site.js). It renders at its native 1200 x 760 and is scaled to its box (--s), so
   the pixel routes stay exact. RAAD's own product colours apply inside it. */
.showcase-item.is-raad { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); }
.showcase-visual.is-console { aspect-ratio: auto; display: block; padding: 0 0 1.5rem; }
/* The native-size frame inside is clipped to this box (plus room for its shadow), so the
   unscaled 1200px layout never widens the page. The box reaches --bleed past the frame's
   left and top edges too, so its hairline outline is never cut while the frame itself stays
   flush with the content column. */
.console-fit { --bleed: 12px; position: relative; width: 90%; aspect-ratio: 1200 / 760; box-sizing: content-box; padding: var(--bleed) 40px 64px var(--bleed); margin: calc(-1 * var(--bleed)) 0 0 calc(-1 * var(--bleed)); overflow: hidden; }
.console {
    position: absolute; left: var(--bleed, 0); top: var(--bleed, 0); width: 1200px; height: 760px;
    transform-origin: 0 0; transform: scale(var(--s, 0.5));
    display: flex; overflow: hidden; border-radius: 18px;
    background: #F8FAFC; color: #403E3F; font-family: var(--sans); font-size: 14px; line-height: 1.3;
    box-shadow: 0 48px 90px -36px rgba(8, 44, 43, 0.5), 0 0 0 1px rgba(8, 44, 43, 0.08);
    --navy: #0C1C2D; --muted: #676767; --faint: #B3B2B2; --border: #E2E8F0; --border-soft: #F1F5F9;
    --accent: #1488CC; --accent-soft: rgba(20, 136, 204, 0.10); --panel: #FFFFFF;
}
.console .rail { width: 64px; flex: none; background: var(--navy); display: flex; flex-direction: column; align-items: center; padding: 14px 0; }
.console .rail img { height: 26px; width: auto; margin-bottom: 12px; }
.console .rail i { width: 20px; height: 20px; border-radius: 6px; margin: 5px 0; background: rgba(255, 255, 255, 0.16); display: block; }
.console .rail i.on { background: rgba(255, 255, 255, 0.5); }
.console .rail hr { width: 44px; border: 0; border-top: 1px solid rgba(255, 255, 255, 0.13); margin: 9px 0; }
.console .col { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.console .head { height: 56px; flex: none; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; background: var(--panel); border-bottom: 1px solid var(--border); }
.console .title { font-weight: 600; font-size: 16px; color: var(--navy); }
.console .user { display: flex; align-items: center; gap: 10px; }
.console .who { text-align: right; line-height: 1.25; }
.console .who b { display: block; font-size: 13px; font-weight: 500; color: #403E3F; }
.console .who span { display: block; font-size: 11px; color: var(--muted); }
.console .av { width: 30px; height: 30px; border-radius: 50%; background: var(--accent); color: #fff; display: grid; place-items: center; font-size: 12px; font-weight: 600; }
.console .tabs { flex: none; display: flex; gap: 4px; padding: 0 24px; background: var(--panel); border-bottom: 1px solid var(--border); }
.console .tab { padding: 12px; font-size: 14px; color: var(--muted); border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap; }
.console .tab.on { color: var(--navy); font-weight: 500; border-bottom-color: var(--accent); }
.console .mapwrap { position: relative; flex: 1; min-height: 0; overflow: hidden; background: #F6F3EC; }
.console .tilemap { position: absolute; inset: 0; overflow: hidden; }
.console .tiles { position: absolute; left: 50%; top: 50%; width: 2816px; height: 1792px; margin-left: -1415px; margin-top: -1006px; }
.console .tiles img { position: absolute; width: 256px; height: 256px; max-width: none; left: var(--x); top: var(--y); }
.console .tiles img.veh { position: absolute; left: 0; top: 0; width: var(--w); height: var(--h); z-index: 4; transform-origin: 50% 50%; will-change: transform; filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.55)); }
.console .tiles img.veh.is-parked { transform: translate(var(--x), var(--y)) translate(-50%, -50%) rotate(var(--r)); }
.console .chips { position: absolute; left: 316px; top: 12px; z-index: 20; display: flex; gap: 3px; padding: 5px 7px; border-radius: 9px; background: rgba(255, 255, 255, 0.95); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); font-size: 12px; }
.console .chips b { display: inline-flex; align-items: center; gap: 5px; padding: 2px 7px; border-radius: 6px; font-weight: 400; color: #403E3F; }
.console .chips b i { width: 8px; height: 8px; border-radius: 50%; background: var(--c); }
.console .chips b.sel { background: var(--accent-soft); box-shadow: inset 0 0 0 1px rgba(20, 136, 204, 0.6); }
.console .chips b.fav { color: #f5a623; }
.console .units { position: absolute; left: 12px; top: 12px; bottom: 12px; width: 292px; z-index: 25; display: flex; flex-direction: column; overflow: hidden; background: rgba(255, 255, 255, 0.95); border: 1px solid var(--border); border-radius: 12px; box-shadow: 0 10px 30px -10px rgba(12, 28, 45, 0.28); }
.console .u-head { padding: 9px 12px; border-bottom: 1px solid var(--border-soft); font-size: 12px; font-weight: 600; color: var(--navy); display: flex; justify-content: space-between; }
.console .u-head span { font-weight: 400; color: var(--muted); }
.console .u-search { padding: 10px 12px; border-bottom: 1px solid var(--border-soft); }
.console .u-search span { display: block; padding: 7px 10px; border: 1px solid var(--border); border-radius: 8px; background: #fff; color: var(--faint); font-size: 12.5px; }
.console .unit { display: flex; align-items: center; gap: 9px; padding: 9px 12px; border-bottom: 1px solid var(--border-soft); }
.console .unit.on { background: var(--accent-soft); }
.console .unit i { width: 9px; height: 9px; border-radius: 50%; flex: none; background: var(--c); }
.console .unit b { display: block; font-size: 13px; font-weight: 500; color: var(--navy); }
.console .unit span { display: block; font-size: 11px; color: var(--muted); margin-top: 1px; }
.console .unit small { display: block; font-size: 11px; color: var(--faint); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.console .u-foot { margin-top: auto; padding: 8px 12px; border-top: 1px solid var(--border-soft); font-size: 11px; color: var(--muted); }
.console .attrib { position: absolute; right: 6px; bottom: 4px; z-index: 20; font-size: 10px; color: rgba(12, 28, 45, 0.55); background: rgba(255, 255, 255, 0.7); padding: 1px 5px; border-radius: 4px; }
/* The phone sits in front of the console's right edge. */
.is-console .device { position: absolute; right: 0; bottom: 0; z-index: 3; width: min(25%, 170px); }

/* Phone mock-up: the raw screen capture with a hairline black bezel, nothing else. The screen's
   corner radius is the frame's less the bezel, so the black stays the same width all round. */
.showcase-visual.is-device { display: flex; align-items: center; justify-content: flex-end; padding-right: 8%; }
.device {
    --bezel: 1.5px; --device-r: 1.25rem;
    position: relative; width: min(44%, 240px); padding: var(--bezel); border-radius: var(--device-r);
    background: #0B1211;
    box-shadow: 0 36px 64px -28px rgba(8, 44, 43, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.35);
}
.device img { display: block; width: 100%; border-radius: calc(var(--device-r) - var(--bezel)); }
/* SIM loader: four of Cloud-IQ's SIM cards chase each other round a circle. All four run
   the same lap on a curve that rushes out of the gate and eases off without ever stopping;
   they start a beat apart (--k) and each also holds a fixed lead (--gap), so the pack closes
   in on the slow stretch but never catches the leader before the next lap opens it up again. */
/* The Cloud-IQ stage: the loader, centred in its column. */
.showcase-visual.is-ring { aspect-ratio: auto; display: flex; flex-direction: column; align-items: center; padding: 1rem 0; }
.sim-ring {
    position: relative; width: min(100%, 360px); aspect-ratio: 1; --gap: 18deg;
    --chase: linear(0, 0.07 7%, 0.3 17%, 0.62 30%, 0.82 44%, 0.92 60%, 0.97 78%, 1);
}
.sim-ring img {
    position: absolute; left: 50%; top: 50%; width: 32%; margin: -16% 0 0 -16%;
    transform: rotate(calc(var(--k, 0) * -1 * var(--gap))) translateY(-125%);
    filter: drop-shadow(0 12px 16px rgba(8, 44, 43, 0.2));
    animation: sim-chase 2.6s var(--chase) infinite; animation-delay: calc(var(--k, 0) * -0.14s);
}
@supports not (transition-timing-function: linear(0, 1)) { .sim-ring { --chase: cubic-bezier(0.6, 0, 0.3, 1); } }
.sim-ring.is-idle img { animation-play-state: paused; } /* off screen (site.js) */
@keyframes sim-chase {
    from { transform: rotate(calc(var(--k, 0) * -1 * var(--gap))) translateY(-125%); }
    to { transform: rotate(calc(360deg - var(--k, 0) * var(--gap))) translateY(-125%); }
}
/* Artwork pieces are positioned by hand per item (left, top, width in %). */
.showcase-art { position: absolute; width: var(--w); left: var(--x); top: var(--y); transform: rotate(var(--r, 0deg)); filter: drop-shadow(0 18px 24px rgba(8, 44, 43, 0.18)); transition: transform 600ms var(--ease-out); }
.showcase-item:hover .showcase-art { transform: rotate(var(--r, 0deg)) translateY(-4px); }
.showcase-result { font-size: 1.1rem; line-height: 1.45; letter-spacing: -0.015em; color: var(--ink-dim); max-width: 30rem; }
.showcase-result b { color: var(--ink); font-weight: 600; }
.showcase-result .arrow-icon { margin-right: 0.35rem; color: var(--ink); vertical-align: -0.1em; }
/* Stats read as one sentence, separated by mid-dots, so they wrap like prose rather than a grid. */
.showcase-stats { display: flex; flex-wrap: wrap; row-gap: 0.2rem; font-size: 0.9rem; line-height: 1.7; color: var(--ink-dim); max-width: 30rem; }
.showcase-stats span { white-space: nowrap; }
.showcase-stats span:not(:last-child)::after { content: '·'; margin: 0 0.55em; color: var(--ink-mute); }
.showcase-stats b { font-weight: 600; color: var(--ink); margin-right: 0.3rem; }
/* The stats settle in (Scroll reveal, a stagger): each comes up out of a soft focus in turn,
   a beat after the copy. The figures themselves are never animated or changed. */
.showcase-stats[data-reveal] { --reveal-delay: 200ms; }
@media (prefers-reduced-motion: no-preference) {
    html.js-motion .showcase-stats[data-reveal="stagger"].is-in > * { animation-name: stat-settle; }
}
@keyframes stat-settle { from { opacity: 0; translate: 0 0.5rem; filter: blur(5px); } }
.showcase-link { align-self: flex-start; display: inline-flex; align-items: center; gap: 0.3rem; font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink); border-bottom: 1px solid var(--ink-line); padding-bottom: 0.1rem; transition: border-color var(--dur-fast) var(--ease); }
.showcase-link:hover { border-color: var(--ink); }
.showcase-item .stretched::after { content: ''; position: absolute; inset: 0; }
/* Pointing at a product row (the whole row is its link) or focusing the link: the link's
   rule darkens, and the lead arrow and the link's arrow fly (site.js, dsIcons). No lift. */
.showcase-link:focus-visible { border-color: var(--ink); }
@media (hover: hover) and (pointer: fine) {
    .showcase-item:hover .showcase-link { border-color: var(--ink); }
}
/* The hand-off line is the connector between the two products: centred in the generous
   interval between the rows, quiet, and the one thing there. A hairline wire runs through it
   from the RAAD row to the Cloud-IQ row (a segment above the text and one below, each fading
   out towards its row). The text is locked copy: the wire is drawn by the pseudo-elements. */
.handoff {
    position: relative; --wire: 2.6rem;
    align-self: center; text-align: center; max-width: 32rem; margin: 0.5rem 0;
    font-size: 1.2rem; line-height: 1.4; letter-spacing: -0.015em; color: var(--ink-dim); text-wrap: balance;
}
.handoff::before, .handoff::after {
    content: ''; position: absolute; left: 50%; width: 9px; height: var(--wire); margin-left: -4.5px; pointer-events: none;
    transform-origin: 50% 0;
    /* Two layers: the signal pulse (parked above the segment until it runs) and the 1px wire. */
    background:
        radial-gradient(circle closest-side, var(--ink) 0 2.5px, rgba(8, 44, 43, 0.16) 3px 4px, transparent 4.5px) 0 -9px / 9px 9px no-repeat,
        linear-gradient(90deg, transparent 4px, var(--ink-line) 4px 5px, transparent 5px);
}
.handoff::before {
    bottom: calc(100% + 0.8rem);
    -webkit-mask-image: linear-gradient(180deg, transparent, #000 70%); mask-image: linear-gradient(180deg, transparent, #000 70%);
}
.handoff::after {
    top: calc(100% + 0.8rem);
    -webkit-mask-image: linear-gradient(180deg, #000 30%, transparent); mask-image: linear-gradient(180deg, #000 30%, transparent);
}
/* The signature moment of the products section (Scroll reveal adds .is-in): the wire draws
   down from RAAD, one pulse runs its length and, as it passes through, the line's ink lifts
   for a moment before it carries on down to Cloud-IQ. The pulse then keeps repeating, one
   signal every --pulse-cycle, all three animations sharing the cycle so they stay in step. */
@media (prefers-reduced-motion: no-preference) {
    .handoff { --pulse-cycle: 3200ms; }
    html.js-motion .handoff[data-reveal].is-in {
        animation: ds-reveal var(--dur-reveal) var(--ease-out) backwards, handoff-signal var(--pulse-cycle) linear 700ms infinite;
    }
    html.js-motion .handoff[data-reveal].is-in::before {
        animation: handoff-draw 620ms var(--ease-out) backwards, handoff-pulse-in var(--pulse-cycle) linear 700ms infinite;
    }
    html.js-motion .handoff[data-reveal].is-in::after {
        animation: handoff-draw 620ms var(--ease-out) 240ms backwards, handoff-pulse-out var(--pulse-cycle) linear 700ms infinite;
    }
}
@keyframes handoff-draw { from { transform: scaleY(0); } }
/* Down the upper segment (accelerating into the text), then parked out of sight. */
@keyframes handoff-pulse-in {
    0% { background-position: 0 -9px, 0 0; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
    22%, 100% { background-position: 0 var(--wire), 0 0; }
}
/* Waits while the signal crosses the text, then runs out of the lower segment. */
@keyframes handoff-pulse-out {
    0%, 36% { background-position: 0 -9px, 0 0; animation-timing-function: var(--ease-out); }
    64%, 100% { background-position: 0 var(--wire), 0 0; }
}
@keyframes handoff-signal {
    0%, 20%, 50%, 100% { color: var(--ink-dim); }
    30% { color: var(--ink); }
}

/* ---------- Reveal strips (what we do) ----------
   The reveal-strips idea from visual-reveal-manager.html laid horizontally: full-width glass
   pills stacked, each showing its band of the hero art through the glass so together they
   reveal the image. The active strip (hover, focus or tap; site.js) expands into the dark
   green panel. */
.strips { display: flex; flex-direction: column; gap: 0.6rem; --strip-art: url(../img/hero-glass.jpg?v=3af24b38); }
.strip {
    position: relative; isolation: isolate; overflow: hidden;
    border-radius: 1.75rem; border: 1px solid var(--glass-line);
    background: var(--glass-sheen), var(--glass-fill);
    -webkit-backdrop-filter: var(--blur-md); backdrop-filter: var(--blur-md);
    box-shadow: var(--glass-shadow-1); color: var(--ink);
    transition: border-radius var(--dur-expand) var(--ease-expand), box-shadow var(--dur-expand) var(--ease),
        border-color var(--dur-expand) var(--ease), color var(--dur-slow) var(--ease);
}
.strip:hover { box-shadow: var(--glass-shadow-2); }
/* The band of art: the image spans the strip's width; each strip shows its own slice (--i). */
.strip::before {
    content: ''; position: absolute; inset: 0; z-index: -2;
    background-image: var(--strip-art); background-size: 100% auto; background-repeat: no-repeat;
    background-position: 0 calc(var(--i, 0) * 100% / max(var(--n, 4) - 1, 1));
    opacity: 0.55; transition: opacity var(--dur-slow) var(--ease);
}
/* The dark panel, faded in over the glass when the strip is open. */
.strip::after {
    content: ''; position: absolute; inset: 0; z-index: -1; opacity: 0;
    background: var(--glass-dark-sheen), var(--glass-dark-fill), linear-gradient(180deg, var(--panel-top) 0%, var(--panel-bottom) 100%);
    transition: opacity var(--dur-expand) var(--ease);
}
.strip-head {
    width: 100%; cursor: pointer; text-align: left;
    display: grid; grid-template-columns: 2.5rem minmax(0, 1fr) auto; align-items: center; gap: 1rem;
    padding: 1.3rem 1.75rem;
    font-family: var(--mono); font-size: 0.78rem; letter-spacing: 0.12em; text-transform: uppercase; color: inherit;
}
.strip-num { color: var(--accent); transition: color var(--dur-slow) var(--ease); }
.strip-chev { width: 0.9rem; height: 0.9rem; opacity: 0.6; transition: transform var(--dur-expand) var(--ease-expand); }
/* The body's height is measured by site.js (--h) so it tweens pixel-accurately, open and
   closed in step, with one ease. The content fades up just behind the edge. */
.strips { --dur-expand: 460ms; }
.strip-body { height: 0; overflow: hidden; transition: height var(--dur-expand) var(--ease-expand); }
.strip-body > div { visibility: hidden; transition: visibility 0s linear var(--dur-expand); }
.strip-inner {
    display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: 2rem 3rem;
    padding: 0.25rem 1.75rem 2.25rem calc(1.75rem + 2.5rem + 1rem);
    opacity: 0; transform: translateY(10px);
    transition: opacity 220ms var(--ease), transform 320ms var(--ease-out);
}
.strip.is-open { border-radius: var(--r-glass-lg); color: var(--on-dark); border-color: var(--on-dark-faint); box-shadow: var(--glass-shadow-dark); }
.strip.is-open::before { opacity: 0; }
.strip.is-open::after { opacity: 1; }
.strip.is-open .strip-num { color: var(--on-dark-dim); }
.strip.is-open .strip-chev { transform: rotate(180deg); opacity: 1; }
.strip.is-open .strip-body { height: var(--h, auto); }
.strip.is-open .strip-body > div { visibility: visible; transition-delay: 0s; }
.strip.is-open .strip-inner { opacity: 1; transform: none; transition-delay: 90ms; }
.strip-inner p { font-size: 0.98rem; line-height: 1.65; font-weight: 300; color: var(--on-dark-body); max-width: 38rem; }
.strip-inner p + p { margin-top: 0.75rem; }
.strip-list { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 1.25rem; }
.strip-list a {
    display: inline-flex; align-items: center; gap: 0.35rem;
    padding: 0.45rem 0.75rem; border-radius: var(--r-sm);
    border: 1px solid var(--on-dark-line); background: var(--on-dark-wash); color: var(--on-dark);
    font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.08em; text-transform: uppercase;
    transition: background-color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.strip-list a:hover { background: var(--on-dark-faint); border-color: var(--on-dark); }
.strip-go {
    width: 3.5rem; height: 3.5rem; border-radius: 50%; flex-shrink: 0;
    border: 1px solid var(--on-dark-line); color: var(--on-dark);
    display: inline-flex; align-items: center; justify-content: center;
    transition: background-color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.strip-go svg { width: 1.25rem; height: 1.25rem; }
.strip-go:hover { background: var(--on-dark-faint); border-color: var(--on-dark); }
/* Opening choreography: the chips rise in one after another just behind the text (site.js
   numbers them --j) and the arrow button springs up last; closing takes them away together
   with the text. The chevron flattens into a minus as its strip opens (site.js morphs it and
   marks it .is-morph), so it no longer turns. The arrow flies on hover and keyboard focus
   (dsIcons), and the button gives a little under a press. */
.strip.is-open .strip-chev.is-morph { transform: none; }
/* The focus ring sits inside the head as a rounded pill, so the strip's clipping never cuts it. */
.strip-head:focus-visible { outline-offset: -5px; border-radius: 1.4rem; }
html.js-motion .strip-list a {
    opacity: 0; translate: 0 0.4rem;
    transition: background-color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease), opacity 150ms var(--ease), translate 0s linear 150ms;
}
html.js-motion .strip.is-open .strip-list a {
    opacity: 1; translate: none;
    transition: background-color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease),
        opacity 260ms var(--ease-out) calc(170ms + var(--j, 0) * 45ms), translate var(--dur-spring) var(--spring) calc(170ms + var(--j, 0) * 45ms);
}
html.js-motion .strip-go {
    opacity: 0; scale: 0.7;
    transition: background-color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease), opacity 150ms var(--ease), scale 0s linear 150ms;
}
html.js-motion .strip.is-open .strip-go {
    opacity: 1; scale: none;
    transition: background-color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease),
        opacity 240ms var(--ease-out) 300ms, scale var(--dur-spring) var(--spring-snappy) 300ms;
}
.strip-go:active { transform: scale(0.94); }

/* ---------- Case study masonry ----------
   Two columns split by a hairline, the right column dropped so the cards stagger. */
.case-masonry { position: relative; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 3rem 4rem; padding-bottom: 4rem; }
.case-masonry::before { content: ''; position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: var(--ink-faint); }
.case-masonry > :nth-child(even) { margin-top: 5rem; }
.case-item { position: relative; display: flex; flex-direction: column; gap: 1.1rem; }
.case-item .case-media {
    aspect-ratio: 4 / 3; overflow: hidden; border-radius: var(--r-glass-sm);
    border: 1px solid var(--glass-line); box-shadow: var(--glass-shadow-1); background: var(--ink-wash);
}
.case-item .case-media img { width: 100%; height: 100%; object-fit: cover; filter: saturate(0.8); transition: transform 600ms var(--ease-out); }
.case-result { font-size: 1.1rem; line-height: 1.45; letter-spacing: -0.015em; color: var(--ink-dim); max-width: 30rem; }
.case-result b { color: var(--ink); font-weight: 600; }
.case-result .arrow-icon { margin-right: 0.35rem; color: var(--ink); vertical-align: -0.1em; }
.case-client { align-self: flex-start; font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-dim); border-bottom: 1px solid var(--ink-line); padding-bottom: 0.1rem; transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease); }
.case-item .stretched::after { content: ''; position: absolute; inset: 0; }
/* Pointing at a case, or focusing its link: the picture leans in, the lead arrow flies
   (site.js, dsIcons) and the link's rule darkens. */
.case-item:focus-within .case-media img { transform: scale(1.03); }
.case-item:focus-within .case-client { color: var(--ink); border-color: var(--ink); }
@media (hover: hover) and (pointer: fine) {
    .case-item:hover .case-media img { transform: scale(1.03); }
    .case-item:hover .case-client { color: var(--ink); border-color: var(--ink); }
}
/* Arrival (Scroll reveal on each item): the frame rises in and the picture is unveiled inside
   it from the foot up, settling from a slight zoom (the scale property, so the lean above
   still applies). The hairline between the columns draws down as the first case arrives. */
.case-masonry::before { transform-origin: 50% 0; }
@media (prefers-reduced-motion: no-preference) {
    html.js-motion .case-item[data-reveal].is-in .case-media img { animation: case-unveil 1150ms var(--ease-out) 140ms backwards; }
    html.js-motion.reveal-ready .case-masonry:has(> [data-reveal]):not(:has(> .is-in))::before { transform: scaleY(0); }
    html.js-motion .case-masonry:has(> .is-in)::before { animation: case-rule 1600ms var(--ease-out) backwards; }
}
@keyframes case-unveil { from { clip-path: inset(100% 0 0 0); scale: 1.14; } to { clip-path: inset(0 0 0 0); } }
@keyframes case-rule { from { transform: scaleY(0); } }

/* ---------- Utilities ---------- */
.stack-sm > * + * { margin-top: 1rem; }
.stack > * + * { margin-top: 2rem; }
.stack-lg > * + * { margin-top: 3rem; }
.mt-0 { margin-top: 0 !important; }
.mb-0 { margin-bottom: 0 !important; }
.mt-lg { margin-top: 2rem; }
.mb-md { margin-bottom: 2rem; }
.mb-lg { margin-bottom: 2.5rem; }
.muted { color: var(--ink-dim); }
.text-sm { font-size: 0.85rem; line-height: 1.65; }
.mono { font-family: var(--mono); }
.nowrap { white-space: nowrap; }

/* ---------- Responsive ---------- */
@media (max-width: 1180px) {
    :root { --gutter: 2rem; }
    .content-wrap { gap: 5rem; }
    .nav-list { gap: 1.5rem; }
    .card-grid.cols-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .logo-row { grid-template-columns: repeat(3, 1fr); }
    .logo-cell:nth-child(3n) { border-right: none; }
    .logo-cell:nth-child(-n+3) { border-bottom: 1px solid var(--ink-faint); }
    .hero-body { padding: 8rem var(--gutter) 2rem; }
    .hero-strip { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); padding: 1.25rem var(--gutter) 1rem; }
    .hero-trust { grid-column: 1 / -1; justify-self: start; text-align: left; }
    .section-open { grid-template-columns: 1fr; gap: 1.25rem; }
}

@media (max-width: 960px) {
    :root { --gutter: 1.25rem; --head-h: 4rem; }
    .nav-pill { display: none; }
    /* No pill below 960px: the layer is always the full bar. */
    .glass-nav::before { left: 0; top: 0; width: 100%; height: 100%; border-radius: 0; border-color: transparent; box-shadow: inset 0 -1px 0 0 var(--glass-divider); transition: none; }
    /* Two cells only (the pill is gone): brand left, the menu pill flush right. */
    .site-head { grid-template-columns: minmax(0, 1fr) auto; gap: 1rem; padding: 0.5rem var(--gutter); }
    .site-head .brand { min-width: 0; justify-self: start; }
    .head-actions { justify-self: end; }
    .head-actions .btn { display: none; }
    .menu-toggle { display: inline-flex; }
    .layout-main { min-height: 0; }
    .content-wrap { padding: 3rem var(--gutter) 4rem; gap: 4rem; }
    .page-head + .content-wrap { padding-top: 2.5rem; }
    .page-head { grid-template-columns: 1fr; padding-top: 2.5rem; gap: 1.5rem; }
    .page-lede { font-size: 1.05rem; margin-top: 1rem; }
    .page-head .page-actions { padding-bottom: 0; gap: 1rem 1.25rem; }
    /* Buttons and small links become comfortable tap targets without changing their look:
       buttons grow to 44px, quiet links get an invisible hit area around them. */
    .btn { min-height: 2.75rem; }
    .btn-ghost, .btn-ghost-light, .link-underline:not(.stretched), .showcase-link:not(.stretched) { position: relative; }
    .btn-ghost::before, .btn-ghost-light::before, .link-underline:not(.stretched)::before, .showcase-link:not(.stretched)::before {
        content: ''; position: absolute; left: -0.5rem; right: -0.5rem; top: -0.75rem; bottom: -0.75rem;
    }
    /* Small screens: the art is cropped to its mid panes so they reach down to the headline,
       and the whole art dissolves into the page towards the foot of the hero, so the headline
       and strip sit on the same ambient ground as the rest of the page (no hard edge). */
    .hero { min-height: 88vh; background: transparent; }
    @supports (height: 100dvh) { .hero { min-height: 88dvh; } }
    .hero-art { -webkit-mask-image: linear-gradient(180deg, #000 45%, transparent 96%); mask-image: linear-gradient(180deg, #000 45%, transparent 96%); }
    .hero-art img { object-position: 46% 0; }
    .hero-art::after { background: linear-gradient(180deg, rgba(249, 250, 250, 0.2) 0%, rgba(249, 250, 250, 0.1) 20%, rgba(249, 250, 250, 0.72) 55%, rgba(249, 250, 250, 0.9) 100%); }
    .hero-body { padding: 7rem var(--gutter) 1.5rem; }
    .hero-title { max-width: none; font-size: clamp(2rem, 8.6vw, 2.6rem); }
    .hero-actions { gap: 0.75rem 1.25rem; margin-top: 1.5rem; }
    .hero-strip { grid-template-columns: 1fr; gap: 0.75rem; padding: 1.25rem var(--gutter) 0; }
    .hero-strip p { max-width: none; font-size: 0.9rem; }
    .hero-strip .hero-arrow { display: none; }
    .hero-cue { display: none; } /* phones scroll by habit; the strip stays one column */
    .hero-trust { justify-self: start; text-align: left; }
    /* Section openers: a touch less air before the content on a phone. */
    .section-open { margin-bottom: 2rem; }
    .section-open .section-title { font-size: 1.75rem; }
    .section-open-aside { gap: 0.75rem; }
    .section-note { margin-bottom: 1.5rem; }
    .site-footer { padding: 0 var(--gutter) 6rem; }
    /* One column: each lockup heads its product, flush left above the visual, then the copy. */
    .showcase { gap: 2.75rem; --lockup-h: 2.2rem; }
    .showcase-item, .showcase-item.is-even { grid-template-columns: 1fr; grid-template-areas: "lockup" "visual" "copy"; grid-template-rows: auto; gap: 1.5rem; }
    .showcase-lockup { margin-bottom: 0; }
    .handoff { font-size: 1.1rem; --wire: 2rem; }
    .showcase-visual { aspect-ratio: auto; display: flex; flex-direction: column; align-items: center; padding: 0 0 1rem; }
    .showcase-visual.is-device { justify-content: center; padding-right: 0; }
    .device { width: min(58%, 220px); }
    .showcase-item.is-raad { grid-template-columns: 1fr; }
    /* The phone overlaps the frame's lower right corner. */
    .showcase-visual.is-console { display: block; padding: 0 0 2rem; }
    .console-fit { width: calc(100% - 40px); }
    .is-console .device { width: 30%; right: 0; bottom: 0; }
    .showcase-visual.is-ring { padding: 0.5rem 0 0.75rem; }
    .sim-ring { width: min(76%, 280px); }
    .showcase-copy { gap: 0.9rem; }
    .showcase-result { font-size: 1.05rem; }
    .handoff { margin-top: 1.25rem; }
    /* Strips: the arrow link sits right under the chips, with a 44px row for every chip. */
    .strip-inner { gap: 1.25rem; }
    .strip-list { gap: 0.5rem; margin-top: 1.1rem; }
    .strip-list a { min-height: 2.5rem; padding: 0.5rem 0.8rem; }
    .strip-go { justify-self: end; width: 3rem; height: 3rem; }
    .strip-inner p { font-size: 0.95rem; }
    /* Case list: shallower tiles so each item is image, line and client in one screen. */
    .case-item .case-media { aspect-ratio: 16 / 10; }
    .case-item { gap: 0.9rem; }
    .case-result { font-size: 1.05rem; }
    .logo-row { grid-template-columns: repeat(3, 1fr); }
    .logo-cell { min-height: 4.25rem; padding: 1rem 0.75rem; border-right: 1px solid var(--ink-faint); }
    .logo-cell:nth-child(2n) { border-right: 1px solid var(--ink-faint); }
    .logo-cell:nth-child(3n) { border-right: none; }
    .logo-cell:nth-child(n+4) { border-bottom: none; }
    .footer-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem 1.5rem; width: 100%; }
    .footer-bottom { letter-spacing: 0.12em; gap: 0.75rem; }
    .case-masonry { grid-template-columns: 1fr; gap: 2.5rem; padding-bottom: 0; }
    .case-masonry::before { display: none; }
    .case-masonry > :nth-child(even) { margin-top: 0; }
    .strip-head { padding: 1.1rem 1.25rem; grid-template-columns: 2rem minmax(0, 1fr) auto; gap: 0.75rem; font-size: 0.74rem; letter-spacing: 0.1em; }
    .strip-inner { grid-template-columns: 1fr; padding: 0.25rem 1.25rem 1.5rem; }
}

@media (max-width: 720px) {
    .card-grid, .card-grid.cols-3, .testimonial-grid { grid-template-columns: 1fr; }
    .service-card.is-feature { grid-column: auto; padding: 2rem; }
    .service-card.is-feature h3 { font-size: 1.25rem; }
    .service-card.is-feature.case-card { padding: 0; }
    .service-card.is-feature.case-card .case-media { aspect-ratio: 16 / 9; }
    .service-card.is-feature.case-card .case-body { padding: 1.75rem 2rem 2rem; }
    .card-grid.is-list .service-card { flex-direction: column; gap: 0; }
    .card-grid.is-list .icon-tile { margin-bottom: 1.5rem; }
    .audience-split { grid-template-columns: 1fr; }
    .audience-divider { width: auto; height: 1px; }
    .tech-access-strip, .tech-access-strip.cols-4 { grid-template-columns: 1fr 1fr; }
    .tech-access-strip .tech-access-item { border-right: 1px solid var(--ink-faint) !important; }
    .tech-access-strip .tech-access-item:nth-child(2n) { border-right: none !important; }
    .tech-access-strip.cols-2 { grid-template-columns: 1fr; }
    .tech-access-strip.cols-2 .tech-access-item { border-right: none !important; }
    .steps, .steps.cols-4 { grid-template-columns: 1fr; }
    .step { border-right: none; border-bottom: 1px solid var(--ink-faint); }
    .step:last-child { border-bottom: none; }
    .gallery { grid-template-columns: repeat(2, 1fr); }
    .logo-cell { border-bottom: 1px solid var(--ink-faint); }
    .form { grid-template-columns: 1fr; }
    .fact-list { grid-template-columns: 1fr; }
    .fact-list dt { border-bottom: 0; padding-bottom: 0.2rem; }
    .fact-list dd { padding-top: 0.2rem; }
    .index-table .col-tag { display: none; }
    .index-table .col-date { width: 84px; padding-right: 1rem; }
    .cta-band { padding: 2rem 1.5rem; gap: 1.5rem; }
    .cta-band h2 { font-size: 1.5rem; }
    .cta-band .aside-actions { width: 100%; }
    .cta-band .aside-actions .btn { width: 100%; justify-content: center; }
    .method-item { grid-template-columns: 2.5rem 1fr; }
    .faq .faq-body { padding-right: 0; }
    .section-head { flex-direction: column; align-items: flex-start; gap: 0.75rem; }
    .priority-row { grid-template-columns: minmax(0, 1fr); padding: 1.25rem; }
    .priority-actions .btn { flex: 1 1 100%; justify-content: center; }
    .steps .step, .method-steps .step { padding: 1.5rem 1.25rem; }
    .service-card { padding: 1.5rem; }
    .service-card .icon-tile { margin-bottom: 1.25rem; }
    .page-title { font-size: 1.9rem; }
    .section-title { font-size: 1.5rem; }
    .prose.is-lead > p:first-child { font-size: 1.15rem; }
    .pull-quote p { font-size: 1.05rem; }
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { animation: none !important; transition: none !important; }
    .service-card:hover, .showcase-item:hover .showcase-art, .showcase-item:hover .device, .case-item:hover .case-media img, .nav-pill::before,
    .nav-menu, .nav-menu a { transform: none; }
    .nav-menu-go { display: none; }
    /* Header: no first-visit gathering (its rules need no-preference) and no reading hairline;
       the menu stage, the menus and the mobile panel appear in place; presses do not move.
       Underlines and the icon morphs (instant swaps) still show state; arrows do not fly. */
    .head-actions .btn:active, .menu-toggle:active { transform: none; }
    /* Footer: the scene rests in its final composition and the arrows stay put; underlines,
       the address chip and the copied state still show, just without movement. */
    .footer-scene-art, .footer-copy:active { transform: none; }
    .footer-scene-art { opacity: 1; }
    /* Hero: no arrival (everything is in place at first paint), no light, no drift or drops;
       presses do not move. The underline, the morph (an instant swap) and the cue still show. */
    .hero-light { display: none; }
    .hero-actions .btn-dark:active, .hero-cue:active .hero-cue-ring { transform: none; }
    /* Body: scroll reveals have no hidden state here at all (Scroll reveal), so every section
       is simply there. The SIM loader rests as an even ring of four cards rather than the
       pack bunched at the top of an empty circle. */
    .sim-ring img { transform: rotate(calc(var(--k, 0) * 90deg)) translateY(-125%) rotate(calc(var(--k, 0) * -90deg)); }
    /* Body hover and focus responses keep their colour changes but do not move: no lean on
       the case pictures, no press on the strip arrows or the CTA button. Chevrons swap shape
       instantly. */
    .case-item:focus-within .case-media img, .strip-go:active, .cta-band .btn:active { transform: none; }
    /* Page head and text reveal: nothing is split or hidden (head.js stops first), so every
       title and line is simply there; the chip's chevron still turns into an arrow (an instant
       swap) but does not lean or press. No page transitions, and anchor jumps are instant
       (both rules need no-preference). */
    [data-text-reveal] { opacity: 1; }
    .tr-in { opacity: 1; transform: none; }
}
