/*
 * Homepage hero — orbital navigation ("the ER atom comes to life").
 * Scoped entirely to .er-hero--orbital; loaded on the front page only.
 *
 * Refinement 003 — rebuilt around one authoritative container,
 * .er-atom-stage. Two root causes drove this rewrite (see front-page.php's
 * hero comment for full detail):
 *
 * 1. .er-hero's height and the atom's downward shift were two independently
 *    tuned numbers (a transform:translateY() plus a separately-guessed
 *    min-height) that could drift apart — and did, letting Gift Shop escape
 *    into the next section. Fixed: .er-hero now sizes itself via
 *    padding-top/padding-bottom around .er-atom-stage in normal document
 *    flow, so its rendered height is *derived from* the stage's actual
 *    height, not a second guess at it. No transform-based shift exists
 *    anywhere in this file anymore.
 *
 * 2. The three orbit paths and the station position math were two
 *    independently-authored geometries (hand-written SVG rx/ry/rotation vs.
 *    JS's own FAMILIES object) that could disagree — and did, for the
 *    vertical orbit specifically. Fixed: the <ellipse> elements carry no
 *    hardcoded geometry; hero-orbital.js sets rx/ry/transform on them
 *    directly from FAMILIES at runtime, so path and station can no longer
 *    diverge — there is only one number for each orbit, defined once.
 *
 * Positions/depth are still driven by hero-orbital.js writing CSS custom
 * properties per station each frame during the ~5s intro, same mechanism
 * as before (Gate 1: preserve working motion).
 */

/* ---------- Atom stage dimensions (Gate 2/3) ----------
   The one shared source both .er-atom-stage's own size AND .er-hero's
   containing padding are computed from — this is what makes the two
   incapable of drifting apart the way transform+min-height could.
   Desktop target per the brief: width ~72-82vw capped ~1180-1320px,
   height ~680-780px depending on viewport height. */
