/* ==========================================================================
   STON.fi Cross-chain — Home (page-specific)
   Hero, seat counter, waitlist, how-it-works, chains, blog, trust, FAQ.
   Anything reusable lives in components.css / layout.css.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Page loader — full-bleed cosmic overlay that hides the page for ~1s so
   the hero WebGL shader (shader-lines.js) has time to compile and start
   producing frames before it's revealed. A 2px cyan→blue line fills from
   0% to 100% over 1s, then the whole overlay fades out via a chained
   keyframe with animation-delay. Pure CSS — no inline script needed (the
   page CSP forbids them anyway). The element stays in the DOM after the
   fade-out as visibility:hidden + opacity:0 + pointer-events:none, which
   makes it invisible and inert; it doesn't affect layout because it's
   position:fixed. Reduced-motion users skip the loader entirely.

   FOUC guard: the loader markup sits AFTER the floating `.site-header`
   in DOM order (header.php opens body and renders the nav before
   index.php inserts the loader div), so progressive HTML rendering used
   to flash the header for a frame or two before the loader's z-index
   kicked in. We fix that by hiding every direct child of <body> except
   the loader from first paint, then crossfading the content back in as
   the loader fades out (see the
   `body > *:not(.page-loader):not(.site-backdrop):not(.cookie-notice):not(.modal)` rule
   below). This works without JS because the browser blocks the first
   paint until home.css is loaded, so the hide rule is always applied
   before any pixels hit the screen. The dimmer is excluded: a
   forwards-filled reveal would lock it at opacity 1 after the loader
   and leave the page dimmed with no overlay open. The cookie notice is
   excluded because it starts `[hidden]` and is revealed by JS — a
   forwards-filled hide would leave it at opacity 0 after dismiss is
   cleared. It still sits under the loader (z-index 110 vs 9999).
   -------------------------------------------------------------------------- */
.page-loader {
    position: fixed;
    inset: 0;
    z-index: 9999;
    display: grid;
    place-items: center;
    background:
        radial-gradient(60% 60% at 50% 50%, rgba(64, 209, 253, 0.10), transparent 70%),
        radial-gradient(80% 80% at 50% 50%, rgba(1, 128, 255, 0.06),  transparent 75%),
        var(--color-bg);
    pointer-events: auto;
    animation: page-loader-fadeout 350ms var(--ease) 1000ms forwards;
}

/* Hold every body-level surface (header, main, footer) hidden until the
   loader's fill animation completes at 1000ms, then crossfade them in
   over 350ms — finishing exactly as the loader's own fade-out finishes.
   Targeting `body > *` rather than `body` itself keeps the loader div
   visible (it's also a direct body child and is excluded by the
   :not() filter). Visibility is animated as a discrete property and
   flips at the keyframe boundary; opacity carries the crossfade. */
body > *:not(.page-loader):not(.site-backdrop):not(.cookie-notice):not(.modal) {
    opacity: 0;
    visibility: hidden;
    animation: page-loader-reveal-content 350ms var(--ease) 1000ms forwards;
}

@keyframes page-loader-reveal-content {
    to {
        opacity: 1;
        visibility: visible;
    }
}

.page-loader__inner {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-4);
    width: min(320px, 60vw);
}

.page-loader__track {
    position: relative;
    width: 100%;
    height: 2px;
    background: rgba(255, 255, 255, 0.06);
    border-radius: var(--radius-sharp);
    overflow: hidden;
}

/* Gradient line — shares the seat-counter / mission-bar grammar:
   --grad-progress anchored to the track, blue at empty → orange at
   full, glow walks along via color-mix(). Animation drives the
   registered `--progress-fill` custom property (see tokens.css
   @property block) rather than width directly, so the same single
   value feeds width AND background-size and the gradient keeps
   pacing the leading edge through the whole transition. */
.page-loader__fill {
    --progress-fill: 0;
    position: absolute;
    inset: 0 auto 0 0;
    width: calc(var(--progress-fill) * 1%);
    background-image: var(--grad-progress);
    background-repeat: no-repeat;
    background-position: 0 0;
    background-size:
        calc(10000% / max(var(--progress-fill), 0.001)) 100%;
    box-shadow: 0 0 12px color-mix(
        in oklab,
        rgba(80, 167, 255, 0.55)
            calc((100 - var(--progress-fill)) * 1%),
        rgba(208, 98, 51, 0.65));
    animation: page-loader-fill 1000ms var(--ease) forwards;
}

