/* THE RIP CARD — /generator (Features A and B of the Surge scope).

   Two columns on desktop: the card itself on the left, at its real 16:9, and
   the three steps beside it. Below 1024 they stack, card first, because the
   card is the thing the page is about and the steps are how you improve it.

   NOTHING HERE INVENTS A COLOUR. Every value is a token out of style.css
   :root — the white tile with the black keyline and the hard 3px drop, the
   pink accent, Barlow Condensed italic for anything that shouts. The canvas
   redraws the same palette as literals (generator.js `T`) because a canvas
   cannot resolve a custom property; the two lists must agree. */

.ov--gen .ov__panel{width:min(1180px,100%);max-height:100%}
.ov--gen .ov__body{padding:20px 26px 26px;overflow:auto;position:relative;z-index:1}
/* the stage fades the body's top 64px; the card's top edge sits there */
html[data-layout="stage"] .ov--gen .ov__body{-webkit-mask-image:none;mask-image:none}

/* BACK, NOT AN X (page.css §2: a page has no X). This one rides in the head's
   flow rather than pinned to the panel, so it needs no new rule over there —
   the head already sits in the band the frame leaves clear under the site
   header. It is here because the way IN is the account menu, which is not one
   of the rows on the left, and that is exactly the case Luke's rule keeps a
   Back for. */
/* .ov__head is justify-content:space-between, which threw the title to the far
   right with Back on the left and the whole window between them. The page's
   name belongs beside its Back, so this head packs left and the title drops to
   the size a label wants rather than the size a modal's shout wants. */
/* ── AND IT STOPS SITTING 34px LOW (Luke, 16 Sep: "the back button and the
   'Rip my card' heading are too low down") ───────────────────────────────────
   MEASURED at 1920x1080: the site header's bottom edge is at 78 and --pg-pad is
   136, which is page.css's own rule — "the header's bottom plus 58" — and 58 is
   the Pokédex figure Luke approved and page.css §2a calls the reference. But
   the first ink here landed at 170, not 136: the sheet's .ov__head was adding
   32.4px of its own on top of the frame's.

   That is page.css §2a's "two insets, one gap, twice the size" exactly, and the
   fix is the one .ov--profile and .ov--clan already have — the page gives up
   the SHEET's inset and keeps the FRAME's. It is not a new number; it is this
   page finally using the agreed one. */
html[data-layout="stage"] .ov--gen .ov__head{padding-top:0}
.gen__head{display:flex;align-items:center;gap:16px;justify-content:flex-start}
.gen__head .ov__h{font-size:clamp(26px,2.4vw,34px)}
.gen__back{flex:none;height:40px;padding:0 14px 0 9px;display:inline-flex;align-items:center;gap:5px;
  background:#fff;border:2.5px solid var(--key);border-radius:12px;box-shadow:0 3px 0 var(--key);
  color:var(--key);font:900 italic 15px var(--display);text-transform:uppercase;letter-spacing:.04em;
  cursor:pointer;line-height:1}
.gen__back:active{transform:translateY(3px);box-shadow:0 0 0 var(--key)}

.gen{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(330px,.8fr);gap:26px;align-items:start}
@media (max-width:1023px){.gen{grid-template-columns:1fr;gap:20px}}

/* ---- the card ---------------------------------------------------- */
.gen__card{position:sticky;top:0}
@media (max-width:1023px){.gen__card{position:static}}
/* NO BOX ROUND A BOX (Luke, 16 Sep: "its nin like a blue box. no need for it").
   paintCard draws a 10px black keyline INTO the image at 5,5 — it has to, so
   the picture has an edge wherever it is pasted, on a white timeline and a black
   one alike. The CSS was then drawing a SECOND border, a drop shadow and a sky
   fill around that one. The image carries its own frame; this just shows it. */
.gen__card canvas{width:100%;height:auto;display:block;aspect-ratio:16/9;
  border:0;border-radius:11px;box-shadow:none;background:none}