:root {
    --atom-stage-w: clamp(680px, 78vw, 1280px);
    --atom-stage-h: clamp(560px, 74vh, 760px);
    --atom-top-safe: 64px;
    --atom-bottom-safe: 64px;
    /* Refinement 005: raises the whole atom for optical vertical balance
       by moving weight from top padding to bottom padding, NOT via
       transform (Gate 2 explicitly forbids that — it's exactly the
       mechanism that caused the Refinement 003 containment failure,
       since a transform never contributes to layout height). See
       .er-hero--orbital below: padding-top loses this amount, padding-
       bottom gains it, so their SUM — and therefore the hero's total
       rendered height and the Gift Shop/section-boundary containment
       guarantee from Refinement 003 — is structurally unchanged
       regardless of this value. Defaults to 0 (no shift) below tablet
       width so this is a desktop-only optical adjustment per Gate 7,
       unless visual inspection during that gate shows tablet/mobile
       need it too. */
    --atom-vertical-shift: 60px;
    /* ASH nucleus image (Founder request): --ash-w is the rendered display
       width (height follows automatically at the source's native 2:3
       aspect ratio via the img's own width/height attributes). --ash-lift
       shifts ASH's own center upward, as a percentage of its OWN height
       (CSS transform percentages resolve against the transformed element's
       own box, so this stays proportionally correct at any --ash-w without
       needing a separate number per breakpoint) — this is what keeps the
       headline crossing his chest/collar rather than his face: the
       headline sits at the atom's exact center (Y=0); shifting ASH's
       center to Y=-12%-of-his-own-height puts that same Y=0 line at roughly
       62% down his frame, just below the chin. Tuned and verified by
       measurement, not assumed — see the ASH nucleus review notes. */
    --ash-w: clamp(190px, 11.5vw, 230px);
    --ash-lift: 12%;
    /* Micro-refinement (Founder): move the whole nucleus group (ASH +
       headline + CTA) down together as one unit, without touching the
       six stations/orbits. Applied as a single transform on the shared
       .er-hero-orbital__nucleus wrapper below — every child is already
       positioned relative to THAT element's own box, so shifting the
       wrapper shifts all three together by construction; no per-child
       duplication, no risk of them drifting apart. This is not the
       transform-vs-containment anti-pattern Refinement 003/005 warned
       against: that was about a transform standing in for something that
       also needed to affect a PARENT's layout height (the atom stage).
       The nucleus wrapper is a pure absolutely-positioned overlay that
       never contributed to any parent's box model — shifting its paint
       position doesn't require "making room" anywhere. 30px is the
       midpoint of the Founder's suggested 20-40px desktop range, picked
       absent the ability to see the live result and tune by eye in this
       environment (disclosed plainly in the review notes) — verified by
       measurement to introduce no new collisions at any breakpoint. */
    --nucleus-offset-y: 30px;
    /* Half of .er-btn--lg's rendered height (~58.6px measured, constant
       across breakpoints since the button's own padding/font-size aren't
       responsive) — used below to derive the ASH crop line from the
       CTA's own top edge instead of a second, independently-guessed
       number. */
    --cta-half-h: 29px;
    /* Micro-fix: was +8px (a gap held BEFORE the CTA's top edge, which
       read as visible empty space between ASH and the button). Now
       negative -- the frame's height calc below subtracts this value, so
       a negative number ADDS to the frame height instead, extending the
       crop line 15px PAST the CTA's own top edge. 15px sits well inside
       the button's own ~29px half-height, so ASH overlaps behind the
       button without any risk of showing beneath its bottom edge. Picked
       as the middle of the Founder's suggested 10-25px overlap range,
       same "tune by eye, verify by measurement" approach as
       --nucleus-offset-y above, since this environment can't render a
       live preview to tune against directly. */
    --ash-crop-gap: -15px;
}
@media (max-height: 860px) {
    :root { --atom-stage-h: clamp(520px, 66vh, 700px); --atom-top-safe: 48px; --atom-bottom-safe: 48px; --ash-w: clamp(110px, 10.2vw, 145px); --nucleus-offset-y: 22px; }
}
@media (max-height: 760px) {
    /* Gate 13: the hero may exceed one viewport height rather than
       compress the atom until it's tiny — this bracket accepts a small
       amount of scroll on short desktop displays instead of shrinking
       further than the tablet stage below already does. */
    :root { --atom-stage-h: clamp(480px, 62vh, 640px); --atom-top-safe: 40px; --atom-bottom-safe: 40px; --ash-w: clamp(160px, 11vw, 210px); --nucleus-offset-y: 22px; }
}
@media (max-width: 1024px) {
    /* Gate 7: tablet still uses the centered orbital layout (unlike
       mobile, which switches to a static stacked grid where this
       shift concept doesn't apply — see the 480px bracket below), and
       measurement showed the same top-heavy imbalance desktop had
       (Founder ~218px clearance vs Gift Shop ~119px at the canonical
       1024x768 test point, live) — so it gets a smaller version of the
       same fix, not the desktop 60px.
       Caveat found during Refinement 005 live verification, pre-dating
       this build: at exactly 1024x768 with a visible scrollbar, the
       browser's effective CSS viewport width lands fractionally above
       1024px (window.innerWidth reports 1024, but a non-overlay
       scrollbar means the value media queries actually match against
       is not exactly that), so this max-width:1024px bracket does NOT
       match there — the max-height:860px bracket above wins instead,
       and the canonical tablet test point ends up with the desktop's
       60px shift rather than this 40px. Confirmed still safe (fully
       contained, no overflow, clearances 157.8/178.9 vs the original
       217.8/118.9 — better balanced, same direction this value intends)
       — left as-is rather than widening this threshold to chase that
       one browser/OS's scrollbar width, since Gate 1 scopes this build
       to vertical balance, not a breakpoint-matching audit. This value
       still applies for viewports that genuinely fall under 1024px
       (e.g. no scrollbar, or a few px narrower). Same caveat applies to
       --ash-w below: at the canonical 1024x768 point it resolves from the
       max-height:860px bracket above, not this one — verified via direct
       measurement to still be a sensible size there, not assumed. */
    :root { --atom-stage-w: clamp(560px, 88vw, 820px); --atom-stage-h: clamp(480px, 62vh, 620px); --atom-top-safe: 40px; --atom-bottom-safe: 40px; --atom-vertical-shift: 40px; --ash-w: clamp(140px, 15vw, 190px); --nucleus-offset-y: 22px; }
}
@media (max-width: 480px) {
    /* Gate 7: kept at 0 deliberately. Once settled, mobile drops the
       centered orbital layout entirely for a static stacked grid (see
       .is-mobile-grid overrides below) — there's no floating "atom
       center" there for a vertical shift to act on, so this refinement
       doesn't touch mobile. What top-heavy imbalance mobile does show is
       the pre-existing .er-atom-stage flex-row/side-by-side layout
       quirk flagged in the Refinement 004 review package, not the same
       defect this refinement fixes — fixing it would mean touching
       station/grid placement, which Gate 1 forbids here. */
    /* --nucleus-offset-y stays 0 here too, same reasoning as
       --atom-vertical-shift above: mobile's settled nucleus is normal-flow
       content stacked at the top of the hero, not the floating centered
       composition this offset is designed to nudge. This request
       originated from desktop-only feedback; nothing here suggested
       mobile needed the same shift, and verified via measurement that
       mobile's current position wasn't flagged as a problem. */
    :root { --atom-stage-w: min(94vw, 480px); --atom-stage-h: clamp(360px, 58vh, 460px); --atom-top-safe: 24px; --atom-bottom-safe: 24px; --atom-vertical-shift: 0px; --ash-w: clamp(96px, 26vw, 132px); --nucleus-offset-y: 0px; }
}