.page-loader__label {
    margin: 0;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--color-cyan);
}
.page-loader__label::before {
    content: "// ";
    opacity: 0.7;
}

@keyframes page-loader-fill {
    from { --progress-fill: 0;   }
    to   { --progress-fill: 100; }
}

/* Final state is held by `forwards` so the overlay stays invisible and
   inert for the rest of the session — no DOM cleanup needed. */
@keyframes page-loader-fadeout {
    to {
        opacity: 0;
        visibility: hidden;
        pointer-events: none;
    }
}

/* Reduced motion — skip the loader entirely so users don't sit on a
   blank screen for a full second when they've asked us not to animate.
   We also have to undo the FOUC-guard hide on body children: the global
   reduced-motion rule in base.css clamps animation-duration to 0.01ms
   but leaves animation-delay intact, so without this override the page
   content would stay hidden for the full 1000ms delay anyway. The
   hero's static fallback (.shader-stage--static) covers the same
   "shader needs warm-up time" case for these users. */
@media (prefers-reduced-motion: reduce) {
    .page-loader { display: none !important; }
    body > *:not(.page-loader):not(.site-backdrop):not(.cookie-notice):not(.modal) {
        opacity: 1 !important;
        visibility: visible !important;
        animation: none !important;
    }
}

/* --------------------------------------------------------------------------
   Hero — centred, dramatic, full-bleed shader backdrop
   The shader stage fills the hero as a backdrop, the headline & CTAs sit
   on top, and a soft radial veil keeps the type legible against the
   brightest pulse area in the centre. The shader is also nudged upward
   so the convergence point of the pulses lands on the "Across chains"
   second headline line, not on the geometric centre of the copy block.
   -------------------------------------------------------------------------- */
.hero {
    position: relative;
    min-height: clamp(620px, 92vh, 940px);
    padding-top: clamp(32px, 5vw, 72px);
    padding-bottom: clamp(40px, 5vw, 80px);
    overflow: hidden;
    isolation: isolate;
    display: grid;
    /* `align-items: center` only — `place-items: center` would also set
       `justify-items`, which would collapse the .container child to its
       intrinsic width instead of letting it span the 1130px page rail. */
    align-items: center;
    text-align: center;
}

/* When the hero is the first section after the floating glass nav, stretch
   it up under the header so the cosmic gradient bleeds to the top of the
   page instead of leaving a flat strip of --color-bg above it. */
main > .hero:first-child {
    margin-top: calc(-1 * (var(--header-h) + var(--header-offset)));
    padding-top: calc(var(--header-h) + var(--header-offset) + clamp(32px, 5vw, 72px));
}

/* Phones: the 92vh hero floor pushed the seat-counter fully below
   the fold. Shorten it so the "live" ticket strip peeks into the
   first screen — a nudge to scroll, not a second hero. */
@media (max-width: 720px) {
    .hero {
        min-height: clamp(520px, 78vh, 720px);
        padding-bottom: 24px;
    }
    .hero__inner {
        padding-block: clamp(24px, 4vw, 48px);
    }
    .seat-counter {
        padding-top: var(--sp-5);
    }
}

/* Cosmic glow + soft starfield (pure CSS, no asset needed). 
.hero::before {
    content: "";
    position: absolute;
    inset: -10% -10% 0 -10%;
    z-index: -1;
    background:
        radial-gradient(60% 50% at 18% 18%, rgba(64, 209, 253, 0.18), transparent 70%),
        radial-gradient(55% 60% at 82% 78%, rgba(1, 128, 255, 0.20), transparent 70%),
        radial-gradient(80% 60% at 50% 100%, rgba(32, 40, 73, 0.55), transparent 70%);
    pointer-events: none;
}
.hero::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background-image:
        radial-gradient(1px 1px at 12% 28%, rgba(255, 255, 255, 0.55) 50%, transparent 51%),
        radial-gradient(1px 1px at 73% 14%, rgba(255, 255, 255, 0.40) 50%, transparent 51%),
        radial-gradient(1px 1px at 38% 62%, rgba(255, 255, 255, 0.35) 50%, transparent 51%),
        radial-gradient(1px 1px at 88% 48%, rgba(255, 255, 255, 0.50) 50%, transparent 51%),
        radial-gradient(1px 1px at 22% 84%, rgba(255, 255, 255, 0.30) 50%, transparent 51%),
        radial-gradient(1px 1px at 58% 92%, rgba(255, 255, 255, 0.45) 50%, transparent 51%),
        radial-gradient(1px 1px at 91% 80%, rgba(255, 255, 255, 0.30) 50%, transparent 51%),
        radial-gradient(1px 1px at 6%  72%, rgba(255, 255, 255, 0.40) 50%, transparent 51%);
    background-size: 100% 100%;
    pointer-events: none;
}
    */

