/*
  onboarding-gateway.css — the Gateway's room: page, backdrop, stage, star
  road, act choreography, and Act 1 (arrival / the two worlds).

  onboarding-gateway-flow.css owns what the JS renders into the later acts.

  Design-token / component conventions only (invariants #27/#30):
    - var(--...) tokens for every color. The ONE raw hex in this file is the
      star gold, marked at its declaration; it is not a brand colour.
    - .btn / .btn-gradient from components.css — nothing here duplicates them.
      Note the glass tiers are deliberately NOT used on this page; see below.
    - Container padding 15px, works at 375px, touch targets >= 44px.
    - Stacked declarations, one property per line.

  ── There is no card, and there must never be one ────────────────────
  This page shipped for a while with the acts inside a 640px .glass-lg card,
  which left it structurally what it had always been: a box floating in the
  middle. The acts are full-bleed over the backdrop now.

  So the layering is, from the back:

    z-index 0   .gw-backdrop       the room: cover art at three depths, and,
                                   above it, the scrim (both <body> children,
                                   inside one scene — #40)
    z-index 2   .gateway-stage     the acts, with NO background of their own
    z-index 8500/9000              confetti + poster flights (<body> children)

  Three rules follow, and all of them are load-bearing:
    1. NOTHING this file styles carries backdrop-filter. Glass buys nothing
       here (the room behind it is art we keep deliberately dim) and costs a
       containing block. Translucent fills give the same look for free — where
       a surface is needed it is translucent, never opaque.
       ⚠ Not yet true page-wide: the acts gateway-flow.js renders still apply
       .glass-sm (onboarding-gateway-flow.css). That is a separate pass.
    2. Nothing that hosts a position:fixed descendant may sit inside anything
       carrying transform / filter / backdrop-filter / will-change / contain
       (#40): such an element becomes its containing block. The flight layer
       and the confetti canvas are <body> children for this reason, and must
       stay there. It is also why the backdrop scene is one.
    3. The backdrop is the room and the acts are the point. If the art ever
       competes with an act, the art is what is wrong: turn it down.
*/

/* The star gold. intentional — stars/crown read as gold universally; not a
   brand colour. Everything else on this page uses the brand gradient, and gold
   is deliberately NOT the CTA or accent colour here. */
:root {
    --gw-gold: #ffc24b;
    --gw-gold-dim: rgba(255, 194, 75, 0.28);
    /* intentional — medal rank colours for the pick-order stars (#1 gold, #2
       silver, #3 bronze). #27 sanctioned one-off: rep-tier gold/silver/bronze is
       an allowed non-brand language. Shades picked to read on dark cover art. */
    --gw-silver: #cfd4dd;
    --gw-bronze: #d68a44;
}

/* The page itself never scrolls: the stage is fixed and each act scrolls its
   own overflow, so a short viewport moves the act's content rather than
   sliding the whole room off the MH. */
.gateway-body {
    min-height: 100vh;
    overflow: hidden;
    background: var(--bg);
    font-family: 'Poppins', -apple-system, BlinkMacSystemFont, sans-serif;
    color: var(--text);
}

/* ==================== The backdrop ==================== */
/*
  The room: fourteen posters of real cover art at three depths, holding the
  periphery of a viewport whose centre belongs to the acts.

  The technique is the home hero's, and only the technique: perspective on the
  scene, translateZ per layer, and brightness as the depth cue
  (home/hero-parallax-section.ftlh). None of the hero's parallax, fly-through,
  orbs, rays or star field comes with it — the Gateway's posters are FIXED.
  They are also FAR darker than the hero's 0.65 / 0.8 / 1.0, because on the
  home page the posters are the content and here they are the room.

  Structure (gateway-backdrop.ftlh):

    .gw-backdrop          fixed scene, perspective, overflow: hidden
      .gw-backdrop__field  the ONE drifting element (preserve-3d)
        .gw-backdrop__slot  x14, absolutely placed, translateZ per layer
      .gw-backdrop__scrim   above the art, below every act. Does not drift.

  The scene is a <body> child and must stay one (#40): a transform / filter /
  will-change / contain ancestor would become the containing block for a
  position: fixed element and clip it.
*/
.gw-backdrop {
    position: fixed;
    inset: 0;
    z-index: 0;
    overflow: hidden;
    pointer-events: none;
    perspective: 1200px;
    /* Explicit because every slot coordinate below is authored against it: the
       projection scales each poster's position toward this point by its own
       depth, and the comments state where each one actually lands. */
    perspective-origin: 50% 50%;
}