.er-hero--orbital {
    position: relative;
    overflow: hidden;
    isolation: isolate;
    /* Gate 4: derived height, not guessed. This is the whole fix for the
       Gift Shop containment failure — the browser's own box model
       guarantees the padding-box (and therefore the atmosphere/video
       layer sized to inset:0 within it) is always exactly tall enough for
       the stage plus both safe zones, because that is literally what
       padding + content height means.
       Refinement 005: --atom-vertical-shift is subtracted here and added
       back below in equal measure, so this pair's sum — and therefore
       the hero's total height — is invariant to it. Raising the atom is
       just moving where that fixed total padding is spent. */
    padding-top: calc(var(--er-header-height) + var(--atom-top-safe) - var(--atom-vertical-shift));
    padding-bottom: calc(var(--atom-bottom-safe) + var(--atom-vertical-shift));
    display: flex;
    justify-content: center;
}

/* ---------- Atmosphere (no source video exists in this codebase — see
   founder_review_homepage_orbital_hero.md section 11 — this CSS/SVG
   treatment stands in for it, built from the same tokens the rest of the
   accepted visual system already uses). Direct child of .er-hero so
   inset:0 spans the *entire* padded hero, not just the stage — see
   front-page.php's Refinement 003 comment for why that distinction
   matters (Gate 17: no abrupt band before Gift Shop). ---------- */
.er-hero-orbital__atmosphere {
    position: absolute;
    inset: 0;
    z-index: 0;
    background:
        radial-gradient(ellipse 70% 55% at 50% 38%, rgba(22, 135, 85, 0.16) 0%, transparent 65%),
        radial-gradient(ellipse 120% 90% at 50% 100%, var(--er-graphite-400) 0%, var(--er-carbon-800) 55%, var(--er-carbon-900) 100%);
    opacity: 0;
    transition: opacity var(--er-motion-cinematic) var(--er-ease-out);
}
.er-hero--orbital.is-atmosphere-in .er-hero-orbital__atmosphere {
    opacity: 1;
}
.er-hero-orbital__atmosphere::after {
    content: "";
    position: absolute;
    inset: 0;
    background-image:
        linear-gradient(rgba(200, 208, 213, 0.035) 1px, transparent 1px),
        linear-gradient(90deg, rgba(200, 208, 213, 0.035) 1px, transparent 1px);
    background-size: 64px 64px;
    mask-image: radial-gradient(ellipse 55% 50% at 50% 45%, black 0%, transparent 75%);
}

.er-hero-orbital__sweep {
    position: absolute;
    inset: 0;
    z-index: 1;
    background: var(--er-gradient-emerald-sweep);
    opacity: 0;
    pointer-events: none;
}
.er-hero--orbital.is-sweeping .er-hero-orbital__sweep {
    animation: er-hero-sweep 900ms var(--er-ease-out);
}
@keyframes er-hero-sweep {
    0% { opacity: 0; transform: translateX(-30%); }
    35% { opacity: 0.5; }
    100% { opacity: 0; transform: translateX(30%); }
}