/* ---- Backdrop stage: shader + veil, sits behind the text content. ----
   The shader fills the entire hero box. Its convergence point lands close
   to the "Across chains" headline line by geometry: the first-child hero
   has ~84px of extra padding-top (to clear the floating glass nav), which
   shifts the visible copy block down relative to the hero box. The shader
   centre therefore sits ~30-40px above the copy centre — right around
   the second headline line. No transform needed.                         */
.hero__stage {
    position: absolute;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
    z-index: 0;
}

/* Soft radial veil between shader and text — wider and stronger than a
   typical scrim because the shader's centre pulse is intensely bright
   and the headline sits right on top of it. */
.hero__veil {
    position: absolute;
    inset: 0;
    background:
        radial-gradient(56% 44% at 50% 50%, rgba(2, 2, 4, 0.92), rgba(2, 2, 4, 0.42) 60%, transparent 82%),
        linear-gradient(180deg, rgba(17, 17, 19, 0.55) 0%, transparent 18%, transparent 80%, rgba(17, 17, 19, 0.65) 100%);
    pointer-events: none;
}

/* ---- Text overlay: single centred column, sits above the stage. ---- */
.hero__inner {
    position: relative;
    z-index: 1;
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--sp-6);
    align-items: center;
    padding-block: clamp(40px, 6vw, 100px);
}

.hero__copy {
    max-width: 60ch;
    margin: 0 auto;
    text-align: center;
}

/* Eyebrow over a busy backdrop — the soft cyan→blue-soft gradient
   already reads as a brighter beacon than the default fg-3 grey, so
   no colour override is needed. A subtle dark halo keeps the small
   mono glyphs crisp against the brightest pulses of the shader; the
   shadow follows the glyph silhouettes (text-shadow renders below
   the transparent text-fill, not behind the bounding box). */
.hero .eyebrow {
    text-shadow: 0 1px 8px rgba(2, 4, 12, 0.65);
}

.hero__headline {
    margin: 0 0 var(--sp-5);
    font-family: var(--font-display);
    font-weight: 500;
    font-size: clamp(48px, 5vw + 18px, 88px);
    line-height: 1.02;
    letter-spacing: -1.2px;
    color: var(--color-fg);
    text-shadow: 0 2px 24px rgba(2, 4, 12, 0.55);
}
/* Block lines, not inline-block: shrink-to-fit was wider than the
   phone column and overflowed one side or the other as the type
   size changed. Each span is a full-width line; text-align:center
   on .hero centres the glyphs without growing the box. */
.hero__headline span {
    display: block;
}

/* Lede sits over the brightest part of the shader — full-white +
   text-shadow so it doesn't get eaten by cyan pulses. */
.hero__lede {
    margin: 0 auto var(--sp-7);
    font-family: var(--font-body);
    font-size: 17px;
    line-height: 1.55;
    color: var(--color-fg);
    text-shadow: 0 1px 12px rgba(2, 4, 12, 0.65);
    max-width: 56ch;
}

/* Desktop: two-line lede — chains sentence, then product sentence.
   The <br> is suppressed below 900px so phones still wrap naturally. */
.hero__lede-break {
    display: none;
}

@media (min-width: 900px) {
    .hero__copy {
        max-width: min(52rem, 100%);
    }
    .hero__lede {
        max-width: none;
    }
    .hero__lede-break {
        display: inline;
    }
}

.hero__ctas {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--sp-4);
}

@media (max-width: 480px) {
    /* Keep "Across chains." on one line on phones instead of wrapping
       the gradient. 10.5vw at 402px ≈ 42px, which fits the 362px rail. */
    .hero__headline {
        font-size: clamp(36px, 10.5vw, 48px);
    }
    .hero__ctas .btn { flex: 1 1 100%; }
}