/*
  The drift lives here rather than on the scene for two reasons. The scrim is
  the first: it is a full-viewport gradient, and drifting it even a few pixels
  would drag its own edge into view. The second is that preserve-3d is what
  lets the scene's perspective reach through this element to the posters, so
  the field is the natural place for the one transform they all share.

  ONE animation for all fourteen posters, never one each: the field composites
  as a single layer and the whole room costs one.

  It also buys real parallax for nothing. The perspective projects this
  element's travel by each poster's own depth, so the far layer moves ~82% as
  far as the near layer without a second keyframe.
*/
.gw-backdrop__field {
    position: absolute;
    inset: 0;
    transform-style: preserve-3d;
    animation: gw-bd-drift 90s ease-in-out infinite;
}

/* Ultra-slow and never quite repeating its path, so the room breathes without
   ever being something the eye can catch moving. Transform only. */
@keyframes gw-bd-drift {
    0% {
        transform: translate3d(0, 0, 0);
    }
    33% {
        transform: translate3d(10px, -12px, 0);
    }
    66% {
        transform: translate3d(-8px, -18px, 0);
    }
    100% {
        transform: translate3d(0, 0, 0);
    }
}

/* left/top are the poster's CENTRE (the -50% translate), which is what makes
   the coordinate table below readable. */
.gw-backdrop__slot {
    position: absolute;
    width: clamp(88px, 9vw, 148px);
    aspect-ratio: 2 / 3;
    border-radius: var(--radius-md);
    overflow: hidden;
    transform: translate(-50%, -50%) translateZ(var(--gw-bd-z, 0px));
    /* intentional — a soft dark drop, sitting the art off the black of the
       room. design-tokens.css defines no shadow token. */
    box-shadow: var(--shadow-xl);
}

/* The depth cue, on the IMG rather than the slot: a filter flattens the
   element it is on, so it belongs on the child of the thing carrying the
   translateZ, exactly as the home hero does it. */
.gw-backdrop__img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    filter: brightness(var(--gw-bd-bright, 0.3));
}

/*
  ── The three layers ──
  Depth, dimness and stacking order, back to front. Tune brightness by eye at
  bring-up, but tune it DOWN: the backdrop losing an argument with Act 1's
  collage is the whole design.
*/
.gw-backdrop__slot--1,
.gw-backdrop__slot--2,
.gw-backdrop__slot--3,
.gw-backdrop__slot--4,
.gw-backdrop__slot--5 {
    --gw-bd-z: -260px;
    --gw-bd-bright: 0.22;
    z-index: 1;
}

.gw-backdrop__slot--6,
.gw-backdrop__slot--7,
.gw-backdrop__slot--8,
.gw-backdrop__slot--9,
.gw-backdrop__slot--10 {
    --gw-bd-z: -140px;
    --gw-bd-bright: 0.30;
    z-index: 2;
}

.gw-backdrop__slot--11,
.gw-backdrop__slot--12,
.gw-backdrop__slot--13,
.gw-backdrop__slot--14 {
    --gw-bd-z: -40px;
    --gw-bd-bright: 0.38;
    z-index: 3;
}

/*
  ── The coordinates ──
  Every slot is placed by hand, and the negative space between them is the
  point: they must never crowd, and none of them may enter the central band
  where the acts live.

  These are AUTHORED centres, and they are not where the posters land. A
  perspective projection pulls everything toward the perspective origin by
  k = 1200 / (1200 + |z|) — 0.82 far, 0.90 mid, 0.97 near — so the far layer in
  particular is authored past the edge of the viewport to arrive inside it. The
  `lands` note on each rule is the effective centre, in % of the viewport, and
  is what the layout was actually reasoned about:

    left band    1 (6, 9)   6 (12, 36)   3 (4, 62)   8 (9, 88)
    right band   2 (94, 13) 7 (88, 38)   4 (96, 64)  9 (91, 90)
    top edge     13 (18, 6) 5 (30, -4)   10 (70, -4) 14 (82, 6)
    bottom edge  11 (26, 94)             12 (74, 95)

  A poster is ~9vw x ~20vh, so the band pairs sit 26-27vh apart vertically and
  the top/bottom rows 12vw apart horizontally: no two overlap on either axis.
  The centre — x 25-75%, y 25-75% at 1440, which is where the act's copy is —
  is empty by construction, and that is what lets the scrim stay light.

  ⚠ Fourteen rules, and GatewayStageService.BACKDROP_SLOTS must equal fourteen.
  A poster with no rule gets no coordinate and lands on the scene's origin.
*/
.gw-backdrop__slot--1 {
    /* lands (6, 9) */
    left: -3.5%;
    top: 0.1%;
}