/* ---------- The atom stage (Gate 2) ---------- */
.er-hero-orbital {
    position: relative;
    z-index: 2;
    width: 100%;
    display: flex;
    justify-content: center;
}
.er-atom-stage {
    position: relative;
    width: var(--atom-stage-w);
    height: var(--atom-stage-h);
    max-width: 96vw;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* ---------- Orbit paths — geometry (rx/ry/rotate) is set by
   hero-orbital.js from the same FAMILIES object the station math uses;
   this file only owns the drawn line's visual weight. Thicker/more
   visible than Refinement 002 so the six stations read as sitting *on* a
   visible ellipse rather than floating near a barely-there line. ---------- */
.er-hero-orbital__orbits {
    position: absolute;
    inset: 0;
    z-index: 1;
    width: 100%;
    height: 100%;
    opacity: 0;
    transition: opacity var(--er-motion-cinematic) var(--er-ease-out);
}
.er-hero--orbital.is-paths-in .er-hero-orbital__orbits {
    opacity: 1;
}
.er-hero--orbital.is-settled .er-hero-orbital__orbits {
    opacity: 0.4;
}
.er-hero-orbital__path {
    fill: none;
    stroke: var(--er-emerald-400);
    stroke-width: 1.5;
    stroke-opacity: 0.65;
    vector-effect: non-scaling-stroke;
}
.er-hero-orbital__path--b,
.er-hero-orbital__path--c {
    stroke: var(--er-metallic-300);
    stroke-opacity: 0.45;
}

/* ---------- Nucleus (Refinement 004: headline + one CTA, independently
   positioned — see front-page.php's Gate 4 comment for the reasoning.
   The wrapper spans the full stage (inset:0, matching how __orbits and
   __stations already do this) purely so its 50%/50% is exactly the
   atom's true center; it carries no visible box of its own, so it must
   NOT capture pointer events across that whole area (it sits at z-index
   20, above the stations at z-index ~15, and would otherwise block every
   station click/hover). Only the headline's tight backplate and the CTA
   button re-enable pointer-events, matching their actual visible/clickable
   footprint. ---------- */
.er-hero-orbital__nucleus {
    position: absolute;
    inset: 0;
    z-index: 20;
    pointer-events: none;
    /* Final layering fix (Founder): this wrapper no longer gates group
       visibility via its own opacity fade — ASH must be fully opaque from
       frame one (no moment of partial transparency) and the CTA must be
       opaque at all times including initial page load, so neither can sit
       behind a fading ancestor. The wrapper is now purely a layout/offset
       container; the headline keeps its own entrance fade directly on
       itself below, so its timing is unaffected by this change. */
    opacity: 1;
    /* Single source of truth for the headline's size, referenced by the
       CTA's offset below so the two can't drift out of sync the way the
       CTA's old flat clamp() did the moment the headline's own height
       changed (one line vs two). */
    --nucleus-headline-fs: clamp(1.375rem, 4vw, 3.25rem);
    /* Micro-refinement: moves ASH + headline + CTA down together as one
       unit — every one of them is positioned relative to THIS element's
       own box (top/left 50%), so shifting the wrapper shifts all three
       in lockstep without touching the stations, which live in a
       separate sibling element entirely. This is a fixed compositional
       offset, not part of the entrance sequence, so it's set once here
       rather than duplicated in the .is-nucleus-in rule below. */
    transform: translateY(var(--nucleus-offset-y));
}
/* ASH nucleus image. Sits BEHIND the headline/CTA (z-index:0 vs their
   z-index:1) but is otherwise a full member of this z-index:20 nucleus
   context, so it automatically inherits the same relationship to the
   orbiting stations' depth system (which briefly exceeds z-index:20
   during each station's front pass) that the headline already has —
   nothing station-related needed changing for this. pointer-events:none
   so the transparent PNG's bounding box never intercepts clicks meant
   for the CTA below it or stations that may visually overlap it.

   Micro-refinement: split into a frame (positioning, entrance, and now
   the clip boundary) and the img itself (just fills the frame at its
   natural aspect ratio). The frame's height is computed, not guessed —
   it starts at the same top offset ASH always had (still -12% of his
   own full height, now expressed in px as 0.93 x --ash-w since 1] his
   full height is always 1.5x his width at this asset's fixed aspect
   ratio, and 2] a live CSS percentage-of-own-height would now resolve
   against the CROPPED frame height, not the original full-image height
   this lift was calibrated against — a moving target once the frame's
   own height stopped equaling his full natural height) and ends 15px
   PAST the CTA's own top edge (its center-offset formula from the CTA
   rule below, minus half the CTA's own height, plus --ash-crop-gap,
   which is negative — see its own comment above for why) — so the clip
   line tracks the CTA's real position by construction rather than a
   second, independently-guessed pixel value that could drift from it
   the way Refinement 003's original SVG/JS geometry did. The 15px
   overlap is intentional (Founder: "no visible gap between ASH and the
   button... ASH should extend slightly behind the button") — well
   inside the button's own ~29px half-height, so ASH never shows past
   its bottom edge.

   Final layering fix (Founder): ASH is completely static -- no
   fade-in, no scale/translate entrance, no transition on transform. It
   sits at its final position/scale immediately, the instant the hero
   paints, so there is never a frame where the crop boundary is exposed
   by an in-progress position/scale animation.

   Render-ready reveal (Founder): ASH still starts hidden (opacity:0),
   but at his exact final layout position/size the whole time -- nothing
   about this hidden state affects layout, so revealing him never causes
   a position/size jump. hero-orbital.js adds .is-ash-ready to the hero
   root only once the PNG has actually decoded (or a bounded fallback
   fires), at which point this element fades to opacity:1 over a short,
   opacity-only 140ms -- a rendering polish, not an animation. The CTA
   needs no equivalent gating: it's unconditionally opaque/final-position
   via pure CSS (see .er-hero-orbital__ctas below), so by the time this
   script can even run, it's already exactly where Gate 6 requires it to
   be. */
.er-hero-orbital__nucleus-ash-frame {
    position: absolute;
    top: 50%;
    left: 50%;
    z-index: 0;
    width: var(--ash-w);
    height: calc(0.54 * var(--nucleus-headline-fs) + 60px - var(--cta-half-h) - var(--ash-crop-gap) + 0.93 * var(--ash-w));
    overflow: hidden;
    pointer-events: none;
    filter: drop-shadow(0 16px 30px rgba(0, 0, 0, 0.45));
    opacity: 0;
    transform: translateX(-50%) translateY(calc(-0.93 * var(--ash-w))) scale(1);
    transition: opacity 140ms var(--er-ease-out);
}
.er-hero--orbital.is-ash-ready .er-hero-orbital__nucleus-ash-frame {
    opacity: 1;
}
.er-hero-orbital__nucleus-ash {
    display: block;
    width: 100%;
    height: auto;
}
/* Gate 4: pinned to the wrapper's own 50%/50% — since the wrapper is
   inset:0 within .er-atom-stage, this is the atom's true geometric
   center, by construction, independent of the CTA below it. */
.er-hero-orbital__nucleus-headline {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 1;
    margin: 0;
    /* Final layering fix (Founder): the group-level entrance fade used to
       live on the shared .er-hero-orbital__nucleus wrapper above (opacity
       0->1 on .is-nucleus-in), which also carried ASH and the CTA along
       with it. ASH and the CTA are now permanently opaque (see their own
       rules), so the fade moved down onto the headline directly -- same
       trigger class, same timing, so the headline's own on-screen
       behavior is unchanged by this refactor. */
    opacity: 0;
    /* Founder: one line, not two. width:max-content (not the default
       width:auto) is load-bearing here, not decorative — an absolutely
       positioned element with only `left` set (no `right`) shrink-to-fits
       against "container width minus the left offset", not the full
       container, so at medium viewports (measured: 1366px) that halved
       available width fell a few px short of the text's natural one-line
       width and silently wrapped it. width:max-content sizes the box to
       its own content only, independent of that left-offset asymmetry.
       white-space stays "normal" as a safety net for the narrow mobile
       column, which overrides this rule back to static/max-width below. */
    width: max-content;
    white-space: normal;
    padding: var(--er-space-4) var(--er-space-5);
    border-radius: var(--er-radius-lg);
    background: radial-gradient(ellipse 120% 120% at 50% 50%, rgba(8, 11, 13, 0.4) 0%, rgba(8, 11, 13, 0.12) 65%, transparent 100%);
    backdrop-filter: blur(2px);
    text-align: center;
    font-size: var(--nucleus-headline-fs);
    line-height: 1.08;
    pointer-events: none;
    transition: box-shadow var(--er-motion-base) var(--er-ease-out),
                opacity var(--er-motion-cinematic) var(--er-ease-out);
}
.er-hero--orbital.is-nucleus-in .er-hero-orbital__nucleus-headline {
    opacity: 1;
}
/* Gate 8: the depth-blend "near front" glow now hugs just the headline's
   tight backplate, not a taller box that used to also contain a paragraph
   and a second button. */
.er-hero-orbital__nucleus.is-lit .er-hero-orbital__nucleus-headline {
    box-shadow: 0 0 60px 10px var(--er-emerald-glow);
}
/* Headline alignment fix: the box itself was always centered (measured:
   24px padding both sides, text-align:center — confirmed, not the real
   defect). The actual cause is the shared .er-ellipsis component (also
   used in the footer tagline, untouched here via this hero-only scope):
   motion.js cycles its three dots through a 0/1/2/3-dots-lit "typing"
   state every 600ms (see motion.css's opacity:0 default), so at the
   0- and 1-dot phases the headline's reserved ellipsis width (min-width:
   1.5em) is mostly or entirely invisible while still occupying its full
   layout space — at the 0-dot phase this measured as a 102px perceived
   gap on the right against 24px on the left, i.e. exactly the reported
   symptom, despite the box being genuinely symmetric throughout. Giving
   inactive dots a dim baseline (rather than fully invisible) keeps the
   pulse animation's character while keeping perceived content close to
   the box's true, already-centered width at every phase. */
.er-hero-orbital__nucleus-headline .er-ellipsis__dot {
    opacity: 0.35;
}
.er-hero-orbital__nucleus-headline .er-ellipsis__dot.is-active {
    opacity: 1;
}
/* Second, separate contributor to the same defect: .er-ellipsis reserves
   min-width:1.5em (78px at this headline's font-size) with
   text-align:left, but three period glyphs need nowhere near that —
   measured a ~63px gap between the dots and the span's own right edge
   that persists even with every dot at full opacity, since min-width
   forces the span wider than its actual content regardless of the
   opacity animation above. That reservation looks like it predates the
   current opacity-toggle implementation (all three dots are always
   present in the DOM; opacity never changes layout size, so nothing here
   actually needs a fixed min-width to avoid reflow) — shrink-wrapping it
   removes this second source of trailing empty space. Scoped to the
   hero only, same as the opacity fix above. */
.er-hero-orbital__nucleus-headline .er-ellipsis {
    min-width: 0;
}
/* motion.css already forces every dot to opacity:1 under reduced-motion
   (a static, fully-visible "..." with no animation) — this hero-scoped
   rule has higher specificity than that unscoped one and would silently
   re-dim it to 0.35 for reduced-motion users without this override. */
@media (prefers-reduced-motion: reduce) {
    .er-hero-orbital__nucleus-headline .er-ellipsis__dot {
        opacity: 1;
    }
}
/* Gate 7: subordinate to the headline — positioned from the same true
   center, offset downward by a calc() derived directly from
   --nucleus-headline-fs (the headline's own font-size) rather than a
   flat clamp() tuned to one specific headline height. The old flat
   clamp() was tuned for the two-line headline; the moment the headline
   became one line (half the height), that same offset left a ~50px gap
   instead of sitting directly beneath it — exactly the kind of drift
   this formula is meant to prevent going forward. 0.54 approximates
   half the headline's line-height (1.08 line-height / 2); the fixed
   64px covers half the headline's own top/bottom padding + the desired
   visual gap + half the button's (font-size-independent) height.
   Verified by direct measurement, not just derived — see chat history.

   Final layering fix (Founder): the CTA used to fade in from opacity:0.55
   to 1 on .is-settled (a de-emphasis effect during the orbital intro).
   That fade is removed entirely -- the button must read as fully solid
   at all times, including initial page load and the intro animation,
   specifically so ASH's cropped bottom edge can never show through it. */
.er-hero-orbital__ctas {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) translateY(calc(0.54 * var(--nucleus-headline-fs) + 60px));
    z-index: 1;
    margin: 0;
    display: flex;
    justify-content: center;
    pointer-events: auto;
    opacity: 1;
}
/* Micro-refinement (Founder): "Explore ER Systems" set bold -- scoped to
   this one button only (the base .er-btn's 600 weight is untouched
   everywhere else it's used). The button has no fixed width, so the
   longer label just grows the existing auto-sized box; no separate
   dimension override needed. */