/* ---- Shader stage primitives ---------------------------------------------
   Used by the WebGL shader animation in assets/js/shader-lines.js.
   `.shader-stage--bleed` is the one variant the page uses today: a full-
   bleed backdrop that fills the entire hero box. The bare `.shader-stage`
   rule is kept generic in case other pages want a contained version later.
   ------------------------------------------------------------------------ */
.shader-stage {
    position: relative;
    isolation: isolate;
}

/* Full-bleed: stretches edge-to-edge inside the hero stage. The shader's
   radial UV (length(uv) with min-axis normalisation) keeps the rings
   round even at non-square aspect ratios, so a rectangle works fine. */
.shader-stage--bleed {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}

.shader-stage__canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
    z-index: 1;
    /* base.css caps replaced media at max-width: 100%. A 2×-dpr
       canvas bitmap is wider than the hero; on iOS that intrinsic
       size still widens the document even with overflow:hidden on
       .hero, which is the slight left/right pan. */
    max-width: none;
}

/* Cosmic-glow fallback for no-WebGL / reduced-motion. Brand-tinted so
   the section still feels like a STON.fi hero when the shader is off. */
.shader-stage--static {
    background:
        radial-gradient(60% 60% at 50% 50%, rgba(64, 209, 253, 0.18), transparent 70%),
        radial-gradient(80% 80% at 50% 50%, rgba(1, 128, 255, 0.10),  transparent 75%);
    overflow: hidden;
}
.shader-stage--static .shader-stage__canvas {
    display: none;
}

/* --------------------------------------------------------------------------
   Seat counter
   -------------------------------------------------------------------------- */
.seat-counter {
    padding: var(--sp-7) 0;
    text-align: center;
    background: rgba(255, 255, 255, 0.02);
    border-top: var(--hairline);
    border-bottom: var(--hairline);
}

.seat-counter__inner {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-4);
}

.seat-counter__headline {
    margin: 0;
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 10px 12px;
    font-family: var(--font-body);
    font-size: 16px;
    line-height: 1.4;
    color: var(--color-fg-2);
}
.seat-counter__headline strong {
    font-family: var(--font-mono);
    font-weight: 500;
    color: var(--color-cyan);
}

.seat-counter__meter {
    width: 100%;
    max-width: 720px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-2);
}
.seat-counter .progress {
    width: 100%;
}
.seat-counter__meter-label {
    margin: 0;
    font-family: var(--font-mono);
    font-size: 11px;
    line-height: 1.2;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--color-fg-3);
}

.seat-counter__bonus {
    margin: 0;
    font-family: var(--font-mono);
    font-size: 13px;
    line-height: 1.4;
    letter-spacing: 0.04em;
    text-transform: lowercase;
    color: var(--color-fg-2);
}
.seat-counter__bonus strong {
    font-weight: 600;
    color: var(--color-cyan);
}
.seat-counter__caption {
    margin: 0;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--color-fg-4);
}

.seat-counter--flight {
    padding-block: var(--sp-6);
}
.seat-counter--flight .seat-counter__inner {
    gap: var(--sp-3);
}

/* --------------------------------------------------------------------------
   Waitlist — stepped form
   -------------------------------------------------------------------------- */
.waitlist__form {
    display: flex;
    flex-direction: column;
    gap: var(--sp-5);
    max-width: 720px;
    margin: 0 auto;
}

.step {
    position: relative;
    padding: var(--sp-7);
    background-color: var(--color-bg-2);
    background-image: var(--grad-card-glow);
    border: var(--hairline);
    border-radius: var(--radius-sharp);
}
.step:nth-child(even) {
    background-image: var(--grad-card-glow-inv);
}

.waitlist__form [data-waitlist-submit] .btn__break {
    display: none;
}

@media (max-width: 540px) {
    /* 40px card gutters leave ~280px for a nowrap 14px mono label
       plus `[ ]` — the miles suffix wraps onto its own line via
       .btn__break so we don't split mid-phrase. */
    .step { padding: var(--sp-5); }
    .waitlist__form [data-waitlist-submit] {
        height: auto;
        min-height: var(--btn-h);
        padding: 12px 14px;
        font-size: 13px;
        line-height: 1.35;
    }
    .waitlist__form [data-waitlist-submit] .btn__break {
        display: block;
    }
}