.gw-backdrop__slot--2 {
    /* lands (94, 13) */
    left: 103.5%;
    top: 5%;
}

.gw-backdrop__slot--3 {
    /* lands (4, 62) */
    left: -6%;
    top: 64.6%;
}

.gw-backdrop__slot--4 {
    /* lands (96, 64) */
    left: 106%;
    top: 67%;
}

.gw-backdrop__slot--5 {
    /* lands (30, -4) — over the top edge, clear of the star road (x 35-65%) */
    left: 25.7%;
    top: -15.7%;
}

.gw-backdrop__slot--6 {
    /* lands (12, 36) */
    left: 7.6%;
    top: 34.4%;
}

.gw-backdrop__slot--7 {
    /* lands (88, 38) */
    left: 92.4%;
    top: 36.6%;
}

.gw-backdrop__slot--8 {
    /* lands (9, 88) */
    left: 4.2%;
    top: 92.4%;
}

.gw-backdrop__slot--9 {
    /* lands (91, 90) */
    left: 95.8%;
    top: 94.7%;
}

.gw-backdrop__slot--10 {
    /* lands (70, -4) — mirror of slot 5 */
    left: 72.3%;
    top: -10.3%;
}

.gw-backdrop__slot--11 {
    /* lands (26, 94) — clear of the skip link, which is bottom centre */
    left: 25.2%;
    top: 95.5%;
}

.gw-backdrop__slot--12 {
    /* lands (74, 95) */
    left: 74.8%;
    top: 96.5%;
}

.gw-backdrop__slot--13 {
    /* lands (18, 6) */
    left: 16.9%;
    top: 4.5%;
}

.gw-backdrop__slot--14 {
    /* lands (82, 6) */
    left: 83.1%;
    top: 4.5%;
}

/*
  The scrim. A vignette's opposite: the art holds the edges and the copy holds
  the centre, so the CENTRE is what gets covered. It is a gradient and nothing
  else — no filter, no opaque fill, and it does not drift with the field.

  Fully token-built: --bg is the page's own black, and color-mix gives the
  midpoints an alpha without inventing a colour.
*/
.gw-backdrop__scrim {
    position: absolute;
    inset: 0;
    z-index: 9;
    background: radial-gradient(115% 95% at 50% 50%,
        var(--bg) 0%,
        color-mix(in srgb, var(--bg) 80%, transparent) 34%,
        color-mix(in srgb, var(--bg) 35%, transparent) 62%,
        transparent 82%);
}

/*
  Narrower viewports: the act's content cap stops shrinking with the viewport
  long before the viewport does, so the "central band" widens until the inner
  posters are underneath the copy. They are dropped rather than dimmed — the
  room keeps only the posters that are still genuinely peripheral.
*/
@media (max-width: 1024px) {
    .gw-backdrop__slot--5,
    .gw-backdrop__slot--10,
    .gw-backdrop__slot--13,
    .gw-backdrop__slot--14 {
        display: none;
    }
}

@media (max-width: 768px) {
    /* At 375px a 88px poster is a quarter of the width, so the eight band
       posters would be the page. Six, smaller, and the scrim over them. */
    .gw-backdrop__slot--8,
    .gw-backdrop__slot--9,
    .gw-backdrop__slot--11,
    .gw-backdrop__slot--12 {
        display: none;
    }
    .gw-backdrop__slot {
        width: clamp(64px, 17vw, 104px);
    }
}

/* The stage. Fixed and full-viewport, and deliberately background-less: every
   pixel of it that no act paints is the room. */
.gateway-stage {
    position: fixed;
    inset: 0;
    z-index: 2;
}

/* ==================== Fixed layers (#40) ==================== */
/* Both are direct children of <body>, and must stay there: inside any glass
   (backdrop-filter) ancestor they would resolve against THAT element and be
   clipped by it rather than the viewport. */
.gw-flight-layer {
    position: fixed;
    inset: 0;
    z-index: 9000;
    pointer-events: none;
}

.gw-confetti {
    position: fixed;
    inset: 0;
    z-index: 8500;
    width: 100%;
    height: 100%;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.6s ease;
}

.gw-confetti.is-live {
    opacity: 1;
}

/* ==================== The star road ==================== */
/*
  A constellation, not a bar. Five nodes, one per act, evenly spaced; the rail
  behind them lights as the visitor travels and the node being travelled toward
  twinkles. gateway-flow.js remaps its band percentage onto these node
  positions, so the rail head always arrives exactly at a star.
*/
/* Pinned to the top of the room. The acts each span the whole stage, so the
   road cannot be stacked above them in flow any more — it is chrome that
   outlives every act. Acts reserve its space with their own top padding.

   The transform makes this a containing block for any fixed descendant (#40).
   That is harmless because the road has none, and it must stay that way. */