.er-hero-orbital__ctas .er-btn {
    font-weight: 700;
}

/* ---------- Stations ("electrons") ---------- */
.er-hero-orbital__stations {
    position: absolute;
    inset: 0;
    z-index: 15;
}

/* Gate 9: the orbital anchor point must correspond to the IMAGE center,
   not the union of image+label. The station link's own box is sized to
   match the image (label is taken out of flow via absolute positioning
   below it), so centering this box via translate(-50%,-50%) precisely
   centers the image on the computed point regardless of label text
   length — a wider label ("Multimedia Workstations" vs "ASH API") can no
   longer pull the anchor off-point. */
.er-hero-orbital__station {
    position: absolute;
    top: 50%;
    left: 50%;
    z-index: var(--station-z, 15);
    width: clamp(64px, 7vw, 108px);
    height: clamp(64px, 7vw, 108px);
    text-decoration: none;
    /* Organic idle drift (Founder): --idle-x/--idle-y are a SEPARATE
       offset from --station-x/--station-y (the orbit home coordinate that
       hero-orbital.js owns exclusively during the intro and in settle()).
       Composed here, after the home translate and before scale(), so a
       few px of drift reads as literal screen pixels regardless of this
       station's own --station-scale -- the drift never touches, overwrites,
       or accumulates into the home coordinate itself. */
    transform:
        translate(-50%, -50%)
        translate(var(--station-x, 0px), var(--station-y, 0px))
        translate(var(--idle-x, 0px), var(--idle-y, 0px))
        scale(var(--station-scale, 1));
    filter:
        blur(var(--station-blur, 0px))
        brightness(var(--station-brightness, 1))
        saturate(var(--station-saturate, 1));
    will-change: transform, filter;
}
.er-hero-orbital__station-image {
    position: relative;
    width: 100%;
    height: 100%;
    border-radius: var(--er-radius-full);
    display: flex;
    align-items: center;
    justify-content: center;
    background:
        radial-gradient(circle at 50% 42%, color-mix(in srgb, var(--station-glow) 55%, transparent) 0%, transparent 68%),
        var(--er-gradient-panel);
    border: 1px solid color-mix(in srgb, var(--station-glow) 45%, var(--er-border-strong));
    box-shadow: 0 0 calc(18px * var(--station-glow-intensity, 1)) color-mix(in srgb, var(--station-glow) 60%, transparent);
    transition: box-shadow var(--er-motion-base) var(--er-ease-out), transform var(--er-motion-base) var(--er-ease-out);
    overflow: hidden;
}
.er-hero-orbital__station-image img {
    width: 78%;
    height: 78%;
    object-fit: contain;
    filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.4));
}
.er-hero-orbital__station-image .er-hero-orbital__station-placeholder {
    width: 34%;
    height: 34%;
    border-radius: var(--er-radius-full);
    background: radial-gradient(circle at 38% 32%, color-mix(in srgb, var(--station-glow) 85%, white 15%) 0%, var(--station-glow) 55%, color-mix(in srgb, var(--station-glow) 60%, black 40%) 100%);
}