.step__head {
    margin-bottom: var(--sp-4);
}

.step__index {
    display: inline-block;
    margin-bottom: var(--sp-3);
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--color-fg-3);
}
.step__index::before { content: "// "; color: var(--color-cyan); }

.step__title {
    margin: 0;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--sp-3);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 20px;
    line-height: 1.2;
    color: var(--color-fg);
}
.step__optional {
    /* now styled by `.pill .pill--mute` in markup */
}

.step__trust a {
    color: var(--color-fg-3);
    border-bottom: 1px dashed rgba(255, 255, 255, 0.28);
    padding-bottom: 1px;
}
.step__trust a:hover {
    color: var(--color-fg-2);
    border-bottom-color: rgba(255, 255, 255, 0.5);
}

/* Step body copy — sentence-case prose used inside a step host (today
   only the "pair the Telegram bot" step). Larger and warmer than
   .step__trust / .step__chains, which are mono micro-labels. Kept just
   slightly tighter than default body so the step card stays compact. */
.step__lede {
    margin: 0 0 var(--sp-5);
    font-size: 14px;
    line-height: 1.55;
    color: var(--color-fg-3);
}

/* --- Step state machine ----------------------------------------------------
   Each .step has data-state="idle|connected|error" set by wallet.js.
   Child slots opt into one or more states via data-wallet-show="idle error"
   (space-separated for multi-state). Anything not in the current step's
   state is hidden by CSS.

   Driving this from CSS instead of the JS-toggled `hidden` attribute is
   important: `.btn`'s own `display: inline-flex` declaration has the
   same specificity as the UA stylesheet's `[hidden] { display: none }`
   and trumps it (later declarations win at equal specificity). The
   attribute selector below sits on the step host, so it has higher
   specificity than any standalone class rule and wins decisively. */
[data-wallet-step][data-state="idle"]      [data-wallet-show]:not([data-wallet-show~="idle"])      { display: none; }
[data-wallet-step][data-state="connected"] [data-wallet-show]:not([data-wallet-show~="connected"]) { display: none; }
[data-wallet-step][data-state="error"]     [data-wallet-show]:not([data-wallet-show~="error"])     { display: none; }

/* Step-card connected ✓ — sits next to the title once the wallet is in. */
.step__check {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    margin-left: 4px;
    color: var(--color-success);
    background: rgba(4, 218, 141, 0.12);
    border: 1px solid rgba(4, 218, 141, 0.40);
    border-radius: 50%;
    font-family: var(--font-mono);
    font-weight: 500;
    font-size: 11px;
    line-height: 1;
}

/* Inline error microcopy under the connect button. */
.step__error {
    margin: var(--sp-3) 0 0;
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: 0.02em;
    text-transform: lowercase;
    color: rgb(255, 138, 138);
}
.step__error a {
    color: inherit;
    border-bottom: 1px dashed currentColor;
    padding-bottom: 1px;
}

/* Supported-chains caption beneath the EVM connect button. */
.step__chains {
    margin: var(--sp-3) 0 0;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: lowercase;
    color: var(--color-fg-3);
}

.waitlist__disclaimer {
    margin: var(--sp-2) 0 0;
    text-align: center;
    font-family: var(--font-mono);
    font-size: 11px;
    line-height: 1.5;
    letter-spacing: 0.06em;
    color: var(--color-fg-4);
}
.waitlist__disclaimer a {
    color: var(--color-fg-3);
    border-bottom: 1px dashed rgba(255, 255, 255, 0.28);
    padding-bottom: 1px;
}
.waitlist__disclaimer a:hover {
    color: var(--color-fg-2);
    border-bottom-color: rgba(255, 255, 255, 0.5);
}

/* Submit-gating helper — shows below the confirm button while disabled. */
.waitlist__helper {
    margin: var(--sp-3) 0 0;
    text-align: center;
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: 0.04em;
    text-transform: lowercase;
    color: var(--color-fg-3);
}

/* Manual "Refresh status" affordance — surfaces below the connect CTA
   after the user has tapped Connect at least once (wallet.js
   showRecheckHost()). Visually subordinate to the primary CTA: same
   secondary button style as elsewhere on the page, plus a one-line
   trust microcopy line explaining the auto-refresh fallback. Layout
   stays block so the [hidden] attribute (UA stylesheet's display:none)
   wins by default — matching the rest of the waitlist children that
   the state machine flips via `hidden` rather than display rules. */