.gw-road {
    position: absolute;
    top: 22px;
    left: 50%;
    z-index: 3;
    width: min(420px, calc(100% - 30px));
    transform: translateX(-50%);
}

/* The outer nodes sit at 8%/100% of the rail and are centred on it, so the
   track is inset by half a node to keep them (and their labels) inside the
   card's padding. */
.gw-road__track {
    position: relative;
    margin: 0 32px;
    padding-bottom: 20px;
}

.gw-road__rail {
    position: relative;
    height: 2px;
    border-radius: var(--radius-pill);
    background: var(--border);
}

.gw-road__lit {
    position: absolute;
    inset: 0 auto 0 0;
    width: 0%;
    border-radius: var(--radius-pill);
    background: var(--brand-gradient-h);
    transition: width 0.6s var(--ease-standard);
}

.gw-road__nodes {
    position: absolute;
    inset: 0;
    margin: 0;
    padding: 0;
    list-style: none;
}

.gw-road__node {
    position: absolute;
    top: 1px;
    width: 64px;
    margin-left: -32px;
    text-align: center;
}

/* Node positions. Evenly spaced by 23% from 8%: the arrival star sits just off
   the gate rather than at the very edge, so an ignited node 1 reads as
   "moving" instead of "parked at zero". */
.gw-road__node--1 {
    left: 8%;
}

.gw-road__node--2 {
    left: 31%;
}

.gw-road__node--3 {
    left: 54%;
}

.gw-road__node--4 {
    left: 77%;
}

.gw-road__node--5 {
    left: 100%;
}

.gw-road__star {
    display: block;
    width: 8px;
    height: 8px;
    margin: 0 auto;
    border-radius: var(--radius-circle);
    background: var(--surface-3);
    border: 1px solid var(--border);
    transform: translateY(-4px);
    transition: background 0.4s ease,
                border-color 0.4s ease,
                box-shadow 0.4s ease,
                transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* A star that has been reached. Gold plus a soft bloom: the one place on this
   page gold is allowed, and it is the reward colour, never the CTA. */
.gw-road__node.is-lit .gw-road__star {
    background: var(--gw-gold);
    border-color: var(--gw-gold);
    box-shadow: 0 0 0 3px var(--gw-gold-dim),
                0 0 12px var(--gw-gold-dim);
    transform: translateY(-4px) scale(1.15);
}

/* The star being travelled toward. Twinkles so the eye knows where it is
   going; it is not lit yet, so its colour stays brand rather than gold. */
.gw-road__node.is-now .gw-road__star {
    background: var(--brand-blue);
    border-color: var(--brand-blue);
    animation: gw-twinkle 1.8s ease-in-out infinite;
}

/* intentional — a brand-blue halo pulse; design-tokens.css defines no shadow
   token, and a keyframe cannot interpolate to a var()-built shadow reliably. */
@keyframes gw-twinkle {
    0%,
    100% {
        box-shadow: var(--ring-blue);
        transform: translateY(-4px) scale(1);
    }
    50% {
        box-shadow: var(--ring-blue);
        transform: translateY(-4px) scale(1.35);
    }
}

.gw-road__label {
    display: block;
    margin-top: 8px;
    font-size: 0.5625rem;
    font-weight: var(--weight-semibold);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-dim);
    transition: color 0.4s ease;
}

.gw-road__node.is-lit .gw-road__label {
    color: var(--text-secondary);
}

.gw-road__node.is-now .gw-road__label {
    color: var(--text);
}

@media (max-width: 420px) {
    .gw-road__track {
        margin: 0 26px;
    }
    .gw-road__node {
        width: 52px;
        margin-left: -26px;
    }
    .gw-road__label {
        font-size: 0.5rem;
        letter-spacing: 0.04em;
    }
}

/* ==================== Act choreography ==================== */
/*
  ONE camera move, not a slideshow. The incoming act rises while the outgoing
  act is blurred and lifted away on top of it. The two overlap for the length
  of the exit, which is what makes it read as a move rather than a cut.

  Every act now spans the entire stage and centres its own content, so the two
  overlap in exactly the same room and neither can shove the other: there is no
  container height for them to fight over, which is what the old card's
  absolute-pin-the-exiting-act trick existed to prevent.
*/
.gw-acts {
    position: absolute;
    inset: 0;
}