/* Taken out of the station's own box (Gate 9) — positioned purely
   relative to the image, never affecting the anchor-centering math above. */
.er-hero-orbital__station-label {
    position: absolute;
    top: calc(100% + var(--er-space-2));
    left: 50%;
    transform: translateX(-50%);
    font-family: var(--er-font-mono);
    font-size: var(--er-text-xs);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--er-text-dim);
    text-align: center;
    padding: var(--er-space-1) var(--er-space-3);
    border-radius: var(--er-radius-full);
    border: 1px solid var(--er-border);
    background: rgba(8, 11, 13, 0.45);
    white-space: nowrap;
    transition: color var(--er-motion-fast) var(--er-ease-standard), border-color var(--er-motion-fast) var(--er-ease-standard);
}

.er-hero-orbital__station:hover .er-hero-orbital__station-image,
.er-hero-orbital__station:focus-visible .er-hero-orbital__station-image {
    transform: translateY(-4px) scale(1.06);
    box-shadow: 0 0 30px color-mix(in srgb, var(--station-glow) 75%, transparent);
}
.er-hero-orbital__station:hover .er-hero-orbital__station-label,
.er-hero-orbital__station:focus-visible .er-hero-orbital__station-label {
    color: var(--er-text);
    border-color: color-mix(in srgb, var(--station-glow) 60%, var(--er-border-strong));
}
.er-hero-orbital__station:focus-visible {
    outline: 2px solid var(--station-glow);
    outline-offset: 6px;
    border-radius: var(--er-radius-full);
}