/* ── THE FINISHED CARD PAGE ──────────────────────────────────────────────
   Luke, 16 Sep, two calls that resolve into one layout:

   "the card needs to be a seperate asset... them bars need to no be shown on
   the x post. only on the site when its generated" — so the picture is the
   CARD, at the card's own portrait shape, and REACH / POWER / RIPPER are HTML
   beside it rather than painted into a landscape frame that then needed a pale
   blue field to sit on. The field was the "blue box" and it has no job left.

   "the page shouldnt be like scrollable... needs to all fit" — MEASURED, not
   guessed, at 1147x655: the body has 424px of content box (the site header
   takes 106, .ov__head 63, and .ov__body's own padding 18 + 44). A portrait
   card is the height-hungry shape, so the cap matters more here than it did
   for the 16:9 composite: the card is capped to the row and the figures sit
   beside it, which is also what stops a tall object pushing its own stats off
   the bottom of the window.

   Two columns, card first. It is the .gen grid the intro already uses, so the
   object does not jump sideways at the moment the reveal hands over. */
.gen--done{display:flex;flex-direction:column;align-items:stretch;gap:12px;
  height:100%;min-height:0}
.gen--done .gen__top{display:flex;align-items:stretch;gap:24px;flex:1 1 auto;min-height:0}
/* the column beside the card carries everything ABOUT it, actions included, so
   the card gets the body's full height rather than the height left over under
   a band of buttons */
.gen__side{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;
  justify-content:center;gap:14px}
/* RELATIVE + ABSOLUTE, NOT A GRID CELL. max-height:100% on an in-flow child
   resolves against an AUTO-sized track, which is indefinite, so it computes to
   none and the canvas overflowed its box by 186px — measured, not predicted.
   Out of flow against a relatively positioned parent whose used height the flex
   line has already fixed, the percentage resolves, and inset:0 + margin:auto +
   both dimensions auto is the replaced-element centring rule: it scales the
   card down to fit and keeps its ratio exactly. */
/* ── THE CARD IS THE SIZE THE REVEAL ENDS ON, EXACTLY ────────────────────
   Luke, 16 Sep: "the slab in screen shot one should be the exact same size as
   in screenshot 2" — the slab at the end of the ceremony against the one on
   this page. Measured, they were 5% apart at 1920x1080, 10% at 1512x982 and
   20% at 1147x655, so the slam grew as it landed.

   THEY MEET AT THE BIGGER SIZE, AND THIS PAGE IS NO LONGER THE ONE THAT MOVES.
   Luke, 17 Sep: "feel like you made the one smaller instead of making the other
   one bigger?" He was: this said 0.710, which was the ceremony's own 71%, so
   the two agreed by bringing the page DOWN to it. The ceremony comes up
   instead — card-open.js moves the camera with the sequence and every beat now
   fills its frame, the ending included. Its own comment says how each of the
   three distances is bounded, and why a CSS scale on this canvas would have
   been a raster upscale of the hero object.

   0.930 IS card-open.js's SLAB_FILL, and it is that file's number, not this
   one's: the camera track measures the slab and aims for exactly this share of
   the stage box, reaching it by the end of the slam. It is scale-invariant because the stage box is 0.80 wide at every
   window this page has — 335x419, 585x731, 650x813 — so one fraction frames it
   at all of them. Measured at the end of the real sequence: 390/419, 680/731
   and 756/813, all 93.0%.

   IT IS NOT 0.8536, WHICH IS WHAT THIS SAID FIRST AND WHY LUKE HAD TO SAY IT
   TWICE. That figure came from the alpha bounding box of the finished frame —
   but the slab throws a DROP SHADOW that reaches the bottom edge of the frame,
   so the box was the slab plus its shadow and 20% too tall. The page was
   matched to the shadow. card-open.js's inkBox now cuts at alpha 200, above the
   shadow and below the holder, and says why.

   DO NOT EDIT THIS NUMBER ON ITS OWN. tools/social-qa/card-fit.mjs reads
   SLAB_FILL off the running page and checks this calc() against it, so a change
   here alone fails and a change there alone fails — which is what stops the two
   drifting a third time. The 26px is the caption's reservation on the stage,
   subtracted here for the same reason it is subtracted there. */
.gen--done .gen__card{position:relative;display:block;flex:0 0 auto;min-height:120px;
  min-width:0;width:clamp(150px,40%,520px)}
.gen--done .gen__card canvas{position:absolute;inset:0;margin:auto;
  width:auto;height:auto;max-width:100%;max-height:calc((100% - 26px) * 0.930);
  aspect-ratio:auto;border:0;border-radius:0;box-shadow:none;background:none}