.waitlist__recheck { margin: var(--sp-5) 0 0; }
.waitlist__recheck .step__trust {
    margin: var(--sp-2) 0 0;
    text-align: center;
}

/* Paired-state success line — sits directly below the wallet-card-style
   Telegram confirmation in step 2's `connected` layout. Deliberately
   light styling: the wallet-card above already carries the green dot +
   "Telegram paired" + handle, so a heavy green-bordered block here
   would read as a duplicated confirmation and crowd the panel.
   Matching the .step__trust microcopy weight keeps the visual rhythm
   of the panel honest — wallet-card = strong confirmation, trust line
   = soft reassurance. */
.waitlist__success {
    margin: var(--sp-3) 0 0;
    text-align: center;
    font-family: var(--font-mono);
    font-size: 12px;
    line-height: 1.5;
    letter-spacing: 0.04em;
    text-transform: lowercase;
    color: var(--color-success);
}

/* --------------------------------------------------------------------------
   How it works — three cards, alternating radial direction.
   -------------------------------------------------------------------------- */
.how__grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--sp-5);
}

@media (min-width: 768px) {
    .how__grid {
        grid-template-columns: repeat(3, 1fr);
        gap: var(--sp-6);
    }
}

.how__card {
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
}
.how__grid .how__card:nth-child(2) {
    background-image: var(--grad-card-glow-inv);
}

.how__step {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    margin-bottom: var(--sp-4);
    font-family: var(--font-mono);
    font-weight: 500;
    font-size: 18px;
    color: var(--color-cyan);
    background: rgba(64, 209, 253, 0.06);
    border: 1px solid rgba(64, 209, 253, 0.30);
    border-radius: var(--radius-sharp);
    letter-spacing: 0;
}

/* --------------------------------------------------------------------------
   Supported chains — text overlay on a Three.js GLSL "hills" backdrop
   (assets/js/glsl-hills.js). The shader renders a perlin-displaced plane
   as a slow scrolling topographic horizon — a literal landscape under
   the "Where you can fly direct" headline. Section min-height is
   generous so the horizon has room; the chip row and headline sit on
   top centred, with a vertical fade-to-bg gradient at the edges so the
   panel reads as a contained scene rather than a hard rectangle.
   -------------------------------------------------------------------------- */
.chains {
    position: relative;
    overflow: hidden;
    isolation: isolate;
    min-height: clamp(440px, 56vh, 640px);
    display: grid;
    align-items: center;
}

.chains__stage {
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    /* Mask the canvas with soft top + bottom fades. The bottom stop
       finishes well before the section boundary so the hills are
       already fully dissolved into the page bg by the time the next
       section begins — no visible horizontal seam. */
    -webkit-mask-image: linear-gradient(180deg,
        transparent 0%,
        #000 22%,
        #000 62%,
        transparent 92%);
            mask-image: linear-gradient(180deg,
        transparent 0%,
        #000 22%,
        #000 62%,
        transparent 92%);
}
.chains__canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
    max-width: none;
}

/* Ambient cyan glow under the horizon — anchors the gray hills to the
   brand palette without touching the shader. Sits below the canvas.
   Centred in the upper-middle of the section and tightened vertically
   so it can't reach the section bottom and create a glow-vs-no-glow
   luminance step at the boundary with the next section. */
.chains::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background:
        radial-gradient(58% 38% at 50% 58%, rgba(64, 209, 253, 0.10), transparent 72%),
        radial-gradient(52% 34% at 50% 32%, rgba(1, 128, 255, 0.06),  transparent 75%);
    pointer-events: none;
}

/* Bottom blend-out to page bg — forces the lower band of the section
   to pure --color-bg so that any residual canvas/glow content fades
   smoothly into the next section instead of cutting off at a hard
   horizontal line. The same trick at the top would help too if a
   visible seam ever appears against the section above. */
.chains::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 22%;
    z-index: 0;
    pointer-events: none;
    background: linear-gradient(180deg, transparent 0%, var(--color-bg) 100%);
}

/* Stack the eyebrow / headline / chip row above the canvas + blend-out. */
.chains > .container {
    position: relative;
    z-index: 1;
}