/*
  An act IS the page. It centres its content and scrolls its own overflow, so a
  short viewport (or a long act, like the wall) moves the content rather than
  the room. The top padding clears the pinned road, the bottom the skip link.

  Centring is done with the inner's `margin: auto`, NOT justify-content: center.
  They look identical until the content is taller than the act, and then they
  differ badly: justify-content distributes NEGATIVE free space too, pushing the
  top of the content up past the padding edge where no scrolling can reach it.
  Flexbox resolves auto margins to zero when free space is negative, so the
  content simply starts at the top and scrolls. The wall at 375px and the rate
  act on a short laptop both land in exactly that case.
*/
.gw-act {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 86px 15px 68px;
    overflow-y: auto;
    text-align: left;
}

/*
  ==================== Act content caps ====================
  The replacement for the card, and the whole point: the cap is on the CONTENT
  of each act, sized to what that act actually is, instead of one box wrapped
  around the entire flow. Nothing here paints a background — a cap is a width,
  not a surface.

    1  arrival     the title and two side cards, a title card's width
    2  the wall    the poster grid + shelf; the widest thing in the flow
    3  the pass    one poster and three control rows; a reading measure
    4  crown/fork  a single subject, centred and close
    5  the reveal  a teaser rail and the quote card
*/
/* `margin: auto` is the act's vertical centring as well as its horizontal one —
   see .gw-act. Do not swap it for justify-content. */
.gw-act__inner {
    width: 100%;
    max-width: 640px;
    margin: auto;
}

/* Wide enough for the two worlds to be worlds. The title and the promise stay
   narrow on their own (the promise caps itself at 440px), so this cap is the
   collage row's, not the copy's.

   Act 1 is a title card and reads centred; every later act is a working surface
   and inherits .gw-act's `text-align: left`. That centring lived in its own
   second rule for this same selector further down the file — folded in here so
   the selector appears exactly once (two rules with one selector is the shape a
   #33 shorthand accident grows in, even when — as it was — both halves are
   harmless longhands). */
.gw-act[data-act="1"] .gw-act__inner {
    max-width: 980px;
    text-align: center;
}

.gw-act[data-act="2"] .gw-act__inner {
    max-width: 1080px;
}

.gw-act[data-act="3"] .gw-act__inner {
    max-width: 640px;
}

.gw-act[data-act="4"] .gw-act__inner {
    max-width: 560px;
}

.gw-act[data-act="5"] .gw-act__inner {
    max-width: 720px;
}

/* An act that is neither on stage nor leaving it. Kept in the DOM only while
   the flow needs it; never visible. */
.gw-act:not(.is-active):not(.gw-act--exit) {
    display: none;
}

.gw-act.is-active {
    animation: gw-act-in 0.55s cubic-bezier(0.4, 0, 0.2, 1) both;
}

@keyframes gw-act-in {
    from {
        opacity: 0;
        transform: translateY(18px) scale(0.985);
        filter: blur(6px);
    }
    to {
        opacity: 1;
        transform: none;
        filter: blur(0);
    }
}

/*
  An act a poster is flying INTO does not rise as a whole.

  Two reasons, and the first is load-bearing: the act's own transform would move
  the flight's destination rect out from under it mid-measure, so the poster
  would land 18px off and then the act would settle under it. The second is that
  the flying poster IS the camera move here — an act sliding underneath it would
  be two moves fighting for the same beat. Its inner .rise children still stage
  in, so the act still arrives in sequence rather than all at once.
*/
.gw-act.gw-act--flight-in.is-active {
    animation: none;
}

/* The act is already absolute across the whole stage, so leaving costs it
   nothing but its interactivity: no re-pinning, no height to give back. */
.gw-act--exit {
    pointer-events: none;
    animation: gw-act-out 0.45s cubic-bezier(0.4, 0, 0.2, 1) both;
}

@keyframes gw-act-out {
    from {
        opacity: 1;
        transform: none;
        filter: blur(0);
    }
    to {
        opacity: 0;
        transform: translateY(-22px) scale(0.98);
        filter: blur(10px);
    }
}

/* ==================== Staged entrances ==================== */
/* Elements inside an act arrive in sequence rather than all at once. */
.rise {
    animation: gw-rise 0.6s cubic-bezier(0.4, 0, 0.2, 1) both;
}

@keyframes gw-rise {
    from {
        opacity: 0;
        transform: translateY(12px);
    }
    to {
        opacity: 1;
        transform: none;
    }
}

.d1 {
    animation-delay: 0.05s;
}

.d2 {
    animation-delay: 0.14s;
}

.d3 {
    animation-delay: 0.23s;
}

.d4 {
    animation-delay: 0.32s;
}