/* ---- the figures, beside the card -------------------------------- */
.gen__stats{min-width:0;display:flex;flex-direction:column;gap:10px;overflow:hidden}
.gen__who{margin:0;font:900 italic clamp(30px,4.2vw,56px)/.95 var(--display);
  text-transform:uppercase;letter-spacing:.01em;color:#fff;
  -webkit-text-stroke:5px var(--key);paint-order:stroke fill;
  overflow-wrap:anywhere}
.gen__meta{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
.gen__rar{padding:6px 13px;background:var(--navy);color:#fff;border:2.5px solid var(--key);
  border-radius:11px;box-shadow:0 3px 0 var(--key);
  font:900 italic 19px var(--display);text-transform:uppercase;letter-spacing:.04em}
.gen__member,.gen__fol{padding:5px 11px;background:#fff;border:2.5px solid var(--key);
  border-radius:11px;font:800 13px var(--ui);color:var(--ink)}

.gen__bars{margin:2px 0 0;display:grid;gap:8px}
/* ONE OBJECT PER ROW, self-grounded: the label, the track and the figure live
   inside one white pill with the site's keyline and hard drop, so none of the
   three is ever text sitting straight on the lobby art. */
.gen__bar{display:grid;grid-template-columns:76px minmax(0,1fr);align-items:center;gap:10px;
  padding:5px 11px;background:#fff;border:2.5px solid var(--key);border-radius:15px;
  box-shadow:0 3px 0 var(--key)}
.gen__bar dt{font:800 12px var(--ui);text-transform:uppercase;letter-spacing:.07em;
  color:var(--ink-dim)}
.gen__bar dd{margin:0;display:grid;grid-template-columns:minmax(0,1fr) 30px;
  align-items:center;gap:9px}
.gen__track{position:relative;display:block;height:16px;background:var(--sky-pale);
  border:2px solid var(--key);border-radius:9px;overflow:hidden}
.gen__track i{position:absolute;inset:0 auto 0 0;
  background:linear-gradient(180deg,var(--acc-hi),var(--acc))}
.gen__bar dd b{text-align:right;font:800 13px var(--ui);color:var(--ink);
  font-variant-numeric:tabular-nums}

.gen__facts{display:flex;flex-wrap:wrap;gap:8px}
.gen__fact{display:flex;align-items:baseline;gap:6px;padding:7px 12px;background:#fff;
  border:2.5px solid var(--key);border-radius:11px;box-shadow:0 3px 0 var(--key)}
.gen__fact b{font:900 italic 24px var(--display);color:var(--ink);line-height:1}
.gen__fact span{font:600 12px var(--ui);text-transform:uppercase;letter-spacing:.06em;
  color:var(--ink-dim)}

/* ── ONE ROW OF ACTIONS, NOT TWO BANDS ───────────────────────────────────
   The three utility links were a band under the picture and the two buttons
   were a numbered tile under THAT — 250px of chrome below a picture with 424px
   to live in. They are one row now: the primary pair and the utilities beside
   them, centred under the thing they act on.

   It is also the correct hierarchy for this screen. There is ONE thing to do
   here — post it — so SHARE IT ON X is the only loud control and Save / Copy /
   Watch again are the site's quiet white chips beside it. */
.gen__act{flex:none;display:flex;align-items:center;justify-content:flex-start;
  gap:10px 14px;flex-wrap:wrap}
.gen__go{display:flex;align-items:center;gap:10px;flex-wrap:wrap;min-width:0}
.gen__go .gen__btn--rip{margin-left:0}

/* the links under the card sit ON THE SKY, not on a panel — plain underlined
   text disappeared into the lobby art behind them, so they wear the site's own
   white chip. 44px is the touch floor and they were 36 — free here, since the
   row's height is set by the 45px buttons beside them either way. */
.gen__cardfoot{display:flex;gap:10px;flex-wrap:wrap}
.gen__cardfoot .gen__lnk{display:inline-flex;align-items:center;min-height:44px;
  padding:0 14px;background:#fff;border:2.5px solid var(--key);
  border-radius:11px;box-shadow:0 3px 0 var(--key);color:var(--ink);text-decoration:none;
  font:800 14px var(--ui)}
.gen__cardfoot .gen__lnk:active{transform:translateY(3px);box-shadow:0 0 0 var(--key)}

/* THE REFERRAL LINE KEEPS A GROUND OF ITS OWN. Inside the old step tile it was
   body copy on white; loose on this page it would be ink-dim text over the
   lobby art, which is the contrast gamble the chips above exist to avoid. */
.gen__refline{flex:none;align-self:stretch;margin:0;padding:8px 13px;background:#fff;border:2.5px solid var(--key);border-radius:11px;
  box-shadow:0 3px 0 var(--key);font:600 13px/1.35 var(--ui);color:var(--ink-dim)}

/* ── THE PHONE STACKS, AND IS WIDTH-DRIVEN ───────────────────────────────
   At 390 the two columns cannot both be real, and the window is not the
   constraint — the column is. So the card goes back INTO the flow at a width
   that leaves the figures room to breathe underneath, and the body may scroll:
   a portrait card and three bars and five chips do not fit one phone screen,
   and crushing the card to make them is the wrong trade. */
@media (max-width:760px){
  .gen--done{height:auto}
  .gen--done .gen__top{flex-direction:column;align-items:center;gap:16px}
  .gen__side{width:100%;gap:14px}
  .gen--done .gen__card{min-height:0;width:min(62%,260px)}
  .gen--done .gen__card canvas{position:static;width:100%;height:auto;
    max-width:100%;max-height:none;margin:0}
  /* NO CAP HERE, AND THE TWO ARE NO LONGER THE SAME SIZE ON A PHONE. Stacked,
     this picture is the column's width and nothing else — 202x342 at 390x844 —
     while the ceremony's stage is height-driven off the viewport
     (min(52vh,460px), above) and its slab is now 0.930 of that: 406 tall. They
     used to agree here by accident, at 193 against 202, because the ceremony
     was small; the push-in has taken the ceremony up and left this where it is.
     So land() puts the slab down 16% smaller on a phone, inside a 520ms flight
     that is fading as it travels — and the page itself is unchanged.
     Matching them needs this column at ~76% instead of 62%, which is 66px more
     of a page that already scrolls, and the 62% is deliberate (above: the
     figures get room and the card is not crushed to buy it). That is Luke's
     trade to make, not a number to quietly move. */
  .gen__stats{width:100%;align-items:stretch}
  .gen__who{text-align:center;-webkit-text-stroke-width:4px}
  .gen__meta,.gen__facts{justify-content:center}
  .gen__act{justify-content:flex-start}
  .gen__go{width:100%}
  .gen__go .gen__btn{flex:1 1 auto;justify-content:center}
  .gen__refline{max-width:none;align-self:stretch}
}

/* ---- the steps --------------------------------------------------- */
.gen__steps{display:grid;gap:14px}
.gen__step{background:#fff;border:2.5px solid var(--key);border-radius:var(--r-card);
  box-shadow:0 3px 0 var(--key);padding:15px 17px 17px}
.gen__step h3{display:flex;align-items:center;gap:10px;margin:0 0 11px;
  font:900 italic 23px/1 var(--display);text-transform:uppercase;letter-spacing:.02em;color:var(--ink)}
/* the step number is a drawn chip, not a glyph */
.gen__step h3 i{flex:none;width:28px;height:28px;display:grid;place-items:center;font-style:normal;
  background:var(--navy);color:#fff;border:2.5px solid var(--key);border-radius:9px;
  font:800 16px var(--ui);line-height:1}

.gen__btn{display:inline-flex;align-items:center;gap:9px;padding:11px 18px;
  background:var(--navy);color:#fff;border:2.5px solid var(--key);border-radius:var(--r-btn);
  box-shadow:0 3px 0 var(--key);font:900 italic 18px var(--display);text-transform:uppercase;
  letter-spacing:.04em;cursor:pointer;line-height:1;transition:transform .08s,box-shadow .08s}
.gen__btn:active{transform:translateY(3px);box-shadow:0 0 0 var(--key)}
.gen__btn[disabled]{opacity:.55;cursor:default;transform:none;box-shadow:0 3px 0 var(--key)}
.gen__btn svg{flex:none}
.gen__btn--ghost{background:#fff;color:var(--ink)}
.gen__btn--go{background:linear-gradient(180deg,var(--acc-hi),var(--acc) 48%,var(--acc-lo));color:#fff}
.gen__btn--rip{background:linear-gradient(180deg,#FFE27A,var(--gold) 52%,#C58F04);color:var(--key);margin-left:10px}
@media (max-width:420px){.gen__btn--rip{margin:10px 0 0}}

.gen__lnk{background:none;border:0;padding:0;font:800 15px var(--ui);color:var(--acc-lo);
  text-decoration:underline;text-underline-offset:3px;cursor:pointer}
.gen__lnk[disabled]{opacity:.55;cursor:default;text-decoration:none}

.gen__hint{margin:10px 0 0;font-size:14px;line-height:1.4;color:var(--ink-dim);font-weight:500}
.gen__note{padding:40px 0;text-align:center;font-weight:600;color:var(--ink-dim)}

.gen__done{display:flex;align-items:center;gap:9px;flex-wrap:wrap;
  padding:10px 13px;background:var(--yellow);border:2.5px solid var(--key);border-radius:11px}
.gen__done b{font:800 17px var(--ui)}
.gen__done span{font-size:14px;color:var(--ink-dim);font-weight:600}

/* linked wallets */
.gen__wal{list-style:none;margin:0 0 11px;padding:0;display:grid;gap:7px}
.gen__wal li{display:flex;align-items:center;justify-content:space-between;gap:12px;
  padding:8px 11px;background:var(--sky-pale);border:2px solid var(--key);border-radius:10px}
.gen__wal code{font:800 15px ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.02em}

/* what each platform came back with */
.gen__plats{margin:13px 0 0;display:grid;gap:6px}
.gen__plats div{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  padding:7px 11px;background:#F4F8FE;border:2px solid var(--key);border-radius:10px}
.gen__plats dt{font:800 15px var(--ui)}
.gen__plats dd{margin:0;font:800 16px var(--ui)}
.gen__plats dd span{font-weight:600;font-size:13px;color:var(--ink-dim)}
.gen__plats dd em{font-style:normal;font-size:12px;font-weight:800;color:var(--acc-lo)}

.gen__tierline{margin:12px 0 0;font-size:15px;font-weight:600;line-height:1.4}
.gen__tierline b{font:900 italic 20px var(--display);text-transform:uppercase;letter-spacing:.02em}
.gen__paid{margin:7px 0 0;font-size:14px;font-weight:700;color:#1E9E63}

/* ═══════════════════════════════════════════════════════════════════
   THE OPENING — generate, connect X, hold, and the pack tears open.

   Luke, 16 Sep: "they hit some kind of generate card button, connect to their
   x account... and then the hold thing firers... the pack opens when its
   ready... and then card comes out and the slab slams into it."

   Same two-column grid as the finished page — the WebGL stage stands exactly
   where the card will be, so the card does not jump across the screen at the
   moment it arrives. NOTHING HERE INVENTS A COLOUR: every value is a token.
   ═══════════════════════════════════════════════════════════════════ */

/* the stage takes the .gen__card slot, at the card's own portrait ratio rather
   than the share image's 16:9 — a slab is taller than it is wide and this is
   the object, not the post about it */
/* NO PLATE, NO GROUND, NO KEYLINE (Luke, 16 Sep: "it looks shit in the blue
   bock make the background transparent"). card-reveal.js builds its renderer
   with alpha:true ON PURPOSE and says why in its own header — "the background
   is the PAGE's ... which is exactly how the product composites its stage"
   (#stageLayer over bg-plate.png). A panel behind it is a second ground
   arguing with the site's own, and the site's wins everywhere else.

   HEIGHT-DRIVEN, NOT WIDTH-DRIVEN. Stretched across its grid column at 4/5 the
   stage came out 704x880 in Luke's 1512x982 window — 90px taller than the body
   can show, so the pack ran off the bottom of the one screen it is the subject
   of. The height is capped to the viewport and the width follows from the
   ratio, so the whole pack is always in frame at any window size. */
/* ── AS BIG AS THE WINDOW ALLOWS (Luke, 16 Sep: "the pack is way too small.
   It could be way bigger") ──────────────────────────────────────────────────
   62vh was a guess that happened to be right at 1147x655 and wasted the rest.
   MEASURED at 1920x1080: the box came out 512x640 with 806px of body to stand
   in — 166px thrown away — and the PACK inside it rendered 287x508 (read off
   the GL buffer's alpha, not off a screenshot: the pane returns stale ones).

   The camera's FOV is vertical, so the pack's rendered height is proportional
   to the BOX's height and to nothing else — which makes this the only lever
   that makes the pack bigger, and the width it drags along is transparent.
   So the height is what the window actually has left, measured the same way
   the card page's cap is: 100vh less the header's bottom (78), the frame's
   approved 58, the head, the body's 18 + 44 of padding, and the caption's 26.

   The pack fills 79% of the box's height at any size; the remaining fifth is
   card-reveal.js's own framing margin and the card needs it on the way out. */
.gen__stage{position:relative;aspect-ratio:4/5;height:min(calc(100% - 26px),900px);
  width:auto;justify-self:center;background:none;border:0;box-shadow:none;margin-bottom:26px}
.gen__stage canvas{position:absolute;inset:0;width:100%;height:100%;display:block}

/* ── THE WAIT, AND WHY IT IS NOT A PICTURE OF A PACK ─────────────────────
   It WAS bp_grail.png, the wrapper's own artwork, standing in for the seconds
   before three.js draws — "there should always be a pack" (Luke, 14 Sep).
   But a flat image cannot line up with the pack the reveal frames: measured on
   eval, the still came out 62x113 against a 119x149 canvas, so both visible at
   once is two packs at two sizes. Luke, 16 Sep: "the flat design is sitting in
   front of the actual pack."

   So the wait is the site's OWN loader — the same ring index.html spins in
   #packLoader while the 3D pack boots, which is this exact wait on the lobby.
   Nothing pretends to be the pack except the pack. */
.gen__wait{position:absolute;inset:0;display:grid;place-items:center;
  pointer-events:none}
.gen__wait[hidden]{display:none}

/* ── THERE IS NO CAPTION ON THE STAGE, AND THERE IS NO PILL ─────────────
   Luke, 17 Sep: "them blsck pills at the bottom that say stuf like 'sealing
   pack' .. they ALL need to go." ALL of them — the four the rip wrote
   ("Opening your pack…", "Pulling your card…", "Sealing it…", "Your Rip
   Card.") and the pre-hold one as well, so the element is gone rather than
   blanked. .gen__cap and #genCap no longer exist anywhere.

   NOTHING IS LOST WITH IT, checked rather than assumed. The three states the
   pre-hold string carried are each already on their own step tile — step 1 is
   the GENERATE button until there is a card, step 2's hint is "Make your card
   first.", step 3's is "Two steps to go." / "Connect X and the pack is yours
   to open." — and the button itself says OPEN THE PACK above the hint "One tap
   and it tears open." (it said HOLD TO OPEN until 17 Sep). The four rip strings narrated an
   animation that is on screen while they were read. #genWait keeps the one
   aria-live region on this stage (role=status), which is the wait, not a pill.

   THE 26px RESERVATION STAYS, and it is now deliberately empty space. It is
   subtracted from the stage's height HERE and from the card page's picture
   ABOVE (the max-height calc), and the two surfaces agree at 0.930 of what is
   left — reclaiming it would have to happen on both at once, and the stage box
   is already at 94-97% of the room .ov__body has (card-fit.mjs asserts it), so
   there is nowhere for it to go. */

/* ── THE OPEN BUTTON ─────────────────────────────────────────────────────
   IT IS A PLAIN BUTTON SINCE 17 Sep (Luke: "no need to hold it as there is no
   money involved"). It was hold.js's press-and-hold, which wrapped it in
   .hring.hring--inline and hung a traced ring outside it — so the wrapper
   sizing and the ring's eased glow both lived on `.hring>.gen__hold` and are
   now unreachable. THE SHADOW AND THE PRESS ARE RESTATED ON THE BUTTON ITSELF
   below: `--e` was hold.js's easing variable and nothing sets it any more, so
   a calc() naming it is invalid at computed-value time and drops the whole
   declaration — the button would have had no shadow at all.
   It keeps RIP PACK's gold: this is still the same object being opened.
   .hring--inline stays for any other inline ring on this page. */
.gen__step .hring--inline{--hring-gap:5px;display:block;width:100%;min-width:0}
.gen__hold{display:block;width:100%;padding:15px 18px;border:2.5px solid var(--key);
  border-radius:var(--r-btn,12px);cursor:pointer;
  font:900 italic 25px var(--display);text-transform:uppercase;letter-spacing:.02em;
  color:var(--key);background:linear-gradient(180deg,#FFE27A,var(--gold) 52%,#C58F04);
  box-shadow:0 5px 0 0 rgba(11,15,24,.30),
  0 14px 40px -16px rgba(var(--acc-rgb),.55),
  0 0 18px rgba(var(--acc-rgb),.26)}
.gen__hold:active{transform:translateY(2px);box-shadow:0 2px 0 0 rgba(11,15,24,.30),
  0 14px 40px -16px rgba(var(--acc-rgb),.55),
  0 0 18px rgba(var(--acc-rgb),.26)}

.gen__hint--wait{opacity:.7}
.gen__hint .gen__lnk{font-size:14px}

/* ── WHILE IT IS PLAYING, THE SEQUENCE IS THE PAGE ───────────────────────
   The steps are done with — their job was to get here — and they go rather
   than grey out: a disabled button beside a playing animation is an invitation
   to interrupt it. They LEAVE, they do not vanish (Luke, 16 Sep: "when ye hold
   rip button and the pack moves over it should slide over... not just apear").

   ── THE PACK MUST NOT CHANGE SHAPE ON THE WAY (Luke, 16 Sep: "it looks like
   the shape of the pack changes as well") ────────────────────────────────────
   It did, and this was why. The open state used to set `aspect-ratio:auto;
   width:100%`, so the box went from a 4:5 portrait to the full row — 512x640 to
   1604x720 at 1920x1080 — WHILE the canvas's backing store stayed at the size
   fit() last gave it. A canvas whose CSS box no longer matches its buffer is
   stretched to fit, so the pack was literally squashed wider for the length of
   the transition. (card-open.js now also re-fits when the box moves, so this
   cannot come back by another route.)

   It also made the finished slab SMALL (Luke: "the card comes out the pack and
   is way too small... i cant see it"): the camera's FOV is vertical, so a
   1604-wide box renders the slab no taller than a 512-wide one — it just
   surrounds it with 1100px of empty canvas.

   SO THE BOX NEVER CHANGES. It keeps its ratio and its height, and the motion
   is the STEPS COLLAPSING and the stage gliding to the middle as they go —
   which is what "moves over" actually means. .gen--open is a flex row for that:
   a flex-basis animates and a grid column does not.

   ── AND SLOWLY (Luke: "it moves over too fast to the centre. It should move
   over slowly") ─────────────────────────────────────────────────────────────
   420ms was a UI transition's duration on a gesture that is meant to feel
   ceremonial. 900ms on the collapse, with the tiles fading out over the first
   half so the space is already clearing before they are gone.

   ── BUT IT CANNOT BE A LAYOUT ANIMATION, AND IT WAS ONE ──────────────────────
   Luke, 17 Sep: "like the pack moves over its not smooth."

   It was not a transform. This rule used to transition `width` and `margin` on
   the steps, and the steps are a FLEX SIBLING of the stage in a row that is
   `justify-content:center` — so every frame of those 900ms reflowed the row and
   moved the stage BY LAYOUT. Style, layout, paint and composite on all 54
   frames, for a move that is 264px of pure translation. ui-ux-pro-max's own
   `transform-performance` rule: "Use transform and opacity for animations /
   Don't animate width/height/top/left".

   SO NOTHING IN THE ROW REFLOWS NOW. The steps keep their box for the whole
   move and leave on `transform` + `opacity`; the stage keeps its LAYOUT
   position and is translated the measured distance by generator.js's glide()
   — measured, because `min(606px,42%)` binds at 606 on a 1920 window and at
   42% on a 1512 one, and on the stacked phone row the stage does not move at
   all. The layout only changes once, on the frame `1` arrives, and glide()
   clears the transform in that same style update so there is nothing to see.

   AND THE PACK PASSES IN FRONT (z-index:2). With the steps still in flow they
   are still under the pack's path, and a transformed element with z-index:auto
   is painted with the positioned layer — so DOM order would have put the
   fading tiles ON TOP of the hero object. 2 is this file's own scale (.ov__body
   is 1, .gen__fly is 80), not an arbitrary large number.

   THE SECOND-ORDER COST THAT MUST NOT COME BACK: a canvas whose CSS box moves
   under a stale backing store is stretched, not redrawn (Luke, 16 Sep: "it
   looks like the shape of the pack changes as well"). A transform does not
   change clientWidth/clientHeight, so card-open.js's per-frame box check does
   not fire and renderer.setSize is never called. Translate; do not resize. */
/* FROM THE CONTAINER, NOT FROM THE VIEWPORT. A vh constant cannot serve both of
   Luke's windows: measured, the chrome above this row is 166px at 1147x655 and
   230px at 1920x1080 — the site header is 47 against 78 and the page head 43
   against 76 — so `100vh - 268` fitted the big one and left 58px unused on the
   short one. .ov__body's content box IS the room, and the panel is a flex
   column with a definite height, so a percentage resolves against it and the
   stage simply takes what there is at any size. */
.gen--open{display:flex;align-items:flex-start;justify-content:center;gap:26px;
  height:100%;min-height:0}
.gen--open .gen__stage{flex:0 0 auto;z-index:2}
.gen--open .gen__steps{flex:0 1 auto;width:min(606px,42%);min-width:0}
/* stacked, the steps sit UNDER the stage and the row is as tall as both, so the
   percentage has nothing definite to resolve against — back to the viewport */
@media (max-width:1023px){.gen--open{flex-direction:column;align-items:center;height:auto}
  .gen--open .gen__steps{width:100%}}
.gen[data-open="going"] .gen__steps,
.gen[data-open="1"] .gen__steps{opacity:0;transform:translateX(38px);
  pointer-events:none}
/* the transitions live on `going` ONLY, so the frame `1` arrives on is a hard
   swap: the steps go display:none and glide() drops the stage's transform in
   the same style update, and neither can animate back out of it. */
.gen[data-open="going"] .gen__stage{transition:transform 900ms cubic-bezier(.4,0,.2,1)}
.gen[data-open="going"] .gen__steps{transition:transform 900ms cubic-bezier(.4,0,.2,1),
  opacity 420ms ease-in}
.gen[data-open="1"] .gen__steps{display:none}

/* the travelling slab (generator.js land()). Fixed, above the page, and out of
   the flow entirely so nothing reflows around it on its way across. */
.gen__fly{border-radius:10px;will-change:transform,opacity}

/* (0,2,0) to match the `going` rules above, which a bare `.gen__steps` would
   lose to. glide() also skips the stage's transform outright under this query,
   so the pack is simply in the middle rather than on its way there. */
@media (prefers-reduced-motion:reduce){
  .gen[data-open] .gen__steps,.gen[data-open] .gen__stage{transition:none}
}

/* ── THE STEP YOU ARE ON ─────────────────────────────────────────────────
   Three numbered tiles with no current-state marker is a progress indicator
   that does not indicate progress. The live step wears the accent on its
   numeral and a heavier keyline; the ones behind it grey their numeral and
   keep everything else, so the row does not move as it advances. */
.gen__step[data-state="done"] h3 i{background:#1E9E63;color:#fff}
.gen__step[data-state="now"]{box-shadow:0 3px 0 var(--key),0 0 0 3px var(--acc)}
.gen__step[data-state="next"]{opacity:.62}
.gen__step[data-state="next"] h3 i{background:var(--ink-dim)}

/* the skip is a real target, not four words of body copy: 44px is the floor and
   an inline <button> in a <p> does not get there on its own */
.gen__hint .gen__lnk{font-size:14px;min-height:44px;padding:0 4px;
  display:inline-flex;align-items:center;vertical-align:middle}

/* ── REDUCED MOTION ──────────────────────────────────────────────────────
   The sequence IS the feature, so it is not silently removed — card-open.js
   checks the same query and hands the card straight over instead of tearing a
   pack at somebody who asked not to be moved. This only tidies what is left. */
@media (prefers-reduced-motion:reduce){
  .gen__still{filter:none}
}

/* ── A SHORT WINDOW: THE TILES GIVE BACK WHAT THE STAGE CANNOT ───────────
   MEASURED at 1147x655, and only in the state BEFORE X is connected — which is
   the tall one, because step 2 is then a button and a two-line hint rather than
   a done row. The column came to 468 against the body's 445 and the ceremony
   scrolled by 23. The stage was not the culprit and shrinking it would not have
   helped: the steps are the taller of the two columns there.
   So the tiles tighten, by 25px across the three, and nothing else moves. */
@media (max-height:760px) and (min-width:1024px){
  .gen--open .gen__steps{gap:11px}
  .gen--open .gen__step{padding:11px 15px 12px}
  .gen--open .gen__step h3{margin-bottom:8px;font-size:21px}
  .gen--open .gen__hint{margin-top:7px;font-size:13px}
}

@media (max-width:1023px){
  /* stacked: the steps are UNDER the stage here, so the stage cannot have the
     window's whole height the way it does on the desktop */
  .gen__stage{aspect-ratio:4/5;height:min(52vh,460px)}
  .gen__hold{font-size:22px}
}