/* ASH API — ember/fire character, not a flat orange glow. */
.er-hero-orbital__station--ember .er-hero-orbital__station-image::before {
    content: "";
    position: absolute;
    inset: -6px;
    border-radius: var(--er-radius-full);
    background: radial-gradient(circle, rgba(232, 90, 30, 0.55) 0%, rgba(120, 20, 10, 0.25) 55%, transparent 75%);
    animation: er-ember-pulse 3400ms var(--er-ease-in-out) infinite;
    pointer-events: none;
}
@keyframes er-ember-pulse {
    0%, 100% { opacity: 0.55; transform: scale(0.98); }
    45% { opacity: 0.9; transform: scale(1.05); }
    70% { opacity: 0.7; transform: scale(1.0); }
}

/* ---------- Idle state (post-intro): organic random drift (Founder) ----------
   Replaces the old repetitive up/down bob (a single shared @keyframes
   animating margin-top). --idle-x/--idle-y are driven by hero-orbital.js's
   per-station idle-drift controller (started only once .is-settled, only
   outside prefers-reduced-motion, only above the mobile-grid breakpoint —
   see the JS file for the full rationale), which picks a small random
   offset, transitions to it, transitions back toward zero, and repeats —
   each station independently. Registering the two properties as <length>
   (not the CSS default of an un-animatable arbitrary string) is what makes
   them transition smoothly rather than jump; a browser without @property
   support simply snaps between values instead of easing, a harmless
   degradation for a decorative effect. */
@property --idle-x {
    syntax: "<length>";
    inherits: false;
    initial-value: 0px;
}
@property --idle-y {
    syntax: "<length>";
    inherits: false;
    initial-value: 0px;
}
.er-hero--orbital.is-settled .er-hero-orbital__station {
    transition: --idle-x var(--idle-duration, 3400ms) var(--er-ease-in-out),
                --idle-y var(--idle-duration, 3400ms) var(--er-ease-in-out);
}
.er-hero--orbital.is-settled .er-hero-orbital__station-image {
    animation: er-hero-breathe 4200ms var(--er-ease-in-out) infinite;
    animation-delay: var(--station-idle-delay, 0ms);
}
.er-hero--orbital.is-idle-paused .er-hero-orbital__station-image {
    animation-play-state: paused;
}
@keyframes er-hero-breathe {
    0%, 100% { box-shadow: 0 0 16px color-mix(in srgb, var(--station-glow) 45%, transparent); }
    50% { box-shadow: 0 0 26px color-mix(in srgb, var(--station-glow) 70%, transparent); }
}