/* ==================== Act 1: arrival ==================== */
.gateway-eyebrow {
    display: inline-block;
    margin-bottom: 12px;
    font-size: 0.8125rem;
    font-weight: var(--weight-semibold);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.gateway-title {
    margin: 0 0 14px;
    font-size: clamp(2rem, 6vw, 3rem);
    font-weight: var(--weight-bold);
    letter-spacing: -0.01em;
    line-height: 1.15;
    background: var(--brand-gradient);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

.gateway-promise {
    margin: 0 auto 32px;
    max-width: 440px;
    font-size: clamp(0.9375rem, 2vw, 1.0625rem);
    line-height: 1.55;
    color: var(--text-secondary);
}

.gateway-side-group__title {
    margin: 0 0 4px;
    font-size: 1rem;
    font-weight: var(--weight-semibold);
    color: var(--text);
}

.gateway-side-group__caption {
    margin: 0 0 20px;
    font-size: 0.875rem;
    color: var(--text-muted);
}

/* ==================== Act 1: the two worlds ==================== */
/*
  Two panels of real cover art, not two icons. The visitor is choosing between
  two bodies of work, so the choice is made of the work.

  They are flex SIBLINGS and must stay siblings: one panel growing is what
  makes the other shrink, and that trade is the entire effect. Nothing here
  uses .glass-sm (or any glass) — see this file's header: no backdrop-filter on
  this page, at all.
*/
.gw-worlds {
    display: flex;
    gap: 22px;
    width: min(980px, 100%);
    height: min(52vh, 460px);
    margin: 0 auto;
}

/*
  Base `flex` is declared ONCE, here. Every state below touches flex-GROW only:
  re-declaring the `flex` shorthand in a state rule would reset basis and
  shrink with it, and a shorthand always wins over the longhand it contains
  (#33). flex-grow is also the only one of the three that animates cleanly.

  ⚠ `border: none` is load-bearing and must NOT be dropped as "the default".
  This element is a <button>, and the UA stylesheet gives buttons their own
  border — deleting the declaration does not remove the rim, it hands the rim
  back to the browser (#33: the shorthand is what silences all four longhands
  at once). No state below may reintroduce a border of any kind.
*/
.gw-world {
    flex: 1 1 0;
    position: relative;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    min-width: 0;
    padding: 26px;
    border: none;
    border-radius: var(--radius-xl);
    /* Transparent: with the border gone (round 2) the glass fill's rounded edge
       was the only thing bounding the panel and it read as a faint card. The
       three posters and the label's own scrim sell the panel now (spec item 1).
       ⚠ #33: this is a <button>, so the declaration must set `transparent`, not
       be deleted — deleting it hands the UA button-face fill back. The zero-cover
       fallback stays a WASH on the collage child (.gw-world--wash), so an empty
       panel still reads as a world without a background returning here. */
    background: transparent;
    color: var(--text);
    font-family: inherit;
    text-align: center;
    cursor: pointer;
    transition: flex-grow 0.8s var(--ease-spring),
                transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1),
                opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1),
                box-shadow 0.5s ease;
}

/* ⚠ INSET, NOT inset: 0, AND THAT IS THE FIX FOR THE CROPPED FAN.
   The three covers total 106% of the collage on purpose (see .gw-world__cimg),
   and the outer two are rotated ~9-10deg, which widens their painted boxes
   further. With the collage filling the panel edge to edge, all of that
   overflowed .gw-world - which is `overflow: hidden` for its rounded corners -
   so the left and right posters were sliced down their outer edges and the fan
   read as cropped rather than fanned (owner call 2026-09-10).

   Insetting the collage shrinks the box the 106% resolves against, so the whole
   fan lands inside the panel with its corners intact. The alternative - dropping
   overflow:hidden - would let the posters spill past the panel's rounded edge
   and over its neighbours, which trades one ugly result for a worse one. The
   bottom inset is larger because the label sits down there and the fan should
   not run under it. */