/* Chip row sits centred near the bottom third of the section so the
   horizon line passes behind it and the eyebrow/headline cluster gets
   the upper sky area. */
.chains__row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: var(--sp-3);
}
.chains__more {
    margin-left: var(--sp-2);
    font-family: var(--font-mono);
    font-size: 14px;
    text-transform: lowercase;
    color: var(--color-fg-3);
}

/* Static fallback if WebGL or the three.js CDN is unavailable — keeps a
   subtle horizon-ish gradient so the section doesn't read as broken. */
.hills-stage--static {
    background:
        linear-gradient(180deg, transparent 0%, transparent 55%, rgba(255, 255, 255, 0.04) 70%, rgba(255, 255, 255, 0.06) 85%, transparent 100%),
        radial-gradient(80% 50% at 50% 78%, rgba(64, 209, 253, 0.08), transparent 70%);
}
.hills-stage--static .chains__canvas { display: none; }

/* --------------------------------------------------------------------------
   Blog cards
   -------------------------------------------------------------------------- */
.blog__grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--sp-5);
}
@media (min-width: 768px) {
    .blog__grid {
        grid-template-columns: repeat(3, 1fr);
        gap: var(--sp-6);
    }
}

/* Each card is now a single anchor (the whole tile is clickable), so we
   reset the anchor defaults here: inherit the card's text color, drop the
   underline, and keep the flex column layout the article used to use. */
.blog__card {
    display: flex;
    flex-direction: column;
    gap: 0;
    padding: var(--sp-5);
    color: inherit;
    text-decoration: none;
}
.blog__card:focus-visible {
    outline: 2px solid var(--color-cyan);
    outline-offset: 2px;
}
.blog__grid .blog__card:nth-child(2) {
    background-image: var(--grad-card-glow-inv);
}

.blog__cover {
    position: relative;
    width: 100%;
    /* Matches the intrinsic aspect ratio of the blog cover source images
       in public_html/assets/img/blog_article_*.webp (currently 1280x671).
       Keep this in sync when those source files are replaced, or
       object-fit: cover on the img below will crop edge elements like
       the "STON.fi" wordmark. */
    aspect-ratio: 1280 / 671;
    margin-bottom: var(--sp-5);
    background:
        radial-gradient(80% 90% at 30% 30%, rgba(64, 209, 253, 0.20), transparent 70%),
        radial-gradient(70% 90% at 80% 70%, rgba(1, 128, 255, 0.25),  transparent 70%),
        var(--color-bg-3);
    border: var(--hairline);
    border-radius: var(--radius-sharp);
    overflow: hidden;
    display: flex;
    align-items: flex-end;
    justify-content: flex-start;
    padding: 14px;
}
/* Single overlay above the cover photo and below the tag:
   - starlight sparkles (the original 4 white radials)
   - cyan / blue radial glow that reintroduces the page's cosmic accent
     palette on top of whatever the photo's native colors are
   - bottom vignette so the .blog__cover-tag stays readable on bright
     photos. Tag still wins the stack via z-index: 1. */
.blog__cover::after {
    content: "";
    position: absolute;
    inset: 0;
    background-image:
        radial-gradient(1px 1px at 20% 30%, rgba(255, 255, 255, 0.45) 50%, transparent 51%),
        radial-gradient(1px 1px at 78% 24%, rgba(255, 255, 255, 0.30) 50%, transparent 51%),
        radial-gradient(1px 1px at 60% 70%, rgba(255, 255, 255, 0.40) 50%, transparent 51%),
        radial-gradient(1px 1px at 30% 80%, rgba(255, 255, 255, 0.25) 50%, transparent 51%),
        radial-gradient(80% 90% at 30% 30%, rgba(64, 209, 253, 0.18), transparent 65%),
        radial-gradient(70% 90% at 80% 70%, rgba(1, 128, 255, 0.22),  transparent 65%),
        linear-gradient(180deg, rgba(8, 12, 20, 0) 45%, rgba(8, 12, 20, 0.55) 100%);
    pointer-events: none;
}
/* Cover photo: object-fit: cover fills the 16:9 frame (these are
   landscape blog covers, not the small motif SVGs the earlier rule
   was sized for). The filter pulls bright saturated photos down to
   the page's dim cyan/blue palette so the cards stop reading as
   bright blue rectangles against the rgb(27,27,31) page bg. */