/* ---------- Reduced motion: static final layout, no sweep, no idle loop ---------- */
@media (prefers-reduced-motion: reduce) {
    .er-hero-orbital__atmosphere,
    .er-hero-orbital__orbits {
        opacity: 1 !important;
        transform: none !important;
        transition: none !important;
    }
    /* .er-hero-orbital__nucleus and .er-hero-orbital__ctas no longer carry
       any opacity transition or entrance transform of their own (Final
       layering fix, Founder: ASH is static, the CTA is permanently
       opaque) — their base rules already resolve to the reduced-motion-
       safe end state, so no carve-out is needed for them here. The
       headline still has its own entrance fade (moved down from the old
       nucleus-level fade), so it keeps the override those three used to
       share. */
    .er-hero-orbital__nucleus-headline {
        opacity: 1 !important;
        transition: none !important;
    }
    /* Render-ready reveal (Founder): the hide-until-decoded behavior is a
       rendering-correctness fix, not a motion effect, so it stays active
       even under reduced motion — ASH still waits for .is-ash-ready
       before showing. Only the 140ms fade is motion, so that's the only
       part removed here: once ready, ASH snaps straight to opacity:1. */
    .er-hero-orbital__nucleus-ash-frame {
        transition: none !important;
    }
    .er-hero-orbital__orbits { opacity: 0.4 !important; }
    /* Organic idle drift (Founder): hero-orbital.js already never starts
       the drift controllers when prefers-reduced-motion matches (checked
       once, at init) -- this is a redundant, defense-in-depth safety net
       so a station can never visibly move even if that JS-side guard were
       ever bypassed. */
    .er-hero-orbital__station {
        filter: none !important;
        animation: none !important;
        --idle-x: 0px !important;
        --idle-y: 0px !important;
        transition: none !important;
    }
    .er-hero-orbital__station-image { animation: none !important; }
    .er-hero-orbital__station--ember .er-hero-orbital__station-image::before {
        animation: none !important;
        opacity: 0.7;
    }
}

/* ---------- Mobile: orbit intro still plays (compact, shortened by JS),
   but the settled arrangement is a deliberate 2-column grid, not the
   desktop clock positions — see hero-orbital.js's mobile handling. The
   stage's own --atom-stage-w/h shrink at this breakpoint (see the tuning
   variables above); this block only handles the settled-state layout
   swap. ---------- */
@media (max-width: 480px) {
    .er-hero--orbital.is-settled.is-mobile-grid .er-atom-stage {
        height: auto;
        min-height: 0;
    }
    .er-hero--orbital.is-settled.is-mobile-grid .er-hero-orbital__nucleus {
        position: relative;
        inset: auto;
        pointer-events: auto;
        /* Reserves room for ASH above the headline. ASH itself stays
           position:absolute (anchored to THIS element, which is why this
           needs position:relative rather than static) and is removed from
           normal flow, so it wouldn't otherwise push the headline down at
           all -- this padding is what creates the intentional overlap:
           the headline (normal flow, starts after this padding) lands
           partway down ASH's height rather than at his very top, so it
           crosses his chest/collar instead of leaving a gap above an
           unoccupied headline position. Tuned via measurement against
           --ash-w's mobile value, not assumed. */
        padding-top: 65px;
    }
    .er-hero--orbital.is-settled.is-mobile-grid .er-hero-orbital__nucleus-ash-frame {
        top: 0;
        left: 50%;
        transform: translateX(-50%);
        /* No crop needed here: mobile's own --ash-w renders ASH's full
           natural height at ~152px, which measurement confirmed already
           lands ~16px above where the (normal-flow, content-dependent)
           CTA starts -- there's no clean closed-form expression for the
           CTA's exact position here the way there is on desktop, since
           its Y is just wherever normal document flow puts it. Rather
           than guess a height that might clip his hand or jacket for no
           reason, this opts out of clipping where measurement shows it
           isn't required. */
        height: auto;
        overflow: visible;
    }
    .er-hero--orbital.is-settled.is-mobile-grid .er-hero-orbital__nucleus-headline {
        position: static;
        transform: none;
        margin-inline: auto;
        /* width:max-content is the fix for the desktop absolute-positioning
           left-offset bug (see the base rule's comment) — it doesn't apply
           here since this element is static, but left uninherited it would
           force this narrow mobile column to the headline's full unwrapped
           width, pushing it wider than the flex-row layout has room for
           (measured: pushed the whole nucleus column 7px past the left
           viewport edge, clipped by .er-hero--orbital's overflow:hidden).
           Resetting to auto restores normal wrap-to-fit block sizing. */
        width: auto;
        max-width: 20ch;
        pointer-events: auto;
    }
    .er-hero--orbital.is-settled.is-mobile-grid .er-hero-orbital__ctas {
        position: static;
        transform: none;
        margin-top: var(--er-space-5);
    }
    .er-hero--orbital.is-settled.is-mobile-grid .er-hero-orbital__stations {
        position: relative;
        inset: auto;
        display: grid;
        grid-template-columns: repeat(2, 1fr);
        gap: var(--er-space-6) var(--er-space-4);
        margin-top: var(--er-space-7);
        width: 100%;
        max-width: 420px;
        margin-inline: auto;
    }
    .er-hero--orbital.is-settled.is-mobile-grid .er-hero-orbital__station {
        position: relative;
        top: auto;
        left: auto;
        width: 72px;
        height: 72px;
        margin-inline: auto;
        transform: none !important;
        filter: none !important;
        animation: none !important;
        z-index: 1 !important;
    }
    .er-hero--orbital.is-settled.is-mobile-grid .er-hero-orbital__station-label {
        position: static;
        transform: none;
        margin-top: var(--er-space-2);
        display: block;
        white-space: normal;
    }
    .er-hero-orbital__orbits { display: none; }
}