.gw-world__collage {
    position: absolute;
    inset: 6% 7% 14%;
    display: flex;
    align-items: center;
    justify-content: center;
    filter: brightness(0.55) saturate(1.1);
    transition: filter 0.6s var(--ease-standard),
                transform 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/*
  No covers at all — a catalog that has not loaded. The panel washes itself in
  the brand gradient instead of being an empty box, and because the wash is on
  the collage it inherits the same brightness and the same hover lift the art
  would have had. It reads as a world either way.
*/
.gw-world--wash .gw-world__collage {
    background: var(--brand-gradient);
}

/* The fan. flex-shrink: 0 is load-bearing — the three covers total 106% of the
   panel on purpose, and letting flex resolve that overflow would square the
   fan back up into a row. */
.gw-world__cimg {
    flex: 0 0 auto;
    width: 34%;
    aspect-ratio: 2 / 3;
    border-radius: var(--radius-lg);
    object-fit: cover;
    /* intentional — a deep drop that lifts the fan off the panel; no shadow
       token exists. */
    box-shadow: var(--shadow-xl);
    transition: transform 0.8s var(--ease-spring);
}

.gw-world__cimg--1 {
    transform: rotate(-9deg) translate(16%, 6%);
}

/* The middle card is larger and in front: the fan has a subject. */
.gw-world__cimg--2 {
    z-index: 2;
    width: 38%;
    transform: rotate(2deg) translateY(-4%);
}

.gw-world__cimg--3 {
    transform: rotate(10deg) translate(-16%, 8%);
}

/*
  Centred, at the foot of the panel. The reference anchored this bottom-left;
  this page is a welcome and every act of it is balanced.

  There used to be a panel-wide veil element here — a `transparent 30% →
  rgba(0,0,0,.92) 92%` ramp across the whole panel — carrying the label over the
  art. It is gone, element and all: it was drowning the bottom two thirds of
  three real covers to light up two short lines of text. Legibility is now the
  label's own problem and it solves it locally, with the scrim below plus the
  type weight, so the art stays art.
*/
.gw-world__label {
    position: relative;
    z-index: 4;
    display: flex;
    flex-direction: column;
    gap: 4px;
    /* intentional — half the legibility work, and the half that costs the art
       nothing: it darkens only the pixels the glyphs actually cover. */
    text-shadow: 0 1px 12px rgba(0, 0, 0, 0.9);
}

/*
  The other half: a soft pool sized to the text block, bleeding a little past it
  on every side and fading to nothing before it reaches an edge. It has no
  boundary of its own, so it cannot read as a panel or as the veil's ghost.

  z-index: -1 is safe here and does NOT escape to the collage: .gw-world__label
  is positioned WITH a z-index, so it is a stacking context, and a negative
  child can only fall to the back of its parent's context. It lands behind the
  words and in front of the art, which is the whole job.
*/
.gw-world__label::before {
    content: "";
    position: absolute;
    left: -20px;
    right: -20px;
    top: -16px;
    bottom: -24px;
    z-index: -1;
    pointer-events: none;
    /* intentional — a legibility scrim, not a surface. No token describes a
       feathered black pool, and it must stay black whatever the art does. */
    background: radial-gradient(ellipse 72% 108% at 50% 58%,
                                rgba(0, 0, 0, 0.85) 0%,
                                rgba(0, 0, 0, 0.6) 46%,
                                rgba(0, 0, 0, 0) 100%);
}

/* Heavier than it was. With the veil gone the weight is doing real work, not
   styling. */
.gw-world__title {
    font-size: clamp(1.125rem, 2vw, 1.375rem);
    font-weight: var(--weight-bold);
}

.gw-world__caption {
    font-size: 0.8125rem;
    line-height: var(--leading-base);
    font-weight: var(--weight-medium);
    color: var(--text-secondary);
}

/*
  Hover / keyboard focus. The panel takes half again as much room, which shrinks
  its sibling with no rule of its own, and the art wakes up: brighter, closer,
  and the outer two cards fan wider.
*/
.gw-world:hover,
.gw-world:focus-visible {
    flex-grow: 1.5;
    /* intentional — a deeper drop under the panel being reached for. */
    box-shadow: var(--shadow-2xl);
}

.gw-world:hover .gw-world__collage,
.gw-world:focus-visible .gw-world__collage {
    filter: brightness(0.75) saturate(1.25);
    transform: scale(1.04);
}

.gw-world:hover .gw-world__cimg--1,
.gw-world:focus-visible .gw-world__cimg--1 {
    transform: rotate(-12deg) translate(12%, 3%);
}

.gw-world:hover .gw-world__cimg--3,
.gw-world:focus-visible .gw-world__cimg--3 {
    transform: rotate(13deg) translate(-12%, 5%);
}

/*
  Chosen. gateway-flow.js toggles .is-selected and moves straight on to Act 2,
  so this state is what the visitor sees the act leave on: the world they picked
  takes the room, and the one they did not falls away.

  After :hover in the cascade on purpose — both set flex-grow, and a choice
  outranks a pointer that happens to be resting somewhere.

  This used to be a --brand-blue rim. Chosen is now carried by LIGHT: the panel
  takes most of the row, it lifts, and its art comes all the way up out of the
  dark while the rejected panel falls away to a quarter opacity. A rim was never
  what made this legible anyway — the flex trade was.
*/
.gw-world.is-selected {
    flex-grow: 2.4;
    transform: scale(1.02);
    /* intentional — the chosen panel's drop is the deepest on the page; it is
       the only thing left standing. */
    box-shadow: var(--shadow-2xl);
}

/*
  The chosen world's art wakes up further than hover took it (0.75) — this is
  the lift that replaces the rim, and it must out-run hover or choosing a panel
  the pointer is already resting on would change nothing at all.

  After the :hover collage rule for exactly that reason.
*/
.gw-world.is-selected .gw-world__collage {
    filter: brightness(1) saturate(1.35);
    transform: scale(1.06);
}

.gw-world.is-selected .gw-world__cimg--1 {
    transform: rotate(-13deg) translate(11%, 2%);
}

.gw-world.is-selected .gw-world__cimg--3 {
    transform: rotate(14deg) translate(-11%, 4%);
}

/* :has() rather than a container class because the flow never sets one: the
   panel that was NOT chosen has no state of its own to key off, only a chosen
   sibling. flex-grow is reset so a stray hover cannot re-expand a panel that
   has just been rejected. */
.gw-worlds:has(.gw-world.is-selected) .gw-world:not(.is-selected) {
    flex-grow: 1;
    opacity: 0.25;
    transform: scale(0.95);
}

/*
  Mobile: stacked. The hover-expand has no meaning on a touch screen, so the
  chosen/dimmed pair carries the whole effect; the fan stays fanned, because it
  is what makes the panel a world rather than a button.
*/
@media (max-width: 768px) {
    .gw-worlds {
        flex-direction: column;
        gap: 14px;
        height: auto;
    }
    /*
      Sized by content, not by a share of the row. Set as longhands rather than
      the `flex` shorthand so the desktop rule's flex-shrink survives and
      nothing here can silently reset a value it never mentions (#33).

      A column's free space is vertical, so growing a panel would shove its
      sibling down the page instead of trading width with it: grow is off in
      every state, and the chosen/dimmed pair carries the choice on its own.
    */
    .gw-world,
    .gw-world:hover,
    .gw-world:focus-visible,
    .gw-world.is-selected,
    .gw-worlds:has(.gw-world.is-selected) .gw-world:not(.is-selected) {
        flex-grow: 0;
        flex-basis: auto;
    }
    .gw-world {
        /* Comfortably past the 44px touch minimum, and tall enough for the fan
           to still be a fan. */
        min-height: 200px;
        padding: 18px;
    }
}

/* ==================== Skip ==================== */
/* Pinned to the floor of the room, for the same reason the road is pinned to
   its ceiling: it belongs to the page and outlives every act. */
/* ⚠ POSITIONING ONLY. This used to carry the whole appearance too - a bare
   0.875rem --text-dim link with no chrome - so the one way out of the gateway
   read as body text rather than as a control (owner call 2026-09-10). It is now
   .btn.btn-ghost.btn-pill in the markup and this rule only places it, so the
   button's look comes from the shared system and stays in step with it.

   min-height/line-height are gone with the rest: .btn sets its own padding, and
   the 44px target is met by that padding plus the label. */
.gateway-skip-link {
    position: absolute;
    left: 50%;
    bottom: 6px;
    z-index: 3;
    transform: translateX(-50%);
    font-size: 0.875rem;
}

/* The local :hover/:focus-visible colour override is GONE deliberately. It set
   `color: var(--text-secondary)`, which ties with .btn-ghost:hover at 0,2,0 and
   wins on source order - so adopting the shared class while keeping this would
   have left the button with the shared shape and the old link's hover, which is
   exactly the half-migration CLAUDE.md #33 warns about. The ghost variant's own
   hover now applies. */

/* ==================== Reduced motion ==================== */
/* The flow must still WORK; nothing flies. Acts cut instead of gliding, the
   road still lights, the twinkle stops. */
@media (prefers-reduced-motion: reduce) {
    .gw-act.is-active,
    .gw-act--exit,
    .rise {
        animation: none;
    }
    /* The room stops breathing. It keeps its depth: the layout, the layers and
       the dimming are all static properties and none of them move. */
    .gw-backdrop__field {
        animation: none;
    }
    /* The worlds still expand, dim and light up on hover and on choice — they
       just arrive there rather than travel. Every one of those is a state the
       visitor asked for, so removing them would remove the act, not the
       motion. */
    .gw-world,
    .gw-world__collage,
    .gw-world__cimg {
        transition: none;
    }
    .gw-act--exit {
        display: none;
    }
    .gw-road__lit,
    .gw-road__star,
    .gw-road__label {
        transition: none;
    }
    .gw-road__node.is-now .gw-road__star {
        animation: none;
    }
    .gw-confetti {
        transition: none;
    }
}