.blog__cover img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    pointer-events: none;
    filter: brightness(0.55) saturate(0.65) contrast(1.05) hue-rotate(-6deg);
}

.blog__meta {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    margin-bottom: var(--sp-3);
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--color-fg-3);
}
.blog__time { color: var(--color-fg-3); }

.blog__card .card__title {
    font-size: 20px;
    margin-bottom: var(--sp-4);
}

/* .blog__read is now a decorative span inside the .blog__card anchor (it
   used to be the link). The hover state therefore triggers on the parent
   card so the chevron still animates when the user hovers anywhere on the
   tile, not only over the "read article" text. */
.blog__read {
    margin-top: auto;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-family: var(--font-mono);
    font-size: 14px;
    text-transform: lowercase;
    color: var(--color-cyan);
    transition:
        gap        var(--dur-hover) var(--ease),
        color      var(--dur-hover) var(--ease);
}
.blog__card:hover .blog__read,
.blog__card:focus-visible .blog__read {
    color: var(--color-cyan-glow);
    gap: 12px;
}

.blog__see-all {
    margin: var(--sp-7) 0 0;
    text-align: center;
    font-family: var(--font-mono);
    font-size: 14px;
    text-transform: lowercase;
}
.blog__see-all a {
    color: var(--color-fg-2);
    transition: color var(--dur-hover) var(--ease);
}
.blog__see-all a:hover { color: var(--color-cyan); }

/* --------------------------------------------------------------------------
   Trust signals
   -------------------------------------------------------------------------- */
.trust { text-align: center; }

.trust .eyebrow {
    display: inline-block;
    margin-bottom: var(--sp-5);
}

.trust__line {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 500;
    font-size: clamp(20px, 1.6vw + 12px, 28px);
    line-height: 1.3;
    letter-spacing: -0.3px;
    color: var(--color-fg);
}

/* --------------------------------------------------------------------------
   FAQ accordion
   -------------------------------------------------------------------------- */
.faq__list {
    border-top: var(--hairline);
}

.faq__item {
    border-bottom: var(--hairline);
    /* Horizontal inset so the 45°-rotated chevron diamond stays inside
       the row (a 28px square grows ~6px per side when rotated) and the
       question/answer aren't flush against the hairline. */
    padding-left: var(--sp-5);
    padding-right: var(--sp-5);
    transition: background var(--dur-hover) var(--ease);
}
.faq__item:has(.faq__question[aria-expanded="true"]) {
    background: rgba(64, 209, 253, 0.03);
}

.faq__question {
    display: flex;
    width: 100%;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-5);
    padding: var(--sp-5) 0;
    font-family: var(--font-display);
    font-weight: 500;
    font-size: 17px;
    line-height: 1.4;
    text-align: left;
    color: var(--color-fg);
    background: transparent;
    transition: color var(--dur-hover) var(--ease);
}
.faq__question:hover { color: var(--color-cyan); }
.faq__question[aria-expanded="true"] { color: var(--color-cyan); }

.faq__chevron {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    font-family: var(--font-mono);
    font-size: 18px;
    line-height: 1;
    color: var(--color-fg-3);
    border: var(--hairline);
    border-radius: var(--radius-sharp);
    transition:
        transform     var(--dur-hover) var(--ease),
        color         var(--dur-hover) var(--ease),
        border-color  var(--dur-hover) var(--ease);
}
.faq__question:hover .faq__chevron {
    color: var(--color-cyan);
    border-color: var(--color-stroke-2);
}
.faq__question[aria-expanded="true"] .faq__chevron {
    transform: rotate(45deg);
    color: var(--color-cyan);
    border-color: rgba(64, 209, 253, 0.40);
}

.faq__answer {
    overflow: hidden;
    padding: 0 0 var(--sp-5);
    font-family: var(--font-body);
    font-size: 16px;
    line-height: 1.6;
    color: var(--color-fg-2);
    max-width: 64ch;
}
.faq__answer p { margin: 0; }
.faq__answer a {
    color: var(--color-cyan);
    border-bottom: 1px dashed rgba(64, 209, 253, 0.4);
    padding-bottom: 1px;
}
.faq__answer a:hover {
    border-bottom-color: var(--color-cyan);
}
