/* =========================================================================
   XBOX Cloud Gaming — prototype page styles
   Design system (tokens, atoms, buttons) comes from the dotcom-refresh
   submodule: vendor/dotcom-refresh/styles.css. Only page-specific layout
   lives here.
   ========================================================================= */

/* Accent used as *text*. Lime reads well on dark surfaces but fails contrast on
   light ones, so the light theme swaps it for a darker green (see below). */
:root{
  --accent-text:var(--accent);
  /* The plate colour for tiles that sit dark on the light page. A token rather
     than a literal because more than one surface can use it and they have to
     stay in step. Deliberately outside the theme block: these tiles are dark by
     design, not because the page happens to be light. */
  --plate-dark:#1a1b1e;

  /* The vendor's .lift transition (vendor/dotcom-refresh/styles.css:146), as a
     token. `transition` is a shorthand, so any rule that outranks .lift and
     sets its own transition drops .lift's three properties entirely unless it
     restates them -- and three separate rules on this page have to. They were
     three hand-copied lists, and one of them silently fell out of step: the
     .opt-quiet wash lost to .has-reveal .card[data-reveal] and its background
     never transitioned at all. Referencing one token means a retime lands
     everywhere at once instead of in two places out of three. */
  --tx-lift: transform .22s cubic-bezier(.2,.7,.2,1), border-color .22s ease,
             box-shadow .22s ease;
  /* The two extra properties the dark tile wash animates on top of the above. */
  --tx-wash: background-color .22s ease, color .22s ease;
  /* The vendor's .btn transition (vendor/dotcom-refresh/styles.css:121), for the
     same reason: the hero button adds colour properties to its hover, and adding
     them means restating these three or losing the lift's easing. */
  --tx-btn: transform .18s ease, box-shadow .18s ease, filter .18s ease;

  /* Body copy size. A token because the get-started step titles are now set at
     body size too, and they are in a different rule a long way down the file —
     two hand-copied clamps that must agree is exactly how the .lift transition
     above drifted. Change here and the light-surface card paragraphs and the
     step titles move together. The dark c4 tiles override to a fixed 15px and
     will not follow. */
  --body-copy:clamp(15px, 1vw, 18px);
}

/* Gutters and content width are inherited 1:1 from dotcom-refresh:
   .wrap = max-width var(--maxw) 1980px, padding 0 72px. Nothing overrides it. */

/* ---------- shared nav overrides (vendor rules live in the generated chrome.css) ---------- */
/* The vendor nav is translucent at rest and fades to 85% black once you scroll
   past 24px. With the hero video running behind it that swing is very visible,
   so the nav is pinned to the darker of the two states and no longer reacts to
   scroll. chrome.js still toggles `.scrolled`, but the class is now inert here:
   the only other rule keyed on it is `.hdr:not(.scrolled) .search`, and this page
   has no `.search` field — the header uses the #searchPill button instead.
   The selector deliberately mirrors the vendor's own specificity — styles.css
   loads after chrome.css, so matching selectors win on source order. */
.hdr, .hdr.scrolled{ background:rgba(0,0,0,.85); }

/* ---------- Cloud Gaming sub-bar (page-specific, sits under the shared nav) ---------- */
.subnav{ border-top:1px solid var(--rule-soft); border-bottom:1px solid var(--rule-soft); background:rgba(0,0,0,.55); }
.subnav .row2{
  display:flex; align-items:center; gap:26px;
  min-height:58px; max-width:var(--maxw); margin:0 auto; padding:8px 72px;
}
.subnav-links{ display:flex; align-items:center; gap:26px; position:relative; }
/* The brand reads as the bar's title at full width, so it sheds every button
   affordance: no chevron, no pointer, and inert to the mouse so it cannot be
   mistaken for a link. The narrow range below hands all of that back. */
.subnav-brand{
  display:inline-flex; align-items:center; gap:8px;
  appearance:none; background:none; border:0; padding:0;
  font:inherit; font-size:14px; font-weight:700; color:var(--ink);
  pointer-events:none;
}
.subnav-brand .chev{ display:none; width:16px; height:16px; }
.subnav-menu{ display:flex; align-items:center; gap:26px; }
.subnav-links a{ font-size:14px; color:var(--ink-2); transition:color .15s; }
.subnav-links a:hover{ color:var(--accent); }
.subnav-links a.here{ color:var(--ink); font-weight:700; }
.subnav .cta{ margin-left:auto; display:flex; align-items:center; gap:12px; }
.linknote{ font-family:var(--mono); font-size:10px; letter-spacing:.04em; color:var(--ink-3); }
.linknote b{ color:var(--ink-2); font-weight:600; }

/* ---------- in-page anchors ---------- */
/* The sub-bar links jump to sections behind a sticky header, so every anchor
   target reserves the header's height (plus a little air) above itself.
   --hdr-h is measured at runtime by the inline script in index.html; the
   fallback is the desktop header height.

   #faq has no sub-bar link, but it is still a linkable target, so it takes the
   same offset — otherwise a jump to it lands underneath the header. */
html{ scroll-behavior:smooth; }
#cloud-gaming, #devices, #how, #ways, #faq{ scroll-margin-top:calc(var(--hdr-h, 115px) + 20px); }

@media (prefers-reduced-motion:reduce){
  html{ scroll-behavior:auto; }
}

/* ---------- section scaffolding ---------- */
/* Vertical rhythm matches dotcom-refresh's .explore section: 38px top,
   18px bottom, and a 24px gap under the head block. Title-led sections
   ("Getting started is easy", "Choose your way to play", the FAQ) get a
   larger run-up.

   --sec-runup sits here rather than on .major because three different rules
   read it: the major sections' top padding, the FAQ's foot against the dark
   band, and any section given .sec-band. Declared once on the base so none of
   them has to restate the clamp. */
.sec{
  --sec-runup:clamp(64px, 5.6vw, 104px);
  padding:38px 0 18px;
}
/* The title-led sections carry real air above and below now. They were 64px
   top against the base 18px bottom, so two of them in sequence were separated
   by 82px — less than the height of one step numeral, which read as three
   blocks butted together rather than three sections.

   Both ends are clamped on 5.6vw, the same slope .sec-title rides. The run-up
   above a title and the title itself then grow together instead of the space
   closing up as the type gets bigger. The bottom takes three quarters of the
   top: enough to separate a section from the next one's run-up, without the
   two ends reading as equal and leaving each section floating.

   Consecutive majors now sit 168px apart at 1712px, against 82px before. */
.sec.major{
  padding-top:var(--sec-runup);
  padding-bottom:clamp(48px, 4.2vw, 78px);
}

/* A section on its own band. The section box is already full-bleed — .wrap
   inside it is what holds the content to the page measure — so a background
   here is the band, with no extra element and no negative margins.

   --paper-2 rather than a literal white: it is the page's raised surface, the
   same one the option cards take, so the band and the cards cannot drift apart,
   and the dark theme gets a raised graphite rather than a white slab.

   One measure, --band-air, for all four gaps: the grey above the band, the
   grey below it, and the white inside it at each end. They started at one
   run-up outside against two inside, which read as a band crowding the tiles
   above it while its own contents floated. 1.5 is the midpoint — the outside
   grew, the inside came down, and nothing now has a different value to drift
   from.

   The band owns both outer gaps rather than inheriting them from its
   neighbours, which is what the two rules below are for. Otherwise the gap
   above would be whatever foot the previous section happened to carry and the
   gap below whatever run-up the next one does — two numbers the band cannot
   see and cannot keep equal.

   Two classes deep, and after .sec.major, because that rule is also two
   classes and sets padding of its own: a single .sec-band would lose the tie. */
.sec.sec-band{
  /* Outside the band and inside it are separate measures. They were one, which
     balanced all four gaps exactly, but the white then read as a field the
     content was adrift in — the inside comes down a step while the grey either
     side holds. */
  --band-air:calc(var(--sec-runup) * 1.5);
  /* 1.425 rather than 1.25, and symmetric.

     This end of the band briefly carried an optical correction instead. The
     content is top-heavy — the head paints roughly four times the ink of all
     three step titles and sits high, which put the ink's centre of mass 26.3px
     above the band's midpoint while both paddings measured the same. Shifting
     the block down fixed that by the mass measure and made the two paddings
     visibly unequal, which turned out to be the louder of the two signals.

     So the band keeps equal ends, at the larger of the two values the
     correction produced. The block still sits fractionally high by weight;
     that is a known and accepted trade, recorded in bandpad-check rather than
     left to be rediscovered. */
  --band-inset:calc(var(--sec-runup) * 1.425);
  background:var(--paper-2);
  padding-block:var(--band-inset);
  margin-block-start:var(--band-air);
}
/* :has() is what lets the band take both gaps without hard-coding which
   sections sit either side of it, and it holds whether those neighbours are
   base sections or majors with much larger padding of their own. Both are
   placed after .sec.major so the matching specificity resolves this way. */
.sec:has(+ .sec-band){ padding-bottom:0; }
.sec-band + .sec{ padding-top:0; }
.sec-label{
  display:flex; align-items:center; gap:12px;
  font-family:var(--mono); font-size:11px; font-weight:600;
  letter-spacing:.14em; text-transform:uppercase; color:var(--ink-2);
  margin:0 0 24px;
}
.sec-label::before{ content:""; width:18px; height:2px; background:var(--accent-text); border-radius:2px; }
/* Doubled from 40px, then pulled back in stages: at 80px the section titles sat
   within 4px of the 84px hero line and the two read as the same rank. 64px put
   them clearly second, and 60px then 56px trim them further so the three
   section heads sit quieter against the panel headings they neighbour.

   Only the ceiling moved. The ramp and the 40px floor are unchanged, so this
   reads as -8px from about 1000px up (where 5.6vw passes 56px) and changes
   nothing below that — narrow viewports were already sized deliberately.

   A fixed size was fine at 40 but not here — "Frequently asked questions" is 29
   characters and would run off a narrow viewport — so the size is clamped and
   the floor holds the old 40px for small screens. text-wrap:balance is the same
   treatment .hero-sub gets: at this size a title that wraps should split evenly
   rather than leave one orphaned word. */
.sec-title{
  font-family:var(--display); font-weight:700;
  font-size:clamp(40px, 5.6vw, 56px); letter-spacing:-.035em; line-height:1;
  margin:0 0 24px;
  text-wrap:balance;
}

.sec-title.sub {
  font-size:clamp(24px, 1.9vw, 32px);
  margin:48px 0 24px;
}

/* ---------- hero ---------- */
/* The hero starts below the sticky nav rather than running up behind it. `.hdr`
   is opaque (see the pin above), so any part of the poster rail tucked under it
   would simply be hidden. The section claims its own space in the flow. */
.hero{ position:relative; overflow:hidden; }

/* ---------- hero poster rail ---------- */
/* Replaces the hero video. A marquee of 2:3 game posters running straight on the
   page — no plate behind it, so the art is the only thing on the band and is
   lifted off --paper by its own shadow. Sized and paced from these three numbers
   alone; the script measures whatever they produce rather than assuming any. */
.hero-rail{
  /* The base width, before the fold cap below can pull it down. Held as its own
     token so the phone rule can reference it instead of restating the clamp —
     two hand-copied clamps that must agree is how the .lift transition drifted. */
  --poster-w-base:clamp(280px, 26vw, 420px);
  /* Fold cap. --poster-w is a width, but the posters are 2:3, so this is really
     a height decision: the poster gets whatever the viewport has left once
     everything else in the hero has taken its share.

     --hero-chrome is that share — header, rail padding, headline, sentence and
     button — published by the rail script, which measures it from the DOM. It
     is deliberately measured rather than written down here. The figure ramps
     with width (351px at 560, 517px from 1500 up) as the type clamps open, and
     it moves whenever the hero copy does: this page has already re-flipped its
     type hierarchy and rewritten the sentence once each, and a constant would
     have gone quietly stale on both occasions while still looking authoritative.
     Measuring also makes the phone case fall out of the same rule instead of
     needing its own breakpoint and its own number.

     There is no feedback loop to worry about. Every term in --hero-chrome sits
     above or below the poster and none of them depend on its height, which is
     why the measured figure holds still against viewport height (0px of spread
     across every height tested) while varying with width.

     min() against the base, so this can only ever pull the poster down: where
     the viewport is tall enough the clamp already wins and none of this
     applies. max() puts a floor under it so a short window cannot drive the
     width to zero.

     svh rather than vh deliberately. vh is the large viewport — toolbar hidden
     — which is not what the reader has on first paint, and sizing to it would
     put the button back under the fold on exactly the load this is meant to
     fix. svh is also stable while scrolling, so the rail cannot resize as the
     toolbar retracts.

     The 520px fallback is what applies before the script has run, or if it
     never does. It is the largest chrome measured at any width, rounded up from
     517: without measurement the only safe assumption is the worst case, since
     guessing low is the bug this cap exists to fix. Guessing high instead costs
     some poster on narrow viewports, which is the cheaper mistake and only
     lands in the no-script path, where the rail is a static strip anyway. */
  --fold-clearance:24px;
  --poster-w:min(
    var(--poster-w-base),
    max(96px, calc((100svh - var(--hero-chrome, 520px) - var(--fold-clearance)) / 1.5))
  );
  --poster-gap:clamp(12px, 1.2vw, 20px);
  /* A flat number rather than a responsive one, because it is only ever read at
     1920px and up (see the mask below) and the original clamp(32px, 7vw, 130px)
     is pinned to its 130px ceiling everywhere past 1857px. Both the floor and
     the vw term are unreachable at the widths that apply it, so this is the
     same fade that expression produced with the dead arms taken off. */
  --rail-fade:130px;
  /* Gap between one poster's entry and the next, left to right. */
  --poster-step:90ms;
  /* The posters' drop shadow, declared here rather than inline on .poster
     because .rail-viewport has to open its clip box by exactly the distance
     this shadow reaches — keeping both derived from one pair of numbers stops
     them drifting apart. */
  --poster-shadow-y:14px;
  --poster-shadow-blur:30px;
  /* How far the shadow actually paints beyond the poster's own box. Half the
     blur reaches out from each edge and the offset shifts that down, but
     Chrome's Gaussian carries a few pixels further than the nominal radius, so
     there is a tail term on top. Measured against these: 7px above, 35px
     below, which is what they resolve to at the values set here. */
  --poster-bleed-t:calc(var(--poster-shadow-blur) / 2 - var(--poster-shadow-y) + 6px);
  --poster-bleed-b:calc(var(--poster-shadow-blur) / 2 + var(--poster-shadow-y) + 6px);
  position:relative;
  /* Asymmetric: the space above the posters separates them from the opaque nav,
     but below they only need to clear the headline, and .hero-copy's own top
     padding adds to whatever is set here. */
  padding:clamp(26px, 3.2vw, 48px) 0 clamp(14px, 3.2vw, 72px);
}

/* Phone fold cap. --poster-w is a width, but the posters are 2:3, so on a phone
   it is really a height decision: below a 1077px viewport the 26vw term drops
   under the 280px floor and the clamp stops responding altogether, which left a
   360px phone carrying the same 420px-tall poster as a tablet. The rail came to
   460px of a 640px screen and pushed Play now 214px under the fold.

   So the poster is additionally capped by what the fold actually leaves:

     460px header + copy + rail padding + clearance
     116     + 279         + 40           + 12       = 447

   Copy is taken at its phone worst case — the hero sentence wraps to four lines
   at 375px and under, which is 42px taller than the three it takes from 390px
   up — so the cap is conservative on the wider phones rather than exact
   everywhere. It costs those a few percent of poster and buys one rule instead
   of a second breakpoint splitting 375 from 390.

   min() against the base, so this can only ever pull the poster down: on a tall
   phone the clamp already wins and nothing here applies. max() puts a floor
   under it so a landscape phone cannot drive the width to zero.

   svh rather than vh deliberately. vh is the large viewport — toolbar hidden —
   which is not what the reader has on first paint, and sizing to it would put
   the button back under the fold on exactly the load this is meant to fix. svh
   is also stable while scrolling, so the rail cannot resize as the toolbar
   retracts.

   Held to phone widths rather than applied globally: at a 1000px desktop
   viewport - an ordinary maximised 1080p window - a global cap would pull the
   poster from 420px to 367px, which is a visible change to a case nobody
   asked about. */
@media (max-width:560px){
  .hero-rail{
    --poster-w:min(var(--poster-w-base), max(96px, calc((100svh - 447px) / 1.5)));
  }
}

/* The clipping box. Horizontally this is what makes the marquee a strip and
   gives the mask something to fade at both ends. Vertically it used to sit
   flush with the posters, which cut their drop shadow off dead — the shadow
   reaches 35px below the art and none of it survived the clip.

   So the box is opened by exactly the shadow's reach and the space is taken
   straight back out of the flow with a matching negative margin: the clip is
   taller, the layout is not. (overflow-x:hidden with overflow-y:visible is not
   an alternative — CSS promotes the visible axis to auto, which would put a
   scrollbar on the band.)

   Note this is no longer the hover target. The padding would have extended the
   pause region into empty space below the rail, so that moved to .poster-track,
   whose box is exactly the posters — see below. */
.rail-viewport{
  padding-top:var(--poster-bleed-t);
  padding-bottom:var(--poster-bleed-b);
  margin-top:calc(-1 * var(--poster-bleed-t));
  margin-bottom:calc(-1 * var(--poster-bleed-b));
  overflow:hidden;
}

/* The edge fade is a wide-viewport treatment only, by request: full-HD desktops
   and up. Note the reason is editorial, not geometric — wherever it applies the
   fade covers a constant 31% of the poster it lands on, so nothing in the
   layout singles 1920 out. What changes is how much strip there is to look at.
   At 1024px about three and a half posters are on screen, so hazing the two at
   the ends dims most of the art; by 1920 there is enough behind the fade for it
   to read as the strip carrying on past the edge rather than as a veil over the
   only posters there are.

   Written as min-width rather than as a max-width override so the rail's
   default state is simply "no mask", and the fade is the thing that gets added
   at size — one declaration to find rather than one that sets it and a second
   that takes it away. */
@media (min-width:1920px){
  .rail-viewport{
    -webkit-mask-image:linear-gradient(90deg, transparent 0, #000 var(--rail-fade), #000 calc(100% - var(--rail-fade)), transparent 100%);
            mask-image:linear-gradient(90deg, transparent 0, #000 var(--rail-fade), #000 calc(100% - var(--rail-fade)), transparent 100%);
  }
}

/* --rail-shift and --rail-dur are published by the script once it has measured a
   set. Until then the fallbacks hold the strip still rather than letting it
   animate by a bogus distance — so with JS off the rail is simply a static row
   of posters instead of something visibly broken. */
.poster-track{
  display:flex; gap:var(--poster-gap);
  width:max-content;
  margin:0; padding:0; list-style:none;
  will-change:transform;
  animation:railScroll var(--rail-dur, 1s) linear infinite;
}
@keyframes railScroll{
  from{ transform:translate3d(0, 0, 0); }
  to  { transform:translate3d(calc(-1 * var(--rail-shift, 0px)), 0, 0); }
}
/* The explicit control is available to pointer, keyboard and touch users. */
.hero-rail.is-paused .poster-track{ animation-play-state:paused; }

.rail-toggle{
  position:absolute;
  z-index:2;
  inset-inline-start:clamp(16px, 2vw, 32px);
  bottom:calc(clamp(14px, 1.6vw, 24px) + 12px);
  display:grid;
  place-items:center;
  width:44px;
  height:44px;
  padding:0;
  border:1px solid rgba(255,255,255,.6);
  border-radius:50%;
  background:rgba(0,0,0,.72);
  color:#fff;
  cursor:pointer;
  box-shadow:0 8px 20px -8px rgba(0,0,0,.8);
  transition:background .15s ease, border-color .15s ease;
}
.rail-toggle[hidden]{ display:none; }
.rail-toggle:hover{
  background:rgba(0,0,0,.9);
  border-color:var(--accent);
  color:var(--accent);
}
.rail-toggle svg{
  width:20px;
  height:20px;
  fill:currentColor;
}
.rail-toggle-play{ display:none; }
.hero-rail.is-paused .rail-toggle-pause{ display:none; }
.hero-rail.is-paused .rail-toggle-play{ display:block; }

.poster{
  flex:0 0 auto;
  width:var(--poster-w);
  aspect-ratio:2/3;
  border-radius:14px;
  overflow:hidden;
  /* The rail has no plate of its own, so the art sits straight on the page and
     is lifted off it by shadow alone. This colour is only ever seen in the gap
     before an image decodes. */
  background:var(--paper-3);
  box-shadow:0 var(--poster-shadow-y) var(--poster-shadow-blur) rgba(20,22,26,.22);
}
.poster img{ display:block; width:100%; height:100%; object-fit:cover; }

/* Entry cascade. The posters come up left to right on load, gated on
   .has-reveal exactly like the device tiles further down: that class is set in
   <head>, so hidden is the first state a poster ever has and nothing animates
   backwards after first paint. --poster-delay is set per poster by the rail
   script, which is the only thing that knows which ones start on screen.

   The motion is a scale rather than a rise. .rail-viewport clips close to the
   posters — it now leaves room for their shadow, but that is only 7px at the
   top — so anything travelling vertically would still be cut off at the edge;
   scaling up from .94 stays inside the poster's own box on every frame. It is
   also a transition rather than an animation, which matters because the rail
   clones these nodes: a clone carrying .in simply renders finished, where a
   clone carrying an animation would replay it mid-scroll on every re-measure. */
.has-reveal .poster{
  opacity:0; transform:scale(.94);
  transition:opacity .62s cubic-bezier(.2,.7,.2,1), transform .62s cubic-bezier(.2,.7,.2,1);
  transition-delay:var(--poster-delay, 0ms);
}
.has-reveal .poster.in{ opacity:1; transform:none; }
/* Reduced motion keeps the posters, drops the entry: they are simply present. */
@media (prefers-reduced-motion:reduce){
  .has-reveal .poster{ opacity:1; transform:none; transition:none; }
}

/* Empty slots: deliberately unfinished-looking, so a missing poster reads as
   "art goes here" rather than as a bug. Flat and outlined rather than lifted —
   a placeholder should not pretend to be a piece of art. Delete .is-empty when
   the art lands. */
.poster.is-empty{
  display:grid; place-items:center;
  background:var(--paper-3);
  box-shadow:inset 0 0 0 1px var(--rule);
}
.poster-slot{
  font-family:var(--display); font-weight:700;
  font-size:clamp(20px, 2vw, 30px);
  color:var(--ink-4);
}

/* Copy sits directly below the rail, centered on the content rails. The old
   video's frame faded to white at its bottom edge and the copy block used to
   pick that white up and blend it down; the rail sits on the page background
   already, so there is nothing to blend from and the block just sits on
   --paper. */
.hero-copy{
  position:relative;
  text-align:center;
  padding-top:clamp(16px, 1.8vw, 26px); padding-bottom:64px;
}
/* The hero leads with the sentence, not the product name. The two lines keep
   their order — "XBOX Cloud Gaming" stays on top — but trade typographic roles,
   so the clamps, colours and measures swap wholesale rather than the font-size
   alone. Moving the size by itself would have left an 84px line at
   line-height:1.5 in --ink-2 across a 40ch measure, which sets as a large grey
   paragraph rather than a headline.

   The name takes what the deck line used to be: 24px at the cap, line-height
   1.5, --ink-2, regular weight. Demoting it this way is the page's own
   convention rather than a new idea — the three device panels already put
   "XBOX Cloud Gaming" above their heading as a label. It keeps the display face
   at regular weight instead of borrowing .kicker's mono/uppercase/green, which
   is drawn for 11-13px; at 24px those caps and .08em tracking stop reading as a
   quiet label and start competing with the line underneath.

   The sentence takes the headline's values verbatim, 20ch included. That
   measure is doing real work: at 84px the line sets to two from 20ch upward and
   breaks to three at 18ch, and text-wrap:balance then picks the same 766/905
   split at every width from 966px up, so the shape holds across the desktop
   range instead of reflowing as the window grows.

   The gap moves with the roles. It used to hang under the headline as .33em of
   the deck's own size; it now sits under the name as .5em of the name's size,
   so it still tracks the type it belongs to and still tightens with it on
   mobile. It cannot stay on .hero-sub — .33em of 84px is 28px, which unbinds
   the label from the line it introduces. */
.hero h1{
  /* Mono rather than the display face. The name is a label introducing the
     sentence below it, not a second headline, and setting it in the same family
     at a quarter of the size made it read as a shrunken version of the line it
     introduces. A different family separates the two roles without making the
     label any louder — which is what the earlier attempts at caps and tracking
     were reaching for, and why those were dropped.

     Size drops a step from the 16-24px ramp: --mono sets large on the shoulder,
     so it holds the same optical weight a couple of pixels smaller. */
  font-family:var(--mono); font-weight:400;
  font-size:clamp(14px, 1.4vw, 20px); letter-spacing:.04em; line-height:1.5;
  /* All caps. In mono this is a smaller step than it looks: the face is already
     even-width, so capitals mostly add height rather than changing the rhythm.
     The size comes down one more notch to pay for that height, and the tracking
     opens slightly — caps set tighter than lowercase at the same value because
     there are no descenders or x-height gaps to carry the eye between them. */
  text-transform:uppercase;
  margin:0 auto .5em; max-width:40ch; text-wrap:balance;
  /* The step numerals' green, through the shared token. Falls back to --ink-2
     on the dark theme, where the hero sits on video and this green is the wrong
     end of the ramp. */
  color:var(--olive-ink, var(--ink-2));
}
.hero-sub{
  margin:0 auto; max-width:20ch;
  font-family:var(--display); font-weight:700;
  font-size:clamp(40px, 5.8vw, 84px); letter-spacing:-.04em; line-height:1.04;
  color:var(--ink); text-wrap:balance;
}
/* The primary CTA repeats the subnav's Play now, so it reuses .btn verbatim
   rather than inventing a hero-only treatment — only the scale is raised here,
   since this is the page's lead action and the 13px chrome button reads as a
   utility control next to a 60px headline. */
/* Two actions now, so the row is an explicit centred flex rather than relying on
   .hero-copy's text-align. wrap lets the pair stack at narrow widths instead of
   forcing a horizontal scroll. */
.hero-cta{
  margin:28px 0;
  display:flex; flex-wrap:wrap;
  justify-content:center; align-items:center;
  gap:14px;
}

.btn .ico-play{
  flex:0 0 auto;
  width:1.05em; height:1.05em;
  /* Both nudges are optical. The triangle's mass sits left of its box centre,
     so it reads as drifting off the pill's left edge without the first; and the
     vendor's 9px .btn gap is set for the trailing arrow, which is a glyph on a
     text baseline — a stroked triangle with open space along its right side
     needs a couple of pixels less to sit the same distance from the word. */
  margin-left:-.1em;
  margin-right:-.15em;
  fill:none; stroke:currentColor; stroke-width:1.8;
  stroke-linejoin:round; stroke-linecap:round;
}

/* Buttons set on one line. The vendor's .btn has no white-space rule, so in a
   narrow column a two-word label wraps and the pill turns into a rounded block
   — the "Get cloud playtime" and "Cloud Gaming with XBOX Game Pass" tiles both
   hit it. A pill reads as a pill only while it is one line tall, so the button
   is allowed to set its own width instead. The grid column it sits in has room;
   what it did not have was permission to use it. */
.btn{ white-space:nowrap; }

/* ---------- secondary button ---------- */
/* Stroke and text in ink, no fill, so it reads as the second action beside the
   lime primary. This is the design system's own .btn.outline — the vendor pairs
   it with a primary in exactly this position — rather than a bespoke variant;
   only the two things tuned for a dark page are corrected here, and nothing else
   on this page uses .outline, so the overrides are safe to make unscoped.
     - border-color: the vendor picks --rule, a pale grey that all but disappears
       on the light page. Ink is what "black stroke" means here, and it comes
       from the token, so the stroke follows if the page ever returns to dark
       (--ink is near-white there) instead of vanishing into it.
     - the stroke stays 1px because .btn already reserves a 1px transparent
       border; widening it would make this pill taller than the primary beside
       it. The weight instead comes from an inset ring, which paints over that
       same 1px and costs no layout.
   Hover: the vendor lightens the border to --ink-3 and washes in white at 4%,
   both aimed at a dark surface — on white the wash is invisible and fading a
   full-ink stroke is backwards. It fills with ink and flips the label to paper
   instead, so hovering the secondary lands on the solid treatment. filter is
   cancelled for the same reason as the fill version: brightness(1.1) on near
   black moves nothing. The lift, scale and neutral halo still come from
   .btn:hover and html.light <main> (TGD: and div[role="main"], added for Campsite) .btn:hover. */
.btn.outline{
  background:transparent;
  color:var(--ink);
  border-color:var(--ink);
  box-shadow:inset 0 0 0 1px var(--ink);
}
.btn.outline:hover{
  background:var(--ink);
  color:var(--paper);
  filter:none;
}

/* ---------- button hover ---------- */
/* The design system's hover is a 2px lift, brightness(1.06) on an already
   saturated lime, and a halo tinted that same lime. At the sizes used on this
   page none of it is perceptible: 2px on a 42px pill is below notice, the
   brightness nudge has almost no headroom left to work with, and against the
   near-black sub-bar a 30%-alpha lime halo is close to the bar's own colour.
   The gesture is kept exactly as the vendor wrote it — same properties, same
   easing, same direction — and simply given the amplitude to be felt: the pill
   grows slightly under the cursor, which is what actually reads as "this is
   live", with the lift and halo raised to match. Source order alone wins here,
   since this matches the vendor selector's specificity.

   The halo is mixed from --accent rather than written as a literal so it follows
   the tweaks panel: at 50% and 42% of the default lime these resolve to exactly
   the rgba() values they replace, but a darker accent now gets a halo in its own
   colour instead of a lime one that no longer belongs to the button. */
.btn:hover{
  transform:translateY(-3px) scale(1.035);
  box-shadow:0 14px 32px -10px color-mix(in srgb, var(--accent) 50%, transparent),
             0 4px 14px -6px color-mix(in srgb, var(--accent) 42%, transparent);
  filter:brightness(1.1);
  color:var(--accent-on);
}
.btn:active{ transform:translateY(-1px) scale(1.01); transition-duration:.08s; }
/* The scale is the one part that cannot simply be slowed down, so motion-averse
   users get the colour change and nothing that moves. */
@media (prefers-reduced-motion:reduce){
  .btn:hover, .btn:active{ transform:none; }
}

/* No auto-motion: the script leaves a single un-cloned set in place and adds
   .is-static, so the rail becomes an ordinary swipeable row. Both axes are given
   a non-visible value — pairing overflow-x:auto with overflow-y:visible would
   silently compute the y axis to auto and add a second scrollbar. */
@media (prefers-reduced-motion:reduce){
  .hero-rail.is-static .poster-track{ animation:none; }
  .hero-rail.is-static .rail-viewport{ overflow-x:auto; overflow-y:hidden; }
  .hero-rail.is-static .rail-toggle{ display:none; }
}

/* ---------- scroll panels ---------- */
/* Used by "Where you can play". Each panel is a single image with the copy laid
   over it, presented as a rounded tile on the page's content column: the markup
   wraps the group in .wrap, so the tiles' edges land on the same rail as every
   other section and no bespoke gutter token is needed.

   Media and copy occupy the *same* grid cell rather than two columns, so they
   stack without absolute positioning — the panel still grows if the copy is
   taller than the media's min-height, which a positioned overlay would not. */

/* The copy sits on imagery, so it is light in both themes — the same reasoning
   that keeps the chrome dark in both. These are local to the panels rather than
   the page ink tokens, which the light theme flips to near-black on <main>. */
.panels{
  --panel-ink:#ffffff;
  --panel-ink-2:rgba(255,255,255,.86);
}

/* Separate tiles now, so they need air between them; as full-bleed plates they
   stacked flush and the section killed its own bottom padding to match.
   Deliberately a margin and not a grid gap on the .wrap: making the wrap a grid
   turns each .panel into a grid item, and below 980px the panels carry
   aspect-ratio:4/5 with max-height:82vh — on a grid item that clamped height
   transfers back through the ratio and shrinks the tile's *width* (a 738px cap
   pulled them to 590px, well inside the column). As block children their width
   stays definite and the ratio only ever drives the height. */
.panel + .panel{ margin-top:clamp(14px, 1.6vw, 24px); }
.panel{
  position:relative;
  display:grid;
  grid-template-areas:"stack";
  grid-template-columns:1fr;
  align-items:stretch;
  /* One height for all three panels, held in a token so the section has a single
     knob. Any future per-panel override has to set this variable rather than
     min-height directly: below 980px the panels switch to a 4/5 ratio where
     `.panel{ min-height:0 }` has to win, and a `.panel.modifier` min-height
     declaration would outrank it from outside that media query. */
  --panel-min:min(72vh, 720px);
  min-height:var(--panel-min);
  border-radius:var(--r-card);
  /* Base plate. The media fades up from opacity 0 on reveal, so without this the
     light copy would animate in over the light page background and vanish. */
  background:#000;
  /* Rounds the media and the scrim to the tile, and clips the media's reveal
     transform. .panel-media is scaled 1.06 until it reveals, and .panel-media's
     own overflow:hidden only clips its *contents* — the scaled box itself still
     escapes this panel and over the neighbouring sections. Clipping here is also
     what makes the reveal read as a window onto moving art rather than a picture
     growing over its surroundings. */
  overflow:hidden;
}

.panel-media{
  grid-area:stack;
  overflow:hidden;
  /* Containing block for the absolutely positioned media below. */
  position:relative;
}
/* Ready for the real imagery: any img/video dropped into .panel-media covers the
   plate edge to edge without further changes.

   Taken out of flow deliberately. In flow, `height:100%` resolves against the
   grid row, which is `auto` while the row is being sized — an indefinite
   percentage, so it falls back to `auto` and the media's *intrinsic* ratio ends
   up setting the panel height. That made --panel-min a floor the art could
   silently overrule: at 1920 the 1672x941 stills stretched their panels to
   999px against a 900px rule, and swapping in a differently proportioned image
   would have moved the height again. Out of flow it contributes nothing to row
   sizing, so the panel is sized by its copy and --panel-min, and the media
   covers whatever that comes to. */
.panel-media img, .panel-media video{
  display:block;
  position:absolute; inset:0;
  width:100%; height:100%; object-fit:cover;
  /* Framing knobs, both defaulting to no-op so a panel only sets what it needs.
     --media-scale enlarges the picture inside its fixed box, and --media-y
     slides it. Deliberately a transform rather than object-position: the panels
     are full-bleed, so the art is already cropped on all four sides and
     object-position can only trade one edge for another — it cannot show the
     subject larger. Scaling first and then sliding does both.

     Kept off .panel-media itself, which carries the reveal's own scale and
     would have this overwritten the moment the panel animated in. */
  transform:scale(var(--media-scale, 1)) translateY(var(--media-y, 0));
  transform-origin:center;
}

/* Legibility scrim. The imagery is arbitrary, so the copy cannot rely on the art
   being dark where the text lands — this guarantees the contrast instead. It is
   weighted to the copy side and falls off to clear before the far edge, so the
   picture still reads. Deliberately on .panel and not inside .panel-media: the
   media carries the reveal transform, and a scrim that scaled and faded with it
   would leave the text unscrimmed for the first ~900ms.
   --scrim-dir is kept as a token rather than hard-coded to 90deg: the copy runs
   down the left here, so the scrim runs left to right, and the portrait layout
   below 980px moves both at once by overriding this one value. Keeping it a
   token also means mirroring a panel would be a one-line gradient reversal with
   no source reordering, so the DOM keeps its reading order (heading before its
   plate) for assistive tech.
   The scrim tone is --panel-scrim, defaulting to black for the bare plates. A
   panel carrying real imagery sets it inline to a colour sampled from the region
   of that image the copy sits over (see the "On the go" panel), which is the
   light-theme scrim sampling from vendor/dotcom-refresh/index.html:1086-1101
   applied per panel — the darkening then reads as the picture falling into its
   own shadow instead of a flat black wash. */
/* --scrim-span is how far across the panel the scrim takes to reach clear,
   measured from the copy side, and is the one knob a panel tunes to its art. An
   evenly lit photo can let it run most of the way; a photo whose subject sits
   near the middle pulls it in so the scrim stops short of that subject (see
   "On your PC", where the laptop screen is the brightest thing in frame and a
   default-span scrim would wash it out). The two inner stops ride along as
   fractions of the span, so the falloff keeps its shape at any span. */
.panel{ --panel-scrim:#000; --scrim-dir:90deg; --scrim-span:88%; }
/* Framing shared by all three photographs: enlarged a little inside the frame
   and slid down, so the subject sits lower and reads bigger at full bleed. The
   scale has to cover the slide — at 1.12 the picture overhangs its box by 6% on
   each edge, which is where the 4% drop comes from. Raising --media-y without
   raising --media-scale to match would pull a bare edge into frame. */
.panel{ --media-scale:1.12; --media-y:4%; }
.panel::after{
  content:"";
  position:absolute; inset:0; z-index:1; pointer-events:none;
  background:linear-gradient(var(--scrim-dir),
    color-mix(in srgb, var(--panel-scrim) 92%, transparent) 0%,
    color-mix(in srgb, var(--panel-scrim) 82%, transparent) calc(var(--scrim-span) * .4),
    color-mix(in srgb, var(--panel-scrim) 40%, transparent) calc(var(--scrim-span) * .75),
    transparent var(--scrim-span));
}

/* Copy sits in a column down the left over the artwork. Below 980px, where the
   panel turns portrait and the column has no room to be a column, this flips to
   centred at the foot — see the responsive block at the end of this file. */
.panel-copy{
  grid-area:stack;
  position:relative; z-index:2;
  align-self:center;
  justify-self:start;
  max-width:min(620px, 52%);
  padding:clamp(48px, 6vw, 96px) clamp(28px, 4vw, 64px);
  color:var(--panel-ink);
}

/* Larger type scale, matching the section titles rather than the 19px card heading.
   The ramp is .sec-title's, so from about 714px (where this floor releases) to
   about 1000px (where .sec-title hits its cap) the two set at exactly the same
   size — a panel heading and a section heading read as the same rank because
   they are.

   Above that they part: .sec-title was trimmed to a 56px ceiling while this one
   keeps 64px, so on wide viewports a panel heading sets 8px larger. That is
   deliberate, not drift — a panel heading carries a full-bleed image behind it
   and can hold the extra weight. Keep the ramp shared; if that 5.6vw changes,
   change it in both places or the two stop tracking below 1000px.

   The floors differ too: these sit in a constrained copy column over artwork,
   so they bottom out at 32px rather than 40px. */
.panel-copy h2{
  font-family:var(--display); font-weight:700;
  font-size:clamp(32px, 5.6vw, 64px); letter-spacing:-.04em; line-height:1.04;
  /* In em, so the gap tracks the clamped title size instead of going slack at
     32px and tight at 56px. Note the space the eye sees is larger than this:
     the paragraph's 1.55 line-height adds ~4.5px of half-leading above its
     first line, so .24em here reads as roughly 16px of optical gap. */
  margin:0 0 .24em; max-width:16ch; text-wrap:balance;
  color:var(--panel-ink);
}
/* The vendor's kicker atom is an inline span sized for a card. Two changes are
   needed for a panel: it has to be a block, because transforms are ignored on
   inline boxes and the reveal below translates every .panel-copy child; and it
   grows a little on wide viewports, where the headline reaches its 56px cap and
   a fixed 11px label starts to look stranded above it. */
.panel-copy .kicker{
  display:block;
  font-size:clamp(11px, .8vw, 13px);
  margin:0 0 clamp(10px, .9vw, 14px);
}
/* Same reasoning as --panel-ink above: the light theme darkens .kicker.green to
   #1a6a00 so it clears AA on white cards, but this copy sits on photography and
   stays light in both themes, so the lime is restored here.

   Both rules read --accent-on-dark rather than --accent. The kicker is the one
   place an accent is printed as small text straight onto a photo, and an accent
   chosen to work as a *fill* is not necessarily legible there — the deep green
   #008746 disappears into the dark frames. The tweaks panel supplies a paired
   on-dark value per preset; the fallback keeps the accent itself, which is what
   the default lime wants. */
.panel-copy .kicker.green{ color:var(--accent-on-dark, var(--accent)); }
html.light main .panel-copy .kicker.green, html.light div[role="main"] .panel-copy .kicker.green{ color:var(--accent-on-dark, var(--accent)); }

.panel-copy p{
  margin:0; max-width:38ch;
  font-size:clamp(16px, 1.15vw, 19px); line-height:1.55; color:var(--panel-ink-2);
}

/* ---------- pinned panel sequence ---------- */
/* The section doubles as a scroll-driven sequence: the first panel grows from
   tile to edge-to-edge as it comes into view, the three then cycle in place
   while the frame is pinned, and the frame shrinks back to a tile on the way
   out. Everything here is gated on .pin-panels, which the driver in index.html
   sets only on a pointer-sized viewport with motion allowed — the resting rules
   above stay the sole layout otherwise.

   .panel-deck is a .wrap by hand rather than a nested .wrap element, because the
   pinned state has to *replace* those gutters with an interpolated width and a
   second box in between would keep imposing the old cap. */
.panel-deck{ max-width:var(--maxw); margin:0 auto; padding:0 72px; }

/* Height of the runway, as multiples of the viewport. One viewport is consumed
   by the sticky frame itself, so the scrollable span is (this - 1) screens:
   3.4 leaves 2.4 screens to divide between the two grow/shrink ramps and the
   three panels. */
.pin-panels .panel-stage{
  /* Resting height of the deck. Declared on the stage rather than the deck so
     the negative margin below and the deck's own sizing read one value. */
  --deck-h0:min(72vh, 720px);
  height:340vh;
  /* The pin viewport is a full screen tall and centres the deck in it, so once
     the deck has collapsed back to a tile the runway's last screen ends with
     half the difference as dead space — roughly 14vh of empty page between this
     section and the next. Pulling the stage up by exactly that much lands the
     following section on the normal inter-section rhythm again.

     Only the tail is affected: everywhere else in the runway the stage's bottom
     edge is far below the viewport, so this is invisible until the deck is a
     tile. Keep it in step with .panel-pin's height and the deck's --deck-h0.

     The same dead space exists above the deck at the head of the runway, for the
     same reason, so the margin is applied to both ends. */
  margin-top:calc((var(--deck-h0) - 100vh) / 2);
  margin-bottom:calc((var(--deck-h0) - 100vh) / 2);
}
.pin-panels .panel-pin{
  position:sticky; top:0;
  height:100vh;
  display:grid; place-items:center;
  /* This frame is a full screen tall but only the deck in its middle is real,
     and .panel-stage's negative margins make the runway overlap its neighbours
     (~285px at a tall viewport). Painted later, the empty area won the hit test
     and swallowed the pointer — the hero's Play now never got :hover at all. */
  pointer-events:none;
}
.pin-panels .panel-deck{
  pointer-events:auto;
  /* Driven from JS, 0 = tile, 1 = full bleed. Declared here so the very first
     frame has a value and the deck never flashes at its unresolved size. */
  --expand:0;
  /* Resting width: the content column inside .wrap's 72px gutters, floored at
     the viewport so the sum still works below --maxw. */
  --deck-w0:calc(min(var(--maxw), var(--vw, 100vw)) - 144px);
  max-width:none; padding:0;
  position:relative;
  width:calc(var(--deck-w0) + (var(--vw, 100vw) - var(--deck-w0)) * var(--expand));
  height:calc(var(--deck-h0) + (100vh - var(--deck-h0)) * var(--expand));
  /* How much page is still showing down each side of the deck, and above and
     below it. Both close to zero exactly when --expand reaches 1. */
  --deck-gap-x:calc((var(--vw, 100vw) - var(--deck-w0)) * (1 - var(--expand)) / 2);
  --deck-gap-y:calc((100vh - var(--deck-h0)) * (1 - var(--expand)) / 2);
  /* Corners open out as the frame reaches the edges, so full bleed is genuinely
     edge to edge rather than a rounded tile that happens to be screen-sized.

     Tied to the *remaining gap* rather than to --expand directly. Scaling the
     radius linearly with --expand looks wrong because the two quantities are
     wildly different sizes: the gutter has ~150px to travel and the radius only
     14px, so at 80% through the ramp the corners are already down to 3px and
     read as square while there is still a good 30px of page showing down either
     side. Clamping to the gap instead holds the full radius for as long as the
     deck is plainly a floating tile, then retires it over the last handful of
     pixels, so the corners square off at the moment the artwork actually
     touches the edge of the window.

     Both axes are considered: a full-width band that is still short of the top
     and bottom is not flush either, and its corners should still be round. The
     max() guards the radius against going negative if the resting deck is ever
     taller than the viewport, which would invalidate the declaration outright. */
  border-radius:max(0px, min(var(--r-card), var(--deck-gap-x), var(--deck-gap-y)));
  /* Opaque plate behind the stack. The panels cross-fade on opacity, so for the
     .55s they overlap both are partly transparent and whatever sits behind the
     deck is visible through them — and nothing between here and <body> paints a
     background, so on the light theme that was the page itself. The seam washed
     out to roughly 15 points of luminance brighter than either panel, read as a
     white flash between the three. Fading over black instead dips the seam dark,
     which is what a cut between two photographs should do.

     Scoped to .pin-panels deliberately: in the resting layout the deck is just a
     wrapper around three flowed tiles with clear gaps between them, and a plate
     there would paint those gaps black. */
  background:#000;
}
/* Stacked, not flowed: every panel fills the deck and only the active one is
   painted. min-height has to be cleared or the resting --panel-min would hold
   the deck open at tile height when it is meant to be full screen. */
.pin-panels .panel{
  position:absolute; inset:0;
  min-height:0;
  /* Both of these come from the portrait resting layout below 980px, and both
     fight a pinned deck: max-height would hold the panel at 82vh inside a frame
     that has grown to a full 100vh, leaving the deck's black plate showing
     along the bottom edge, and the ratio is only meaningful while the panel is
     sizing itself. With inset:0 both dimensions are already definite. */
  max-height:none;
  aspect-ratio:auto;
  border-radius:inherit;
  /* The deck now paints the plate, so the panel must not paint its own. Both
     boxes are the same rounded rect to the pixel, and stacking two opaque ones
     compounds their antialiasing instead of blending it: an edge pixel that
     each layer covers 50% ends up 1-(1-.5)^2 = 75% opaque, and with the artwork
     clipped on top the ramp collapses to a hard step. The corners went visibly
     jagged when the deck's plate was added underneath this one — measured
     against an analytic rounded rect, corner error went from 0.18 to 0.45.
     One plate takes it to 0.11, better than either.

     Only the pinned layout is affected. In the resting layout there is no deck
     plate, so .panel keeps the background declared in its base rule. */
  background:transparent;
  opacity:0; pointer-events:none;
  transition:opacity .55s ease;
}
.pin-panels .panel + .panel{ margin-top:0; }
.pin-panels .panel.on{ opacity:1; pointer-events:auto; }

/* ---------- scroll reveal ---------- */
/* The hidden state is scoped to .has-reveal, which the inline script in
   index.html sets on <html> only once it has an IntersectionObserver. Without
   JS the class is never added and everything renders visible — the animation is
   an enhancement, never a prerequisite for reading the page. */
/* Two delays shape the whole reveal. Both are declared here so the sequence can
   be retimed from one place rather than by hunting through the rules below.

   --reveal-delay  the tile's place in a top-to-bottom cascade, set by the
                   observer in index.html at the moment the tile reveals. It
                   holds each tile one --reveal-step behind the one before it,
                   so tiles arriving together fan out instead of landing as one
                   block, while a tile arriving on its own starts immediately.
   --copy-lead     how long the copy holds back after its own backplate starts.
                   The media takes .9s on a fast-out ease, so by the time the
                   copy moves at ~.43s the imagery has visually landed and the
                   words rise onto a plate that is already there.
   --rise-ease     one curve for every reveal on the page. Near-exponential
                   deceleration: most of the travel is spent in the first third
                   and the last few pixels take the rest, so things arrive
                   quickly but settle rather than stop. Three slightly different
                   ease-outs used to be spread across these rules, which read as
                   vagueness rather than as variety.
   --rise-dur      the follower duration -- sub-lines and body copy.
   --rise-lead-dur headings, which take the longer 58px travel and want longer
                   to cover it at the same apparent speed.
   --rise-tile-dur tiles and buttons: objects landing rather than text settling.
                   Between the two, closer to the leads than the old .5s, which
                   was brisk enough to read as a pop next to this curve. */
.has-reveal [data-reveal]{
  --reveal-step:120ms;
  --reveal-delay:0ms;
  --copy-lead:380ms;
  --rise-ease:cubic-bezier(.16, 1, .3, 1);
  --rise-dur:.72s;
  --rise-lead-dur:.95s;
  --rise-tile-dur:.8s;
}

.has-reveal [data-reveal] .panel-media{
  opacity:0; transform:scale(1.06);
  transition:opacity .9s cubic-bezier(.2,.7,.2,1), transform .9s cubic-bezier(.2,.7,.2,1);
  /* After the shorthand, which would otherwise reset the delay to zero. */
  transition-delay:var(--reveal-delay);
  /* Scale about the panel's subject rather than its centre. The scrim is static
     on .panel, so a centre-origin scale would walk the subject sideways under a
     fixed gradient for the length of the reveal; anchoring the origin on the
     subject keeps it pinned to the same spot in the scrim from first frame to
     last. --focus-x/-y are set per panel and default to the centre. */
  transform-origin:var(--focus-x, 50%) var(--focus-y, 50%);
}
.has-reveal [data-reveal].in .panel-media{ opacity:1; transform:none; }

/* Copy rise-in, originally ported from the dotcom-refresh hero carousel
   (vendor/dotcom-refresh/index.html:217-229). The travel distances and the
   lead/follow split are still the vendor's; the durations and the easing are
   not — both now come from the page's own --rise-* tokens, so the panels speak
   the page's motion language rather than the carousel's.

   The shape of it: the heading leads — it starts almost immediately and takes
   the long, slow 58px travel — while the sub-text starts later on the shorter
   24px rise. Both land together despite entering separately. The two offsets
   below are the vendor's; --copy-lead then shifts the pair as a unit so the
   backplate goes first without disturbing their relationship to each other.
   Trigger is `.in` from the scroll observer rather than the carousel's `.on`. */
@keyframes panelRise{     from{ opacity:0; transform:translateY(24px); } to{ opacity:1; transform:none; } }
@keyframes panelRiseLead{ from{ opacity:0; transform:translateY(58px); } to{ opacity:1; transform:none; } }

/* The resting state doubles as the exit state. On the way in the animations
   below own both properties, so these values are only ever seen on the way out:
   dropping .in hands the properties back here and the transition plays the rise
   in reverse, at about two thirds the duration so leaving is brisker than
   arriving. The travel distances mirror the two keyframes exactly. */
.has-reveal [data-reveal] .panel-copy > *{
  opacity:0; transform:translateY(24px);
  transition:opacity .38s ease, transform .38s ease;
}
.has-reveal [data-reveal] .panel-copy > h2{ transform:translateY(58px); }
.has-reveal [data-reveal].in .panel-copy > *{
  animation:panelRise var(--rise-dur) var(--rise-ease) both;
  animation-delay:calc(var(--reveal-delay) + var(--copy-lead) + .34s);
}
.has-reveal [data-reveal].in .panel-copy > h2{
  animation:panelRiseLead var(--rise-lead-dur) var(--rise-ease) both;
  animation-delay:calc(var(--reveal-delay) + var(--copy-lead) + .05s);
}

/* Card tiles rise into place, in reading order. They used to fall from just
   above, which made them the one thing on the page moving against everything
   else; sharing the direction of the headings and the copy is what makes the
   whole page read as one movement.
   The device panels above hold their own choreography (the backplate settles
   while the copy rises onto it), so this is the treatment for the plain card
   grids — "What you can do", the get-started plate and the three options.

   Small tiles in a row want a quicker beat than the full-width panels, so the
   step is shortened here. The observer reads --reveal-step off each tile as it
   reveals, which is what lets one page carry two paces.

   The fill mode is the load-bearing detail. These cards are also .lift, whose
   hover moves the same property: an animation with `forwards`/`both` keeps its
   final transform applied for good and would silently kill the hover lift,
   since animations outrank ordinary declarations. `backwards` fills only the
   delay before the tile plays, then releases transform entirely — so the tile
   holds still and hidden while it waits its turn, and hands the property back
   the moment it lands. Opacity is not left to the animation for the same
   reason: .in carries it, so the tile stays visible once the animation is
   out of the picture. */
@keyframes tileRise{ from{ opacity:0; transform:translateY(var(--tile-rise)); } to{ opacity:1; transform:none; } }

/* Fades back out on exit, and only fades: the rise is deliberately not reversed
   here. These tiles are also .lift, and putting a translate on this rule would
   need .in to reset it — a four-class selector that outranks .lift:hover and
   would silently kill the hover lift for the rest of the page. Opacity is the
   one property the two treatments do not share.

   The transition has to restate .lift's own three properties as well as
   opacity. transition is a shorthand, and this rule outranks .lift, so listing
   opacity alone drops transform, border-color and box-shadow from the
   transition entirely and the hover snaps on and off with no easing. They come
   from --tx-lift so this cannot drift from the vendor's timing. */
.has-reveal .card[data-reveal]{
  --tile-rise:24px; --reveal-step:70ms;
  opacity:0;
  transition: opacity .34s ease, var(--tx-lift);
}
.has-reveal .card[data-reveal].in{
  opacity:1;
  animation:tileRise var(--rise-tile-dur) var(--rise-ease) backwards;
  animation-delay:var(--reveal-delay);
}

/* The steps row takes the same tile entrance without being a card, but as a
   cascade rather than one block: each numeral rises, then its title, then the
   next pair. The row itself no longer animates — it stays put and its children
   carry the motion, which is why the opacity:0 sits on them rather than on the
   list. Hiding the list would hide the children with it and leave nothing for
   the stagger to reveal.

   The order is numeral, its own title, next numeral — not all three numerals
   and then all three titles. Reading down a step before moving across is what
   the numbering already asks the eye to do.

   The index is set per item rather than computed, because nth-child cannot be
   fed into calc(). Three literals for three steps is cheaper than the script it
   would otherwise take, and a fourth step failing to animate is a visible
   prompt to add its line. */
.steps-row{ --step-stagger:80ms; --tile-rise:24px; }
.steps-row li:nth-child(1){ --step-i:0; }
.steps-row li:nth-child(2){ --step-i:1; }
.steps-row li:nth-child(3){ --step-i:2; }

.has-reveal .steps-row[data-reveal] .step,
.has-reveal .steps-row[data-reveal] .step-body{
  opacity:0;
}
.has-reveal .steps-row[data-reveal].in .step,
.has-reveal .steps-row[data-reveal].in .step-body{
  opacity:1;
  animation:tileRise var(--rise-tile-dur) var(--rise-ease) backwards;
}
.has-reveal .steps-row[data-reveal].in .step{
  animation-delay:calc(var(--step-i) * 2 * var(--step-stagger));
}
.has-reveal .steps-row[data-reveal].in .step-body{
  animation-delay:calc((var(--step-i) * 2 + 1) * var(--step-stagger));
}

/* Section titles rise in with their section. They borrow panelRiseLead rather
   than getting keyframes of their own: that is already the page's "a heading
   leads" motion — the long, slow 58px travel the panel headings take — and a
   section title is the same role one level up. Reusing it keeps one motion
   language rather than two that nearly match.

   The hero h1 joins them for the same reason: it is the page's title, so it
   gets the treatment the section titles get, not a hero-only variant. Being
   above the fold it intersects on load, which turns the scroll reveal into an
   entrance without any separate code path.

   These are the only [data-reveal] elements that are neither a .card nor inside
   a .panel-copy, so they need their own hidden state; the generic rule above
   sets the timing custom properties and nothing else. The title is its own
   reveal target rather than riding the plate below it, so the heading lands
   before the thing it introduces. */
.has-reveal .sec-title[data-reveal],
.has-reveal .cta-row[data-reveal],
.has-reveal .hero-copy h1[data-reveal]{
  opacity:0; transform:translateY(58px);
  transition:opacity .38s ease, transform .38s ease;
}
.has-reveal .sec-title[data-reveal].in,
.has-reveal .cta-row[data-reveal].in,
.has-reveal .hero-copy h1[data-reveal].in{
  opacity:1;
  animation:panelRiseLead var(--rise-lead-dur) var(--rise-ease) both;
  animation-delay:var(--reveal-delay);
}

/* The hero sub-line follows on the shorter, snappier 24px rise — the same
   lead/follow split .panel-copy uses between its h2 and everything under it.
   Giving both hero lines the 58px travel would start the h1 in the space the
   sub-line occupies and the two would cross over each other on the way up;
   the shorter travel on the follower is what keeps them clear. */
.has-reveal .hero-copy .hero-sub[data-reveal]{
  opacity:0; transform:translateY(24px);
  transition:opacity .38s ease, transform .38s ease;
}
.has-reveal .hero-copy .hero-sub[data-reveal].in{
  opacity:1;
  animation:panelRise var(--rise-dur) var(--rise-ease) both;
  animation-delay:var(--reveal-delay);
}

/* The two CTAs arrive last, on the card cascade rather than the copy rise: they
   are objects landing, not text settling, and the quicker 70ms step keeps the
   pair tight after the slower beat of the two lines above.

   Two constraints make this look unlike the rules above. tileRise is filled
   `backwards`, not `both`, because .btn:hover animates transform — a `both`
   fill would pin the final transform permanently and silently kill the hover
   lift, exactly as the note on .card[data-reveal] describes. And no transition
   is declared here at all: .btn carries its own transition (now --tx-btn plus
   colour), and the shorthand would replace it and take the hover easing with
   it. Opacity rides .in instead, which needs none. */
.has-reveal .hero-cta .btn[data-reveal]{
  --tile-rise:24px; --reveal-step:70ms;
  opacity:0;
}
.has-reveal .hero-cta .btn[data-reveal].in{
  opacity:1;
  animation:tileRise var(--rise-tile-dur) var(--rise-ease) backwards;
  animation-delay:var(--reveal-delay);
}

/* FAQ rows cascade as you reach them. Same tileRise as the card grids, at the
   same quick 70ms step, so the page has one cascade and not a third variant.

   The travel stays smaller than a card's: these rows sit flush against each
   other and share hairline rules, so a long offset pulls visible gaps between
   neighbours while the cascade is mid-flight. 14px is enough to read as motion
   without opening the seams — it was not raised with the cards for that reason.
   `backwards` again, so the row hands transform back on landing and nothing is
   left pinned over .faq-q.open. */
.has-reveal .faq-q[data-reveal]{
  --tile-rise:14px; --reveal-step:70ms;
  opacity:0;
}
.has-reveal .faq-q[data-reveal].in{
  opacity:1;
  animation:tileRise var(--rise-tile-dur) var(--rise-ease) backwards;
  animation-delay:var(--reveal-delay);
}

@media (prefers-reduced-motion:reduce){
  .has-reveal [data-reveal] .panel-media,
  .has-reveal [data-reveal] .panel-copy > *,
  .has-reveal [data-reveal] .panel-copy > h2{
    opacity:1; transform:none; transition:none;
  }
  .has-reveal [data-reveal].in .panel-copy > *,
  .has-reveal [data-reveal].in .panel-copy > h2{ animation:none; }
  .has-reveal .card[data-reveal]{ opacity:1; }
  .has-reveal .card[data-reveal].in{ animation:none; }
  .has-reveal .steps-row[data-reveal] .step,
  .has-reveal .steps-row[data-reveal] .step-body{ opacity:1; }
  .has-reveal .steps-row[data-reveal].in .step,
  .has-reveal .steps-row[data-reveal].in .step-body{ animation:none; }
  .has-reveal .sec-title[data-reveal],
  .has-reveal .hero-copy h1[data-reveal],
  .has-reveal .hero-copy .hero-sub[data-reveal]{ opacity:1; transform:none; transition:none; }
  .has-reveal .sec-title[data-reveal].in,
  .has-reveal .hero-copy h1[data-reveal].in,
  .has-reveal .hero-copy .hero-sub[data-reveal].in{ animation:none; }
  .has-reveal .hero-cta .btn[data-reveal],
  .has-reveal .faq-q[data-reveal]{ opacity:1; }
  .has-reveal .hero-cta .btn[data-reveal].in,
  .has-reveal .faq-q[data-reveal].in{ animation:none; }
}

/* ---------- generic card grid ---------- */
/* 16px gutter, same as the vendor .xtiles grid. */
.grid{ display:grid; gap:16px; }
.grid.c3{ grid-template-columns:repeat(3, 1fr); }
/* .c4 is the "What you can do" set: four across, each tile skinny and upright. */
.grid.c4{ grid-template-columns:repeat(4, 1fr); }
.grid.c2{ grid-template-columns:repeat(2, 1fr); }

/* The ways-to-play row. Two even columns, one per option.

   This was four columns with the free tier spanning two, back when there were
   three tiers and the paid pair held one each. Folding those two into a single
   tile left both sides on two of four, which is exactly a half each — so the
   row says that directly rather than keeping a span that no longer varies.

   Not a .cN utility despite now matching .c2, because the collapse below and
   the tile treatment are specific to this row, and reaching for .c2 would tie
   the two together the next time either moves. */
.grid.opt-row{ grid-template-columns:repeat(2, 1fr); }

/* Alt ways-to-play */
.grid.opt-row.opt-row-3{ grid-template-columns:repeat(3, 1fr); }

.card{
  background:var(--paper-2); border:1px solid var(--rule-soft);
  border-radius:var(--r-card); padding:26px;
}
/* One title size across all three card sections -- "What you can do", "Getting
   started is easy" and "Choose your way to play" -- so the page reads as one set
   of tiles rather than three scales. The tighter line-height and tracking come
   with the size: at this weight the display face opens up noticeably, and the
   "What you can do" titles run to two and three lines in a ~256px measure. */
.card{ --tile-title:clamp(20px, 1.7vw, 28px); }
/* The option cards take the title down a step from the "What you can do" tiles,
   where the title is the only text on a tall dark plate and wants the full
   scale.

   The slope moved with the ceiling (1.5 -> 1.7vw, the same 1.12 the max took),
   so the ramp still tops out around 1650px rather than raising the ceiling and
   never reaching it inside the 1980px page. The 20px floor stays put: it is
   there for phone widths, where nothing about this change applies.

   This was shared with the steps plate, back when the step headings took
   --tile-title through the shared card-heading rule. They are body-sized now and set their own
   size, and the plate itself is gone, so the pairing had nothing left to keep
   matched. */
.opt{ --tile-title:clamp(20px, 1.7vw, 28px); }
.card :is(h2, h3){
  font-family:var(--display); font-weight:700; font-size:var(--tile-title);
  letter-spacing:-.03em; line-height:1.15; margin:0 0 12px;
}

/* Accessibility Text-spacing 1.4.12 */
.text-spacing .card :is(h2, h3), .card :is(h2, h3)[style]{
  font-size: clamp(14px, 1.7vw, 17px);
}
/* Body copy on the option cards. The clamp came from the step paragraphs, which
   the plate no longer has — the steps are headings alone now — so this reaches
   only the option cards and the dark tiles in practice.
   --tile-title covers the headings the same way one rule up. */
.card p{ margin:0; font-size:var(--body-copy); line-height:1.55; color:var(--ink); }

/* Accessibility Text-spacing 1.4.12 */
.text-spacing .card p, .card p[style]{
  font-size: clamp(13px, 1vw, 15px);
}

/* One step up from the inherited 400, on the light option cards. The face is
   loaded as a 400-700 variable, so 500 is a real instance rather than a
   synthesised weight. Deliberately not applied to the dark "What you can do"
   tiles: light text on a dark plate already blooms, so the same 500 reads closer
   to semibold there and would push those paragraphs past their headings. */
.opt p{ font-weight:500; }

/* "What you can do" tiles are skinny and upright, four across: a dark plate on a
   light page, with the copy at the top and the space left open beneath it.
   Scoped to .c4 on purpose: .card is shared with the get-started steps plate
   and the plan options (.c2), which stay light and keep their natural height.

   The colours are done by re-mapping the ink tokens on the tile rather than
   restating a colour on every child — .card p already paints with var(--ink-2),
   so redefining the token here reaches it for free. `color` is the exception and
   has to be set explicitly: it inherits as a *computed* value, so the light
   page's #161718 is already resolved by the time it reaches the tile and
   re-mapping --ink alone would never reach the h3.

   Height comes from aspect-ratio alone, with no min-height floor. That is not an
   oversight: the ratio is portrait, so a narrower column yields a *taller* tile,
   never a squatter one, and a floor would only ever add a few pixels at the
   narrowest widths. It would also cost more than it gives, because a min-height
   on a box with an aspect-ratio transfers back through the ratio into an implied
   minimum *width* (340px tall at 3/4.4 implies 232px wide), which a grid item
   will not shrink below — enough to burst the track and scroll the whole page
   sideways. Any height floor here has to be paired with min-width:0. */
.grid.c4 .card{
  /* Ink and hairline are the vendor dark palette's values. The plate is not:
     the vendor's dark --paper is #1b1d1b, which is faintly green, and these
     tiles want a neutral, slightly cooler dark against the light page. */
  --ink:   #f4f5f4;
  --ink-2: rgba(244,245,244,.66);
  aspect-ratio:3/5.1;
  padding:30px 28px;
  background:var(--plate-dark);
  /* No stroke: the plate is much darker than the page, so the edge is already
     unambiguous and a hairline on top of that contrast reads as a seam rather
     than a frame. The 1px border itself is kept and only made transparent, so
     the tile's box is unchanged — dropping it would pull every tile 2px in each
     direction and shift the grid. */
  border-color:transparent;
  color:var(--ink);
  /* The three art tiles each declare this for their own layers; the plain tile
     does not, and without it the go disc would position against the page. */
  position:relative;
}

.grid.c4 .card h2{ 
  font-size: clamp(20px, 1.2vw, 28px);
}

/* .grid.c4 .card p{ font-size:15px; } */
/* .lift:hover sets border-color:var(--rule), which these tiles do not remap —
   so without this the hairline would fade back in under the cursor, which is
   exactly the stroke being removed above. The lift and the shadow are left
   alone; only the edge is held transparent. */
.grid.c4 .card.lift:hover{ border-color:transparent; }

/* ---------- tile "go" disc ---------- */
/* Ported from dotcom-refresh's explore tiles (.xtile .go in its index.html):
   a 48px glass disc carrying the system's arrow glyph. The disc, its border,
   the blur and the hover fill are the vendor's values verbatim.

   PARKED. The markup is out of index.html: in the vendor the whole tile is an
   <a> and the disc points at where it goes, but these tiles are <article>s with
   nothing to go to, so the arrow was promising a destination that does not
   exist. The rule below already conceded half of that — no :focus, no cursor,
   aria-hidden — which was treating the symptom. Restore by putting this back
   inside each .grid.c4 article, after the paragraph:

     <span class="go" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 12h14M13 6l6 6-6 6"></path></svg></span>

   Kept rather than deleted because the tiles are one editorial decision away
   from being links, and this is the vendor's disc reproduced exactly.

   Two things differ from the vendor. There the disc sits top-right; here it
   moves to the foot.

   The arrow glyph never travels. Only the disc's fill answers the pointer,
   which is the vendor's behaviour exactly: .xtile .go svg carries no transition
   and no transform, and .xtile:hover .go changes background alone. The 3px
   nudge in .btn:hover .arr and .tlink:hover .arr belongs to arrows that sit
   inside a real link or button, where the movement is the control acknowledging
   the cursor. This glyph sits in a disc on a tile that does not click, so the
   same gesture would promise a press that never comes. Fill for presence,
   motion for controls; the tile's own lift is what tells you the pointer
   landed.

   z-index 4 clears everything the tiles declare: the copy sits at 3 on all
   three art tiles, and no art layer goes above 3 either. */
.grid.c4 .card .go{
  position:absolute; right:28px; bottom:30px; z-index:4;
  width:48px; height:48px; border-radius:50%;
  display:grid; place-items:center;
  background:rgba(255,255,255,.14);
  border:1px solid rgba(255,255,255,.28);
  color:#fff;
  backdrop-filter:blur(4px);
  transition:background .16s;
}
.grid.c4 .card .go svg{
  width:20px; height:20px;
  stroke:currentColor; fill:none; stroke-width:2;
}
.grid.c4 .card:hover .go{ background:rgba(255,255,255,.26); }

@media (prefers-reduced-motion:reduce){
  .grid.c4 .card .go{ transition:none; }
}

/* The go disc stays absolutely placed at every width. It sits in the tile's
   empty foot, which exists because .grid.c4 .card keeps an aspect-ratio all the
   way down to the single-column layout — see the responsive block at the end of
   this file. If a ratio is ever dropped there, this disc lands on the paragraph
   and has to rejoin the flow as the last child (position:relative, not static,
   or the sync tile's z-index:1 art paints over it). */

/* ---------- poster fan tile ---------- */
/* Ported from dotcom-refresh's "Play" tile (.xtile.x-poster-fan in its
   index.html, written up in that repo's parallax.md §3d). The transforms,
   rotations, easing and durations are the vendor's verbatim, with one
   exception: the centre poster's hover scale is halved from 1.05 to 1.025, to
   keep it in step with the art tile's own halved zoom — the two parallax tiles
   sit in the same row and a mismatched pop between them reads as a mistake.
   What else changes is the frame. There the tile is a link with the art filling
   it and the heading alone at the foot, here it is a card in a set whose copy
   sits at the top, so the poster stack is inset to the lower two-thirds and the
   scrim is weighted to the top instead of the bottom to hold the heading
   legible.

   Layering is by z-index against the copy, not the negative indices the vendor
   uses: those depend on its `isolation:isolate`, and the card has no stacking
   context of its own to isolate against here. */
.card.fan{ position:relative; overflow:hidden; isolation:isolate; }
.card.fan > :is(h2, h3), .card.fan > p{ position:relative; z-index:3; }

/* Blurred, blown-up copy of the tile's own art: the backdrop is drawn from the
   content standing in front of it, so it always keys to whatever that tile
   carries — one of the posters here, the character lockup on the cast tile. One
   declaration for both, so the two backdrops cannot drift apart. */
.card.fan .fan-bg,
.card.art.cast .cast-bg{
  position:absolute; inset:-15%; z-index:0;
  background-size:cover; background-position:center;
  filter:blur(30px) saturate(1.2); transform:scale(1.12);
}
/* Top-weighted so the heading and paragraph keep their contrast, easing off
   where the posters sit and closing again at the foot.

   The colour is sampled from the art rather than baked: .fan-bg is a poster
   blown up, blurred and centred, so what lies under the dense top of this
   scrim is that poster's upper middle, which averages rgb(65,74,82) — a cool
   blue-grey. Taken down to 45% brightness it becomes a dark that still carries
   the cast, where the previous rgba(15,16,15) was a faintly green near-black
   belonging to no particular picture.

   The backdrop is Starfield, and so is this sample — the two have to agree,
   because the scrim's job is to be right about what is actually beneath it.
   Starfield is the left poster rather than the centre one, which makes no
   difference: the backdrop is what this is sampled from. Whenever the backdrop
   changes, retake the sample with it.

   Only the colour changed. The 94/78/42/88 alpha ladder is the one that was
   already tuned to this composition, so it is carried over stop for stop.

   It also has to sit close to --plate-dark, because unlike the full-bleed tile
   this one is a cut-out: the plate shows around and below the scrim, so a
   scrim far from it would band where the gradient ends. rgb(29,33,37) is 16
   summed off #1a1b1e, which is under the threshold where an edge appears — and
   that is a real constraint on any candidate backdrop, not a description of
   this one. It is why the backdrop stopped tracking the centre poster: sampled
   the same way, Forza Horizon 6 lands at 185 and Flight Simulator 2024 at 95,
   against Starfield's 11. Forza's art is white rice-paper, so at 45% it is a
   mid-grey — the wrong end of the scale for something whose whole job is to
   darken. A poster this light cannot be the backdrop without abandoning either
   the sampling or the plate match. */
.card.fan{ --scrim:rgb(29,33,37); }
.card.fan::after{
  content:""; position:absolute; inset:0; z-index:1; pointer-events:none;
  background:linear-gradient(180deg,
    color-mix(in srgb, var(--scrim, rgb(15,16,15)) 94%, transparent) 0%,
    color-mix(in srgb, var(--scrim, rgb(15,16,15)) 78%, transparent) 34%,
    color-mix(in srgb, var(--scrim, rgb(15,16,15)) 42%, transparent) 62%,
    color-mix(in srgb, var(--scrim, rgb(15,16,15)) 88%, transparent) 100%);
}

.card.fan .fan-posters{
  position:absolute; inset:34% 0 7% 0; z-index:2;
  display:grid; place-items:center; pointer-events:none;
  /* the pointer drift; --fgx/--fgy are written by the parallax driver */
  transform:translate3d(var(--fgx,0px), var(--fgy,0px), 0);
  transition:transform .22s ease-out;
}
.card.fan .fan-poster{
  grid-area:1 / 1; width:46%; aspect-ratio:2/3; object-fit:cover;
  border-radius:11px; box-shadow:0 18px 38px -14px rgba(0,0,0,.78);
  transition:transform .55s cubic-bezier(.2,.7,.2,1);
  transform-origin:50% 100%; will-change:transform; backface-visibility:hidden;
}
.card.fan .p-left{  z-index:1; transform:translateX(-5%) rotate(-7deg); }
.card.fan .p-right{ z-index:2; transform:translateX(5%) rotate(7deg); }
.card.fan .p-center{ z-index:3; transform:translateY(-2%); }
.card.fan:hover .p-left{  transform:translateX(-26%) rotate(-12deg); }
.card.fan:hover .p-right{ transform:translateX(26%) rotate(12deg); }
.card.fan:hover .p-center{ transform:translateY(-5%) scale(1.025); }

@media (prefers-reduced-motion:reduce){
  /* The fan is the tile's whole visual idea, so it keeps its resting spread;
     what goes is the motion — the hover fan and the pointer drift. */
  .card.fan .fan-poster{ transition:none; }
  .card.fan:hover .p-left{  transform:translateX(-5%) rotate(-7deg); }
  .card.fan:hover .p-right{ transform:translateX(5%) rotate(7deg); }
  .card.fan:hover .p-center{ transform:translateY(-2%); }
  .card.fan .fan-posters{ transform:none !important; }
}

/* step numeral */
/* A dark numeral with an acid rounded bar under it. This was a filled acid
   square with the numeral knocked out of it; the plate has come down to an
   underline, so the green is now an accent beside the figure rather than the
   surface it sits on.

   The numeral goes back to --ink. On a filled plate it had to be --plate-dark,
   a fixed value, because the plate was fixed and the glyph had to stay legible
   on it whatever the theme did. Sitting on the page instead, the figure is
   ordinary text and takes the page's own ink, which flips with the theme the
   way every other piece of type does.

   justify-self:start, and only on this element. The li is a stacked grid whose
   default stretch is what gives .step-body the whole column; the numeral has to
   opt out of that and shrink to its own advance, or the bar under it would run
   the full column width and read as a rule rather than an underline. Putting
   justify-items on the li instead would shrink-to-fit the title as well and
   wrap it several words early.

   The bar is a pseudo-element rather than a border-bottom, because a border
   cannot round its own ends independently of the box, and rather than
   text-decoration, which can be offset and thickened but not rounded.

   No padding-right compensation any more. That existed to cancel the trailing
   letter-space while the numeral was being centred inside a fixed box; nothing
   is centred now, and the 0.03em the tracking takes off the right simply pulls
   the bar a hair tighter under the last digit. */
.step{
  justify-self:start;
  position:relative;
  display:block;
  margin:0;
  /* Exactly the bar's height, so the bar occupies the padding and the visible
     gap above it is the line box's own slack under the baseline - digits have
     no descender, so that is about .22em. */
  padding-bottom:.13em;
  font-family:var(--mono); font-weight:700;
  font-size:clamp(40px, 3.9vw, 84px); line-height:1;
  letter-spacing:var(--step-kern, -.03em);
  color:var(--ink);
}
/* "01" needs more tracking than the others. The face is monospaced, so every
   digit is handed the same advance, but "1" is a narrow glyph and its spare
   width turns into side bearing — which showed up twice: 9.9px between the two
   digits against 5.9px in "02" and "03", and a bar overhanging the last digit
   by 6.9px against their 1.9px.

   There is no font-feature route. proportional-nums, "pnum" 1 and "tnum" 0 all
   measure byte-identical to the default here; the face simply has no
   proportional figure set to fall back to.

   The correction is in px, not em, which is the whole point. The excess
   measures 4.0px at the 56px cap and 4.0px again at the 40px floor — it does
   not scale with the type, because it comes from hinting snapping stems to the
   pixel grid rather than from anything proportional. An em value can therefore
   only be right at one size: -.09em matched the gap at 56px and still left
   1.6px of it at 40px. A flat -4px lands both.

   Keyed to the first step rather than to the character, because CSS cannot
   select on content. That holds while the sequence is 01/02/03 — "1" appears
   in no other numeral here. A fourth step, or a renumbering, wants this
   revisited rather than extended. */
.steps-row li:nth-child(1){ --step-kern:calc(-.03em - 4px); }
.step::after{
  content:"";
  position:absolute; inset-inline:0; bottom:0;
  block-size:.13em;
  /* Fully round ends. A half-height radius would need restating whenever the
     height moves; a large constant is clamped to half automatically. */
  border-radius:999px;
  background:var(--accent);
}

/* ---------- art tile ---------- */
/* The storage tile. Same idea as the fan above — art behind copy that sits at
   the top — but a single cut-out layer instead of a stack, so it follows the
   vendor's .xtile.x-parallax-img variant rather than the poster fan: one image
   that drifts under the pointer and scales on hover, at data-pf="0.25". The
   drift transform and its .22s ease-out are the vendor's values verbatim (its
   index.html, and that repo's parallax.md §3c). The hover scale is the one
   deliberate departure: the vendor's 1.08 is halved to 1.04 here, because this
   art grows from its bottom-left corner rather than its centre, so a given
   percentage travels the full width at the cropped right edge instead of half
   of it — the vendor's number reads as twice the movement on this composition.

   The art is a cut-out with real alpha — a perspective quad whose left edge is
   full height and whose right edge falls away — so it is placed like an object
   on the plate rather than fitted to the frame: anchored to the left, sized past
   the tile's width so the right of it crops away at the edge, and dropped low so
   the copy has clear plate above it. */
.card.art{ position:relative; overflow:hidden; isolation:isolate; }
.card.art > :is(h2, h3), .card.art > p{ position:relative; z-index:3; }
.card.art .tile-art{ position:absolute; inset:0; z-index:0; pointer-events:none; }
.card.art .tile-art img{
  position:absolute; left:0; bottom:-4%;
  /* Sized from the height so the crop tracks the tile's own proportion: at 86%
     of a 3/4.4 tile the art is about a quarter wider than the frame, which is
     what puts its right edge past the tile and leaves the left intact. */
  height:86%; width:auto; max-width:none;
  /* Grown from the bottom-left rather than the centre. The whole point of the
     composition is that the art is pinned to the left edge and cropped on the
     right, and a centre origin would pull it off that edge as it scaled. */
  transform-origin:0% 100%;
  transform:translate3d(var(--fgx,0px), var(--fgy,0px), 0) scale(var(--imgs,1));
  transition:transform .22s ease-out;
  will-change:transform;
  filter:drop-shadow(0 26px 34px rgba(0,0,0,.6));
}
.card.art:hover .tile-art img{ --imgs:1.04; }
/* Top-weighted and mixed from the plate colour itself, so it reads as the plate
   thinning out over the art rather than as a separate grey wash. It holds close
   to opaque through the copy — which runs to about 24% of the tile at every
   column width — and only starts releasing below it, so the art never ghosts up
   behind the paragraph. Clear by 68%.

   The colour is --scrim where a tile supplies one, falling back to the plate.
   That is dotcom-refresh's own pattern (var(--scrim,#070707) throughout its
   .xtile rules): a cut-out tile sits on the plate and wants the plate, while a
   full-bleed tile has no plate showing and wants a colour taken from its own
   art. One ladder, two sources. */
.card.art::after{
  content:""; position:absolute; inset:0; z-index:1; pointer-events:none;
  background:linear-gradient(180deg,
    color-mix(in srgb, var(--scrim, var(--plate-dark)) 99%, transparent) 0%,
    color-mix(in srgb, var(--scrim, var(--plate-dark)) 96%, transparent) 28%,
    color-mix(in srgb, var(--scrim, var(--plate-dark)) 46%, transparent) 50%,
    color-mix(in srgb, var(--scrim, var(--plate-dark))  0%, transparent) 68%);
}

/* Cast variant: a group of characters rather than the storage tile's single
   object, so it is composed like the fan two tiles over rather than like its
   own neighbour — backdrop, scrim, art, copy.

   The base rules above put .tile-art *under* the scrim, which is right when the
   art is scenery behind the copy. Here the cast is the subject, so it moves to
   z-index 2 and sits on top of the scrim exactly as the fan's posters do; the
   scrim's dense top still carries the heading, and its 88% foot now closes the
   backdrop behind the figures' feet instead of washing over them.

   --scrim is sampled from what actually lies beneath it. .cast-bg is this art
   blown up, blurred and centred over the plate, so the dense top of the scrim
   covers the lockup's upper middle composited onto --plate-dark — and this
   asset is half transparent, so the plate is genuinely part of that colour.
   Averaging the pixels alone would have read far too bright. The composite is
   rgb(75,56,55), a warm brown off the armour and skin tones; taken to 45%
   brightness it becomes rgb(34,25,25), which sits in the band the vendor's
   baked SCRIM values occupy while still belonging to this picture.

   Like the fan, this is a cut-out tile — the plate shows past the backdrop's
   feather — so the scrim has to stay near --plate-dark or it bands where the
   gradient ends. rgb(34,25,25) is 15 summed off #1a1b1e, inside the threshold
   where an edge appears.

   The ladder is the fan's, with one stop moved. 94/78/…/88 are carried over —
   the same three planes and the same copy at the top mean the same shape is
   already right — but the 62% stop is raised from 42 to 62.

   That stop is where this tile differs from the fan. At the narrow 4-up columns
   the heading takes a third line and the copy reaches 60% of the tile, landing
   almost exactly on it, and at 42% the backdrop came through warm enough to put
   the paragraph's lightest pixels at 4.33:1 — under AA. Raising it costs
   nothing visually, because the cast is painted *above* this scrim: density
   here dims the blurred backdrop and cannot touch the characters. The same
   pixel now measures 4.68:1. */
.card.art.cast{ --scrim:rgb(34,25,25); }
.card.art.cast::after{
  background:linear-gradient(180deg,
    color-mix(in srgb, var(--scrim) 94%, transparent) 0%,
    color-mix(in srgb, var(--scrim) 78%, transparent) 34%,
    color-mix(in srgb, var(--scrim) 62%, transparent) 62%,
    color-mix(in srgb, var(--scrim) 88%, transparent) 100%);
}
.card.art.cast{
  /* The art is a flow item in this tile, not an absolute layer, so the card has
     to be able to hand it the height the copy does not use. */
  display:flex; flex-direction:column;
}
.card.art.cast .tile-art{
  position:relative; inset:auto; z-index:2;
  /* Takes every pixel the copy leaves and not one more. This is the whole point
     of putting the art in flow: the copy's height is not a constant. The
     heading sets on two lines in a 302px column and three in a 213px one, which
     carries the copy from 39% of the tile to 70%, and no fixed percentage is
     both generous at one end and safe at the other. This was a hand-tuned 46%
     with a media query cutting it to 38% for the narrow 4-up band, and it still
     overlapped the paragraph by 25px at exactly 981px. Free space cannot miss.

     Which matters more here than on the other art tiles, because this one's art
     is painted *above* the scrim: an overlap is not the usual harmless ghosting
     under a dense gradient, it puts the paragraph on unscrimmed art at 1.2:1.

     flex-basis is 0 rather than auto, and both halves of that matter. It keeps
     the art's own 900x726 out of the card's intrinsic height — at auto the
     image's full size became the flex base size and dragged the 2-up card from
     340px to 457px — and it makes the resolved height definite, which is what
     lets the percentage max-height below resolve at all. */
  flex:1 1 0; min-height:0;
  /* Negative margins give back the card's 30px/28px padding so the art may run
     out to the tile's own edges; the 16px top is the air under the copy. */
  margin:16px -28px -30px;
}
.card.art.cast .tile-art img{
  /* Fitted into that box rather than sized off the tile. object-fit:contain
     grows the art until it meets whichever edge comes first — the tile's sides
     in a wide column, the box's own ceiling in a cramped one — and holds the
     asset's ratio either way, so none of the four figures is ever cropped.

     It is object-fit and not max-width/max-height, which is what this started
     as. A percentage max-height resolves against the parent's *specified*
     height, and .tile-art's is auto — the number comes out of flex layout, not
     out of the style sheet — so the constraint evaluated to none and the art
     ran 62px out through the bottom of the tile. An absolutely positioned box
     has no such problem: its percentages resolve against the containing
     block's used size, which is definite by the time it is laid out.

     object-position pins it bottom-centre: centred, so the group sits in the
     middle of the space it was given, and bottom-flush, so the figures stand on
     the tile's floor. The earlier left-of-centre offset belonged to a smaller
     piece of art leaning into the corner the copy left free; at this size there
     is no free corner. The drop-shadow inherited from the base rule stays —
     there is a real cut-out edge here and it needs grounding.

     The box is 15% wider than the tile, and centred on it, so the art may run
     past the tile's sides and .card.art's overflow:hidden trims it. Without
     that the art stopped as soon as it touched the tile's width and left 88px
     of dead plate above it at the widest columns, because the asset is a good
     deal wider than it is tall. 15% is a ceiling rather than an amount: contain
     still scales by whichever axis binds first, so the art takes the extra
     width only while it has height left to grow into. At 1400px it fills the
     height after about 4% and stops there; only the widest columns spend the
     full 15%, which costs the outermost two figures about 7% of their width. */
  position:absolute; left:-7.5%; top:0; width:115%; height:100%;
  max-width:none; max-height:none;
  object-fit:contain; object-position:50% 100%;
  transform-origin:50% 100%;
}

@media (prefers-reduced-motion:reduce){
  /* The driver already declines to run, so --fgx/--fgy never move; what is left
     to freeze is the hover scale. */
  .card.art .tile-art img{ transition:none; }
  .card.art:hover .tile-art img{ --imgs:1; }
}

/* ---------- storage tile: the drawn panel ---------- */
/* The art box. The base .card.art rule lays .tile-art over the whole tile with
   inset:0, which is right for a bled photograph; this one is an object with
   edges, so it takes a band and centres inside it. Written at the same depth as
   that rule — .card.art .tile-art is two classes and would otherwise win the
   tie and hold this at the top of the tile.

   A band rather than the flow position the cast tile uses. That tile has to be
   in flow because its art is painted *above* the scrim, so an overlap puts the
   paragraph on unscrimmed art; this group is under the scrim and, since the
   card came off, is mostly empty box — the ring and the two lines are the only
   ink in it, and they sit well inside. What matters here is where the ink
   lands, not where the box's corners reach, and a band places the ink exactly.

   28/7 rather than the 33 it started at: the group's centre moves from 66% of
   the tile to 61%, which is the lift, and the 7% keeps it off the floor. */
.card.art .tile-art.storage-ui{
  top:28%; bottom:7%;
  display:grid; place-items:center;
  perspective:1100px;
}
/* Where the copy runs to three lines the ring needs to start lower, and the
   lever for that is the group's size rather than its position: the band is
   only 65% of the tile's height, so moving the band down pushes the group out
   through the floor — measured overflowing by 36px at a 336px tile. Shrinking
   the group moves its top edge down and its bottom edge up at once.

   The threshold is asked of the tile, not the window: the four-up grid puts a
   312px tile on a 1440px page and a 390px one on a 430px page, so no viewport
   breakpoint separates them. 300px is the tile's *content* box — a container
   query measures that, not the border box, which is why a first attempt at
   350px fired 56px of padding early and caught two tiles that were fine.

   Without this the ring's own ink ran under the paragraph by 10.1px at 1440,
   6.8px at 768 and 11.9px at 360, and cleared comfortably everywhere else.

   The override itself sits below .su-panel, not here: a container query carries
   no specificity of its own, so at equal specificity source order decides, and
   written above the rule it modifies it silently lost every time. */
.grid.c4 .card{ container:sutile / inline-size; }
/* The isometric attitude. rotateY lays the panel back to the right so its left
   edge stands nearest the viewer, which is the angle the photograph it replaces
   was shot at; rotateX drops the top edge away so it reads as lying down rather
   than standing up, and the small rotateZ stops the whole thing sitting square
   to the tile's own edges.

   The parallax translate comes first and the hover scale last: transforms apply
   right to left, so scaling after the rotations grows the panel in its own
   plane rather than along the tile's axes, and translating before them moves it
   across the tile rather than along its tilted surface. --fgx/--fgy are the
   same pair the driver publishes for the other art tiles, so this one drifts
   under the pointer exactly as they do with no change to the script. */
/* The ring and its figures, with nothing behind them. This began as a drawn
   card — gradient, hairline, rounded corners, its own shadow — standing in for
   the photograph it replaced. The card turned out to be the least interesting
   part: it repeated the tile's own plate at a smaller size and boxed the one
   thing worth looking at. Dropping it lets the ring take the space the card was
   spending on its own edges, which is most of why this reads larger now; the
   ring itself is up from 104px to 200px at the cap.

   The perspective attitude stays. Without it the group would read as an overlay
   printed on the tile rather than an object lying on it, and the other three
   art tiles all hold something in perspective. */
.su-panel{
  --su-ease:cubic-bezier(.16, 1, .3, 1);
  /* The perspective divide does not move the near and far halves by equal
     amounts: the receding side shrinks faster than the approaching side grows,
     so the painted centroid lands left of the panel's own anchor even though
     the anchor is centred by place-items.

     A percentage, not a length, and that matters. The shift goes with the
     *square* of the group's width, so a flat figure tuned at the widest column
     overcorrected by 4px at the narrowest. translateX resolves its percentage
     against the element's own width, which turns the compensation linear in w
     — still not quadratic, but close enough across the 285-380px the group
     actually spans to land every column inside about a pixel. */
  --su-nudge:2.1%;
  width:min(92%, 380px);
  /* The type inside is sized against this box. It has to be a container query,
     because a percentage font-size resolves against the *parent's font size*,
     not against any width — the clamps here were written as percentages first
     and every one of them sat pinned to its floor, so the figures never grew
     with the composition at all. They looked deliberate: a floor is a
     legitimate value, and nothing overflowed. cqw is a share of this element's
     inline size, which is the thing they were always meant to track. */
  container-type:inline-size;
  display:grid; gap:clamp(12px, 5%, 22px); justify-items:center;
  transform:
    translate3d(var(--fgx,0px), var(--fgy,0px), 0)
    translateX(var(--su-nudge))
    rotateX(9deg) rotateY(21deg) rotateZ(-2deg)
    scale(var(--imgs,1));
  transition:transform .22s ease-out;
  will-change:transform;
}
.card.art:hover .su-panel{ --imgs:1.04; }
/* The narrow-tile reduction, placed here so it wins on source order. */
@container sutile (max-width: 300px){
  .su-panel{ width:min(75%, 380px); }
}

.su-ring{ position:relative; display:grid; place-items:center; width:clamp(120px, 70%, 250px); }
.su-ring svg{ display:block; width:100%; height:auto; transform:rotate(-90deg); }
/* pathLength normalises the circle to 100 units, so the dash figures below are
   literally percentages and the radius can change without recomputing 2πr. */
.su-track, .su-fill{
   /* TGD: Add !important to fill style to override style#sa-xbox-base-overrides */
  fill:none !important; stroke-width:9;
  stroke-dasharray:100;
}
.su-track{ stroke:rgba(255,255,255,.14); }
.su-fill{
  stroke:var(--accent); stroke-linecap:round;
  /* 5 units left undrawn is the 95% the ring rests at. */
  stroke-dashoffset:5;
  transition:stroke-dashoffset 1.5s var(--su-ease);
}
/* The figure inside the ring. 15cqw lands it at about a quarter of the ring's
   own diameter, the ring being 70% of the same box.

   Semibold, matching the label under it rather than going a step heavier. 600
   is a real instance and not a synthesised one — the same line sets 155.1px at
   400, 165.6 at 600 and 171.5 at 700, so the face has a genuine semibold to
   land on. Worth checking rather than assuming: --display is a system stack,
   and a missing weight there renders identical glyphs while the declaration
   looks applied.

   Segoe Sans, not the mono face this started in. Mono was chosen so a counting
   number could not shuffle its glyphs sideways on every step, and dropping it
   gives that problem back — which is what font-variant-numeric:tabular-nums is
   for. The face carries a tabular set, so the digits set on one width and the
   figure stays put while it counts: measured 0px of drift across every
   two-digit value with the property, 14.9px without it. */
.su-pct{
  position:absolute;
  font-family:var(--display); font-weight:600;
  font-variant-numeric:tabular-nums;
  font-size:clamp(24px, 15cqw, 56px); line-height:1;
  letter-spacing:-.02em;
  color:#f4f5f4;
}

.su-rows{ display:grid; gap:5px; justify-items:center; text-align:center; }
.su-title{
  font-family:var(--display);
  font-size:clamp(16px, 7.6cqw, 28px); line-height:1.2; font-weight:600;
  color:#f4f5f4;
}
.su-free{
  font-family:var(--display);
  font-variant-numeric:tabular-nums;
  font-size:clamp(13px, 6cqw, 22px); line-height:1.2;
  color:rgba(244,245,244,.66);
}

.card.art:hover .su-fill,
.card.art:focus-within .su-fill{
  stroke-dashoffset:65;
}

@media (prefers-reduced-motion:reduce){
  /* Preserve both states without interpolating between them. */
  .su-panel{ transition:none; }
  .card.art:hover .su-panel{ --imgs:1; }
  .su-fill{ transition:none; }
}

/* ---------- sync tile ---------- */
/* A TV with a handheld in front of it, the same frame playing on both. The
   other two parallax tiles each animate a single plane, so the driver's
   --bgx/--bgy pair — kept there precisely so a background layer could be added
   without touching it — has never had a user until now. This tile has a real
   near object and a real far one, so the TV takes the background pair and the
   handheld the foreground pair, and the driver's own 2:1 gain does the work:
   20px of travel on the handheld against 10px on the TV. No change to the
   driver; it was already written for this.

   Two wrappers rather than one, mirroring the fan tile. The drift has to be
   quick to feel attached to the pointer (.22s) while the hover spread wants to
   be slower and softer (.55s), and a single element cannot carry two durations
   on one property. Wrappers drift, images spread. */
.card.sync{
  position:relative; overflow:hidden; isolation:isolate;
  display:flex; flex-direction:column;
}
.card.sync > :is(h2, h3), .card.sync > p{ position:relative; z-index:3; }

/* The art region takes whatever vertical space the copy leaves, and the negative
   margins put it back out to the card's edges. This is the whole trick: the copy
   is 19% of the card at the widest column and 60% at the narrowest, so art sized
   from the card collides with the text at one width and floats at another. Sized
   from what is left over, it simply shrinks instead of climbing up behind the
   words, at every width and with no breakpoints. */
.card.sync .sync-stage{
  position:relative; z-index:1; pointer-events:none;
  flex:1 1 auto; min-height:0;
  margin:0 -26px -26px;
  container-type:inline-size;
}

/* Acrylic backdrop: the same frame that is playing on both screens, blown up
   and blurred so the tile reads as the game bleeding out of them rather than as
   a second picture. It is only ever seen through the blur, so the file is 480px
   wide and 23 KB — a 34px radius erases everything finer than itself, and no
   amount of extra source resolution survives it. */
.card.sync .sync-bg{
  position:absolute; inset:-16%; z-index:0; pointer-events:none;
  background:url('https://cms-assets.xboxservices.com/assets/21/9b/219b8f2e-2078-490e-90f2-7789492f3621.jpg?n=sync-bg.jpg') center/cover no-repeat;
  filter:blur(34px) saturate(1.3);
  transform:scale(1.12);
}
/* The tint is what turns the blur into a material instead of a soft photo. It
   is a child of the blurred element, so it is blurred too — harmless for a flat
   fill except at its own edges, which is why it is inset well past the parent's
   bleed: the feather then falls outside the card and is clipped away. */
.card.sync .sync-bg::after{
  content:""; position:absolute; inset:-25%;
  background:color-mix(in srgb, var(--plate-dark) 44%, transparent);
}

.card.sync .sync-far,
.card.sync .sync-near{
  position:absolute; inset:0;
  transition:transform .22s ease-out;
  will-change:transform;
}
.card.sync .sync-far{ z-index:0; transform:translate3d(var(--bgx,0px), var(--bgy,0px), 0); }
.card.sync .sync-near{ z-index:1; transform:translate3d(var(--fgx,0px), var(--fgy,0px), 0); }

/* Both are parked against the foot of the art region rather than measured down
   from its top, so the stack stays seated on the bottom edge of the card as the
   region grows and shrinks with the copy. */
.card.sync .sync-tv,
.card.sync .sync-hh{
  position:absolute; left:50%; max-width:none;
  transform:translateX(-50%);
  transition:transform .55s cubic-bezier(.2,.7,.2,1);
  backface-visibility:hidden;
}
/* Every value is a percentage of the art region, so the composition holds its
   proportions whatever the copy leaves. The cqw ceilings are the same ratios
   measured against the card's width instead, and they take over once the region
   is taller than 90% of that width — which happens on a wide card whose copy
   fits on one line, where a purely height-derived TV would grow wider than the
   card. Because both halves of each min() are the same proportion expressed two
   ways, the stack keeps its exact shape in either regime; only its size changes. */
.card.sync .sync-tv{
  bottom:min(34%, 30.6cqw); height:min(64%, 57.6cqw); width:auto;
  filter:drop-shadow(0 18px 26px rgba(0,0,0,.55));
}
/* The handheld's frame carries 7% of clear pixels down each side and 12% below
   the grips, so its box is bigger than what it draws: this renders as a device
   about 72% of the card wide against the TV's 89% — the eight-tenths the
   composition asks for. */
.card.sync .sync-hh{
  bottom:min(9.5%, 8.55cqw); height:min(39%, 35.1cqw); width:auto;
  filter:drop-shadow(0 22px 30px rgba(0,0,0,.72));
}

/* The spread keeps the drift's near/far ratio rather than moving both the same
   distance: at the widest card the handheld travels ~13px against the TV's
   ~7px. Percentages are of each image's own height, so the gesture scales with
   the tile instead of reading as a large shove on a small card. */
.card.sync:hover .sync-tv{ transform:translateX(-50%) translateY(-2.5%); }
.card.sync:hover .sync-hh{ transform:translateX(-50%) translateY(8%); }

/* The scrim is an empty element sitting in normal flow directly after the copy,
   so it is anchored to wherever the text actually ends. This matters because
   the copy's depth is not fixed: the same two lines occupy 19% of the card at
   the widest column and 60% at the narrowest, and a scrim measured from the top
   of the card can only be right at one of those. Attached to the copy's own
   foot it is right at every width, with no breakpoints.

   Zero-height and in flow, so it adds nothing to the layout; the painting is
   all in the pseudo-element, which runs from just below the last line up past
   the top of the card. It sits above both device planes and below the copy. */
.card.sync .sync-veil{ display:block; height:0; position:relative; z-index:2; }
.card.sync .sync-veil::before{
  content:""; position:absolute; pointer-events:none;
  /* Wider than the card's 26px padding and taller than any card, because the
     card clips it — cheaper than measuring either one. */
  left:-30px; right:-30px; bottom:-26px; top:-1200px;
  background:linear-gradient(0deg,
    color-mix(in srgb, var(--plate-dark)  0%, transparent) 0,
    color-mix(in srgb, var(--plate-dark) 20%, transparent) 26px,
    color-mix(in srgb, var(--plate-dark) 46%, transparent) 58px);
}

@media (prefers-reduced-motion:reduce){
  /* The driver already declines to run, so the wrappers never move; what is
     left to freeze is the hover spread. The stack keeps its resting overlap —
     that is the tile's whole idea, and none of it is motion. */
  .card.sync .sync-tv, .card.sync .sync-hh{ transition:none; }
  .card.sync:hover .sync-tv{ transform:translateX(-50%); }
  .card.sync:hover .sync-hh{ transform:translateX(-50%); }
  .card.sync .sync-far, .card.sync .sync-near{ transform:none !important; }
}

/* ---------- get started: three steps, no plate ---------- */
/* The inset treatment: a quieter centred head over columns pulled in from the
   page edges, rather than a big left-aligned title over a full-width row.

   Written as a section modifier rather than hung off #how, so it stays a
   treatment that can be applied elsewhere and keeps the specificity of an
   ordinary class. An id selector here would outrank every .sec-title rule above
   it by a whole level and make the next override a fight. */
.sec-inset .sec-title{
  /* Down from clamp(40px, 5.6vw, 56px). That ramp is for a title of a few
     words; this one is a sentence, and at 56px it ran the full width and read
     as a banner rather than as a lead into the steps below it.

     The three properties that come with the size: line-height goes from 1 to
     1.25, because 1 is only safe on a line that never wraps and this one always
     does; the display tracking relaxes from -.035em to -.02em, since -.035 is
     drawn for headline sizes and pinches here; and the measure is capped to the
     same width the row below takes, so the head and the columns share an edge
     rather than being two different widths stacked on each other. */
  /* TGD: Make this the same font size as other section titles */
  /* font-size:clamp(22px, 1.9vw, 30px);
  line-height:1.25;
  letter-spacing:-.02em; */
  text-align:center;
  max-width:var(--inset-measure);
  margin-inline:auto;
  /* Double the 24px every other section title carries. At 24px the head sat
     close enough to the numerals to read as part of the same block; the extra
     air is what separates a sentence that introduces the steps from the steps
     themselves. Written as a longhand so it overrides only the bottom of
     .sec-title's margin shorthand and leaves the auto sides above intact. */
  margin-block-end:48px;
}
/* A cap in px, not a percentage inset. A percentage takes the same bite at
   every width, which is wrong here: at 1920 it leaves the step titles on one
   line and by 1440 it has them on four. A measure cap bites hard where there is
   width to spare and stops biting on its own once the page is narrower than it
   — which is the taper a percentage would need breakpoints to fake.

   900px puts the row at 52.6% of a 1712px viewport, against the 53.7% of the
   reference this follows. The step titles run to three lines at that width;
   that was the trade taken deliberately, since the alternative that keeps them
   on one line is a 11% inset that does not read as inset at all. */
.sec-inset{ --inset-measure:1400px; }

/* Three even columns straight on the page. This began as a 1fr 2fr split
   holding a steps list and a requirements checklist, became one sequence on a
   white card when the checklist folded in, and has now lost the card too.

   Nothing replaces the plate's padding. The row sits under the section title
   and takes the gap the title's own margin already provides, so the two line up
   on one measure and the title-to-content spacing matches the ways-to-play row,
   which never had a plate to inset it.

   The list is the reveal target now that the wrapper div is gone; the rule for
   that sits with the other reveals rather than here, so all the entrance
   motion stays in one place.

   min-width:0 on the items is what holds the thirds even: without it the
   longest unbroken run in a heading can widen its own column and pull the
   other two in. */
.steps-row{
  list-style:none; margin:0 auto; padding:0 80px;
  max-width:var(--inset-measure);
  display:grid; grid-template-columns:repeat(3, 1fr);
  /* Column gutter is a flat number, not a viewport ramp, because the row is a
     flat 900px for the whole range in which it has three columns: the cap holds
     it at the measure from 981px up, and at 980 it collapses to one. A vw term
     would keep moving the gutter while the thing it divides never changed.

     64px comes from the reference this layout follows, by ratio rather than by
     pixel: its columns run about 255px with 67px between them, a gutter of
     0.26 of a column. Holding that against a 900px measure gives 3c + 2(0.26c)
     = 900, so c = 256 and the gutter = 66. 64 is that, rounded to the page's
     own 8px rhythm.

     Row gap keeps its ramp — that one is the vertical space between stacked
     steps below the collapse, where the row really is viewport-width. */
  column-gap:64px;
  row-gap:clamp(24px, 2.6vw, 40px);
}

/* Optical centring. The row's box is centred exactly — measured at 0.0px off
   the page axis at every width — but its painted ink is not: the third column's
   title stops short of its column edge, so the row paints 34px past the
   header's leftmost letter and only 12px past its rightmost. The block reads as
   sitting left.

   A grid is normally centred on its box and the ragged right of a text column
   is left alone. Here the header directly above is a centred block, which gives
   the eye an axis to compare against, and 11px against that is visible.

   Half the measured shortfall, as a relative offset so nothing in the layout
   moves — the columns keep their widths and the gutters keep theirs. Only the
   painted result shifts.

   Held to widths where the row has room to move into. The cap keeps the row at
   900px while the wrap keeps growing, so above 1200px there is at least a
   gutter of slack either side; below it the row nearly fills the wrap and a
   nudge would trade a centred box for a visibly uneven one. The shortfall is
   also larger there (45.6px against 22.1px) because --body-copy drops to its
   13.5px floor and the titles wrap differently, so one value would not have
   served both anyway. */
@media (min-width:1200px){
  .steps-row{ position:relative; inset-inline-start:11px; }
}
/* The numeral sits above its title, both on the column's left edge. This was an
   inline gutter — numeral left, title right — which cost the title about 100px
   of an already inset column. Stacked, the title gets the whole column.

   No alignment properties, deliberately. The default stretch is what gives the
   right result for both children at once: .step carries a fixed inline-size, so
   stretch cannot widen it and it falls to the start edge; .step-body has auto
   width, so it stretches across the column and its text wraps against the full
   measure. justify-items:start would look equivalent and is not — it
   shrink-to-fits .step-body, so the title would wrap against its own content
   width instead of the column and break several words early.

   No column template either. Two children in one implicit column is exactly the
   stack this wants, and naming a template would be a second thing to keep in
   step with the markup.

   align-items:center is gone with the inline arrangement it served: stacked,
   there is no shorter box to centre against a taller one.

   align-content:start is load-bearing, though. The row stretches every li to
   the tallest of the three, and a grid distributes that extra height into its
   auto rows — so a step whose title fits on one line had the slack pushed
   between its numeral and its title, opening the gap to 28px against the 16px
   on the step that wrapped, and dropping two of the three titles 12px below the
   third. Packing to the start leaves the slack at the bottom of the column,
   where nothing is looking at it, and the three titles start on one line. */
.steps-row li{
  min-width:0;
  display:grid; align-content:start;
  gap:16px;
}

/* Body size, from the same token the card paragraphs use, so a step title and
   the copy on an option card set identically — which is the point: these read
   as list entries now, not as headings that happen to be small. (The dark c4
   tiles opt out of the token with a fixed 15px one rule up, so they are not
   part of this pairing.)

   The tracking and leading remain explicit so these labels match the adjacent
   option-card copy rather than picking up broader paragraph treatments.
   Margin is zero because each label is the only item in its column. */
.steps-row p{
  font-size:var(--body-copy);
  font-weight:500;
  letter-spacing:normal;
  line-height:1.55;
  margin:0;
}

/* ---------- ways to play / plans ---------- */
.sec.major.sec-backdrop{
  position:relative;
  isolation:isolate;
  overflow:hidden;
  width:100%;
  aspect-ratio:16/5;
  min-height:430px;
  padding:0;
}
.sec-backdrop-media{
  position:absolute;
  inset:0;
  z-index:-1;
  max-width:2560px;
  margin-inline:auto;
}
.sec-backdrop-media,
.sec-backdrop-media img{
  display:block;
  width:100%;
  height:100%;
}
.sec-backdrop-media img{ object-fit:cover; }
@media (min-width:980px){
  .sec-backdrop-media img{
    object-fit:contain;
    object-position:center top;
  }
}
.sec-backdrop > .wrap{
  min-height:100%;
  display:flex;
  flex-direction:column;
  justify-content:flex-end;
  padding-bottom:clamp(32px, 5vw, 96px);
}
.sec-backdrop .sec-title{
  align-self:center;
  margin-bottom:24px;
  color:#fff;
  text-align:center;
}
.sec-backdrop .opt-row{
  width:100%;
  max-width:1440px;
  margin-inline:auto;
  gap:12px;
}
.opt{
  display:block;
  padding:0; overflow:hidden;
}
.opt-body{
  min-height:100%;
  display:flex;
  flex-direction:column;
  align-items:center;
  padding:30px;
  text-align:center;
}
/* No top margin: the 16px here used to clear the "Option A/B" badge that sat
   above the heading, and the card's own 30px padding now sets the inset.
   Size comes from the shared --tile-title. */
.opt h3{ margin:0 0 10px; }
/* Product names hold together across a line break. "Cloud Gaming with XBOX Game
   Pass" was breaking after "Game" and leaving "Pass" alone on the second line;
   binding the name makes the heading break in front of it instead, so it reads
   "Cloud Gaming with / XBOX Game Pass".

   A nowrap unit rather than a hard <br>, because the tile goes one-up and wide
   between roughly 480 and 900px, where the column reaches 758px and the whole
   heading fits on one line -- a forced break would look arbitrary there. This
   way the break appears only when the line actually needs it. Safe at every
   width: the name measures at most 210px against a 450px column, and 150px
   against the 258px column at 360px. */
.nb{ white-space:nowrap; }
.opt > p{ margin-bottom:22px; }
.opt .cta-row{ margin-top:auto; padding-top:24px; display:grid; place-items:center; gap:10px; }

/* ----- the quiet option tile -----
   The whole tile is the control. position:relative is what makes that work —
   the link stretches a transparent ::after over the card, so the hit area is the
   tile while the accessible name, the href and the keyboard stop all stay on the
   one real link. Nesting the card inside an <a> would swallow the heading into
   the link text; a click handler on the card would give a mouse-only control. */
.opt-quiet{
  position:relative; 
  /* cursor:pointer; */
  /* Beats .lift on source order at equal specificity, so it has to carry
     --tx-lift as well as the wash's own two properties. */
  transition: var(--tx-lift), var(--tx-wash);
}
/* ...but .has-reveal .card[data-reveal] outranks both, so on a revealed page the
   rule above never applies. That is what produced the 1px stroke: the reveal's
   list has no background-color, so the fill snapped to the dark plate on the
   first frame while the border eased across behind it, painting the resting
   hairline as a bright ring on the new dark surface -- measured at 11.41:1 at
   t=0, decaying over ~160ms.
   Restating the wash at a specificity that beats the reveal rule fixes it. Both
   the border and the fill now travel together, and because they start from
   different colours (hairline vs paper) the edge never reads stronger than its
   resting 1.27:1 at any point in the animation. */
.has-reveal .card.opt-quiet[data-reveal]{
  transition: opacity .34s ease, var(--tx-lift), var(--tx-wash);
}

/* The hover/focus wash. The tile flips to the page's dark plate and the content
   inverts with it: acid green for the icon and the call to action, white for the
   heading and its sub.

   --plate-dark rather than a fresh #000, because this is the same inversion the
   "What you can do" tiles already perform, and reusing their plate means the two
   dark surfaces on the page are the one colour instead of two blacks a shade
   apart. The ink tokens are that block's values for the same reason.

   Dark is what lets the accent do the work it could not do on white. Lime is a
   very light colour -- 0.737 relative luminance -- so on the light card it had
   to be substituted for a darker green to stay legible; against this plate the
   raw accent reads at 12.91:1 and can finally be itself. Measured on the plate:
   heading 15.76:1, sub 7.48:1, CTA 12.91:1.

   An acid fill was tried in place of this and reverted. It inverted cleanly on
   paper — the same pair reversed, so the same 12.91:1 — but a green surface
   competes with the art now running down the left column, where a dark one
   sits behind it.

   Re-mapped as tokens rather than restated per child, matching how .grid.c4
   inverts. `color` is the exception and is set explicitly: the shared card-heading rule never
   declares one, and an inherited colour arrives already computed, so re-mapping
   --ink alone would never reach it.

   --accent-text and --lime-ink point at the raw accent here, which is what
   makes the CTA lime against the plate. That pairing is specific to a dark
   fill: on a light one it would paint the link its own background colour.

   border-color goes to the plate, not transparent: unlike the .c4 tiles these
   have a visible resting hairline, and clearing it would let the page show
   through a 1px ring around the wash.

   :focus-within, so the wash answers the keyboard as well as the pointer --
   focusing either link is focus inside the card. */
/* TGD: Removing card hover styles */
/* .opt-quiet:hover,
.opt-quiet:focus-within{
  background:var(--plate-dark);
  border-color:var(--plate-dark);
  color:var(--ink);
  --ink:   #f4f5f4;
  --ink-2: rgba(244,245,244,.66);
  --lime-ink:var(--accent);
  --accent-text:var(--accent);
} */
/* Block, not the grid the other cta rows use: a grid item is blockified, and a
   blockified link draws its underline per inline child, which is what breaks the
   line between the label and the arrow. Keeping this an ordinary block leaves
   the link inline-level, so one continuous decoration runs the whole string. */
.opt-quiet .cta-row{ display:block; padding-top:18px; }

/* One slot height for all three marks, taken from the same clamp as the .step
   numerals above, so the icons and the "01 / 02 / 03" set at one scale and track
   together if that ramp ever moves. Holding the slot — not just the glyph — is
   what keeps the three headings on one line across the row, since the marks
   inside it are different heights.

   Both marks are commented out in index.html for now. This rule and .opt-ico
   below are kept so uncommenting the markup is all it takes to restore them. */
.opt-quiet{ --ico-slot:clamp(40px, 3.9vw, 56px); }

/* The square icons fill the slot. They land at nearly the same optical height as
   the numerals without a fudge factor: the timer spans 18.5 of its 24-unit
   viewBox (0.77), and the mono digits' caps run about 0.72 of their em box, so
   matching the boxes matches the marks.

   12px below, not the 20 it started at, to match the steps plate optically. The
   numeral there carries 10px, but its box is not its glyph: with line-height:1
   the baseline sits around 0.85 of the box, so roughly 8px of that box is empty
   before the margin even starts. These icons leave far less — the timer only
   2.75 of its 24 units — so the same visual gap needs a smaller number here.

   The green comes from the same --lime-ink the step numerals and the hero
   eyebrow read, with --accent-text as the fallback for any surface that has not
   defined it. Contrast is a graphic threshold here (3:1), which this clears on
   the white plate. */
.opt-ico{
  display:block; margin:0 0 12px;
  width:var(--ico-slot); height:var(--ico-slot);
  color:var(--lime-ink, var(--accent-text));
  /* Matches the wash on the tile. The custom property flips instantly, but the
     colour it resolves to is an ordinary animatable value, so the mark crosses
     to dark over the same 220ms the card takes to fill. */
  transition:color .22s ease;
  /* Stroke set here rather than on the paths so it can be tuned against the
     rendered size. Fluent's own ramp thins the stroke relative to the box as the
     icon grows — roughly 7% of the box at 24 down to 6% at 48 — so scaling the
     24px weight of 1.8 straight up to 56px would land 4.2px on screen and read
     heavier than the mark ever does at its native sizes. 1.45 units is that 6%,
     which comes out near 3.4px beside numerals whose stems run about 6px. */
  stroke-width:1.45;
}

/* The text link. Not .btn.ghost and not the vendor's .tlink: .tlink sets 13px
   700 ink with a lime arrow, and this wants the card's own body size in green
   throughout, with the arrow reading as part of the string.

   display:inline, deliberately, where this began as inline-flex. A flex
   container decorates each of its items separately, so the rule came out in two
   pieces with the gap between them bare. As one inline box the label, the space
   and the arrow are a single run. nowrap keeps that run on one line.

   No resting rule — the green plus the arrow already mark it as a link
   against body copy that is neither. */
   /* TGD: Updated to be more generic text-link in line with HP */
.tlink-green{
  display:inline-flex; white-space:nowrap;
  font-size:13px; font-weight:700; line-height:1.5;
  color:var(--olive-ink, var(--accent-text));
  text-decoration:none; gap: 7px;
  transition:color .16s ease, gap .16s ease;
}
/* The whole tile is the hit area, so the hover state has to answer a pointer
   anywhere on the card, not just over the string itself.

   The rule is a background gradient rather than text-decoration. An underline is
   drawn per inline box, so the .arr span's own margin falls outside every box
   that carries one and the line breaks there — which survived both dropping the
   flex gap and skip-ink:none, because the hole was never ink to skip. A
   background paints across the parent's whole inline box, child margins
   included, so the line is continuous by construction.

   The band sits at the bottom of the inline content area, which is roughly a
   descender below the baseline — about where text-underline-offset:3px put it. */
.tlink-green{
  background-image:none;
  background-repeat:no-repeat;
  background-size:100% 1.5px;
  background-position:0 100%;
}
/* TGD: Replace card hover styles with link hover styles */
.tlink-green:hover,
.tlink-green:focus-visible{
  background-image:linear-gradient(currentColor, currentColor);
}

.tlink-green .arr {
   display:inline-block;
   transition: transform .16s ease;
}

.tlink-green:hover .arr {
    transform: translateX(3px);
}

/* The stretched hit area. Transparent, over the whole card, and the card is the
   nearest positioned ancestor. */
   /* TGD: Remove clickable card overlay */
/* .tlink-green::after{ content:""; position:absolute; inset:0; } */
/* Except where the card carries two of them. The stretch turns a card with one
   link into a single big target, which is the right trade when there is only
   one destination; with two it is actively wrong — both would cover the whole
   card, the later one would win on paint order, and the first link would be
   unclickable everywhere including on its own text. So a card offering a
   choice asks for the choice to be made, and each link is the size it looks.

   :has() on the row rather than a modifier class, so adding or removing a
   second link needs no second edit somewhere else to stay correct. */
.cta-row:has(.tlink-green + .tlink-green) .tlink-green::after{ content:none; }
/* Air between the pair. A margin on the first rather than the second, so the
   gap disappears rather than becoming an indent if the row is narrow enough to
   wrap them onto separate lines.

   Not a flex or grid gap: .opt-quiet .cta-row is display:block on purpose,
   because a flex or grid item is blockified, and a blockified .tlink-green
   paints its background rule across the full row rather than across its own
   text — the rule is a background precisely so it survives the .arr span's
   margin, and blockifying trades that problem for a worse one. */
.opt-quiet .cta-row .tlink-green:not(:last-child){ margin-right:26px; }
/* A little more air than the markup's single space, and no hover nudge: the
   arrow used to slide 3px to acknowledge the pointer, which now leaves it
   overhanging the end of a rule that does not move with it. The rule is the
   acknowledgement instead. */
/* .tlink-green .arr{ margin-left:.15em; } */

/* The three plan rows list the Game Pass SKUs as plain rows on the card. The
   gradient backplates that reproduced the supplied subscription cards are gone,
   so each SKU is now identified by its name being filled with that plan's own
   gradient instead. The two stops per plan are the light and dark ends of the
   original artwork's ramp, both dark enough to clear AA on the white card. */
.plan{
  padding:14px 0 16px; margin:0;
  border-bottom:1px solid var(--rule-soft);
}
.plan:last-of-type{ border-bottom:0; }
.plan-line{
  display:flex; align-items:baseline; justify-content:space-between;
  gap:12px;
}
.plan strong{
  font-family:var(--display); font-size:clamp(18px, 1vw, 24px); font-weight:700; letter-spacing:-.02em;
  /* Painted through the text box, so the ramp runs across the word itself.
     -webkit- prefixes are required: background-clip:text is still prefixed in
     WebKit/Blink, and the unprefixed color property is the fallback for any
     engine that ignores the clip. */
  background:linear-gradient(90deg, var(--plan-a), var(--plan-b));
  -webkit-background-clip:text; background-clip:text;
  -webkit-text-fill-color:transparent; color:var(--plan-a);
}
/* Matched to .plan strong so the two halves of the row read as one line of
   type; only the colour separates them. */
.plan .hrs{
  font-family:var(--display); font-size:clamp(16px, 1vw, 18px); font-weight:700; letter-spacing:-.02em;
  color:var(--ink-3);
}
.plan .hrs b{ color:var(--ink); font-weight:700; }

/* The two stops per plan are the light and dark ends of the original artwork's
   ramp, both dark enough to clear AA on the white card. The fill is a gradient
   painted through the text, so it is the *lighter* stop that sets the worst
   case here. */
.plan--ultimate{ --plan-a:#0f5a53; --plan-b:#04303f; }
.plan--premium{  --plan-a:#7b3257; --plan-b:#2d1b32; }
.plan--essential{ --plan-a:#245277; --plan-b:#232640; }

.price{
  border:1px solid var(--rule-soft); border-radius:var(--r-card);
  background:var(--paper-3); padding:30px; text-align:center;
}
.price .big{ font-family:var(--display); font-weight:700; font-size:38px; letter-spacing:-.03em; line-height:1; }
.price .sub{ font-size:13px; color:var(--ink-3); margin-top:8px; }
.price .amt{ font-family:var(--display); font-weight:700; font-size:44px; letter-spacing:-.03em; margin-top:22px; }

/* ---------- the three-up variant's card contents ----------
   Used only by index-3up.html. Every rule here is scoped to .opt-row-3, so
   index.html is untouched; the two pages share this stylesheet.

   These cards carry no art, so the tile's art/copy pair collapses to a single
   column and the body supplies all the padding. Each card is a heading, a line
   of copy, a list of what you get, and one green text link pinned to the foot
   by .opt .cta-row's margin-top:auto — so the three links line up across the
   row however unevenly the lists above them run.

   The link is the main page's own .tlink-green, so .opt-quiet's whole-tile
   click target works here unchanged, and each card carries exactly one: the
   stretch covers the whole tile, so a second link would cover it too and the
   first would become unclickable.

   This was briefly a filled .btn instead, which does not work and is worth
   recording. .btn:hover applies a transform; a transform makes the element a
   containing block for absolutely positioned descendants; so a stretched
   ::after would snap from the tile back to the button the moment the pointer
   entered the tile, ending :hover, releasing the transform, re-expanding the
   ::after and starting again — a flicker loop rather than a static bug. */
.grid.opt-row-3 .opt{ grid-template-columns:1fr; }

.opt-row-3 .opt-body { 
   display: flex;
   align-items: normal;
   text-align: left;
   flex-direction: column;
   margin:0 0 22px;
   padding: 30px; 
}

/* Air under the sub-copy. .opt > p already sets 22px and is not what is doing
   the work here: the markup is .opt > .opt-body > p, so that child selector
   matches nothing on either page. It goes unnoticed on index.html, where the
   sub-copy is followed by the CTA row and .cta-row's own padding supplies the
   gap. Here a list follows it, and with the rule inert the two were 4px apart.
   Left alone rather than repaired in place: making .opt > p match would add
   22px under the sub-copy on index.html, which is a change to the main page
   that nothing asked for. */
.opt-row-3 .opt-body > p{ margin:0 0 22px; }

/* What each option gives you. A tick in ink rather than in the accent: three
   lime ticks beside a lime button is the accent doing two jobs at once, and the
   button is the one that needs it. */
.opt-row-3 .opt-feats{ list-style:none; margin:0 0 22px; padding:0; display:grid; gap:14px; }
.opt-row-3 .opt-feats li{
  display:grid; grid-template-columns:auto minmax(0, 1fr); gap:10px; align-items:start;
  font-family:var(--display); font-weight:600; font-size:clamp(16px, 1vw, 18px); line-height:1.4; color:var(--ink);
}
.opt-row-3 .opt-feats li::before{ content:"\2713"; font-weight:700; line-height:1.4; }

/* The hour packs, as a real radio group.

   A fieldset rather than a list: the rows are one choice, and the legend names
   it. min-inline-size:0 because a fieldset's default min-width is min-content,
   which would stop these rows narrowing with the card.

   Each row is a <label> wrapping its input, so the whole row is the hit target
   without a `for`/`id` pair to keep in step.

   These rows needed lifting over a stretched hit area while the card was one
   big link: .opt-quiet stretches .tlink-green::after across the tile from an
   element later in the DOM, which swallowed every click meant for a radio.
   That is gone -- the cards are no longer .opt-quiet and the call to action is
   a button -- so nothing overlays them and no z-index is needed. Worth knowing
   if the whole-tile link ever comes back: it would break these silently, since
   the selected fill only ever appears once something is checked. */
.opt-row-3 .packs{
  border:0; margin:0 0 22px; padding:0; min-inline-size:0;
  display:grid; gap:10px;
}
.opt-row-3 .pack{
  display:grid; grid-template-columns:auto minmax(0, 1fr); align-items:center; gap:12px;
  border:1px solid var(--rule-soft); border-radius:10px;
  background:var(--paper-2); padding:14px 16px;
  font-size:clamp(16px, 1vw, 18px); line-height:1.3; color:var(--ink);
  cursor:pointer;
}
.opt-row-3 .pack strong{ font-family:var(--display); font-weight:700; }
/* accent-color keeps the native control and its native focus ring, and only
   recolours the mark -- a hand-drawn replacement would mean rebuilding both.

   Chrome paints nothing with this until the radio is checked: an unchecked
   radio is drawn as a UA grey ring (measured at #767676, 4.54:1 on the card)
   whatever this says. It is kept for engines that do tint the unchecked
   control, where --lime-ink rather than --accent matters -- the raw accent is
   1.33:1 on the white card and would make a ring that is barely there. */
.opt-row-3 .pack input{
  accent-color:var(--lime-ink);
  inline-size:18px; block-size:18px; margin:0;
  appearance: auto; cursor: pointer;
}
.opt-row-3 .pack input:focus{
  outline: none;
}
/* The selected row fills with the page's dark plate and carries white ink.

   --plate-dark rather than a literal black: it is the same surface the c4 tiles
   and the legal band use, so the selected row belongs to the palette instead of
   introducing a second near-black.

   This replaces an accent fill, and the reason is contrast. The raw green
   measures 1.33:1 against the white card, so the fill was barely a signal on
   its own and the state rested almost entirely on the radio. The plate measures
   about 17:1 against the same card, which makes the selected row unmistakable
   at a glance.

   :has() on the label rather than a class, so the state follows the input and
   there is nothing to keep in step. Exclusivity is the radio group's, not
   something re-implemented here. */
.opt-row-3 .pack:has(:checked){
  background:var(--plate-dark);
  border-color:var(--plate-dark);
  color:#f4f5f4;
}
/* The dot switches with the fill. --lime-ink is a mid green picked to read on
   the white card and drops to about 2:1 on the plate, so the selected row takes
   the raw brand accent instead: too light to carry a dot on white at 1.33:1,
   but about 13:1 against the plate, which is the inversion the two greens exist
   for. Chrome draws the inner mark in a colour it derives from the accent, so
   what the dot actually lands at is measured from pixels rather than declared
   here -- see threeup-check. */
.opt-row-3 .pack:has(:checked) input{ accent-color:var(--accent); }

/* The Game Pass rows reuse .plan wholesale, including the per-SKU gradients.
   Wrapped in .plans for one reason: .plan:last-of-type is meant to drop the
   final hairline, but the rows are divs and so is the .cta-row that follows
   them, so the last div of that type is the CTA and no .plan ever matched.
   Scoping to the wrapper's last child makes the rule describe what it says. */
.opt-row-3 .plans{ margin:0 0 22px; }
.opt-row-3 .plans .plan:last-child{ border-bottom:0; padding-bottom:0; }

/* Focus and hover both lift the card.

   .lift gives the pointer a shadow and a 3px rise; this extends the same thing
   to :focus-within, so arriving by keyboard gets the acknowledgement arriving
   by pointer does. It is the whole treatment now -- these cards carried the
   dark wash until the controls went in, and a card that inverts to a dark plate
   while holding a radio group and a filled button is a lot of movement for what
   is, at that point, a container rather than a control.

   Restating .lift's own three properties rather than adding to them: a rule
   that declares transition drops them otherwise, which is the drift recorded at
   the top of this file.

   The reveal rule outranks a plain class on a revealed page, so the shadow is
   restated at a specificity that beats it -- without that, focus would lift the
   card everywhere except where it actually matters. */
.opt-row-3 .card.lift:focus-within,
.has-reveal .opt-row-3 .card.lift[data-reveal]:focus-within{
  transform:translateY(-3px);
  border-color:var(--rule);
  box-shadow:0 20px 44px -26px rgba(0,0,0,.8);
}

/* ---------- frequently asked questions ---------- */
/* Ported from design-lab-refresh (designlab.css .dl-faq/.dl-q). Two of its
   tokens do not exist on this page: --paper-pure becomes --paper-2, which is
   the purest surface here (#fff, and the page sits on #e7e8ea so the circle
   still reads), and the --t-h3 type step becomes a clamp in this page's scale. */
.faq{ width:100%; }
.faq-q{ border-top:1px solid var(--rule); }
.faq-q:last-child{ border-bottom:1px solid var(--rule); }

.faq-head{
  width:100%; display:flex; align-items:center; justify-content:space-between; gap:20px;
  padding:24px 4px; text-align:left;
  font-weight:700; letter-spacing:-.01em; font-size:clamp(17px,1.15vw,20px);
  color:var(--ink); background:none; border:0;
}
/* The mark is drawn, not set. As a text "+" it could not sit centred here: the
   glyph rasterised to 7px of ink inside a 30px box, and centring 7 in 30 puts
   its edge on 11.5px, which the rasteriser has to resolve to 11 or 12. Both
   axes landed exactly half a pixel out, and no nudge could fix that -- shifting
   it only moves the half pixel to the other side. Drawing the mark removes the
   font, its hinting and its metrics from the question entirely.

   Every number here is even, so each bar centres on whole pixels: 8px of length
   leaves 11px either side, 2px of weight leaves 14px. 8px matches the ink the
   glyph was already drawing, so the mark keeps its old footprint.

   Positioned rather than laid out, with negative margins rather than a
   translate: .faq-q.open rotates this element, and `transform` is one property
   -- a translate here would be overwritten by that rotate. Being out of flow
   also keeps the bars independent of the leftover "+" text node, which stays in
   the markup as a fallback if this stylesheet never arrives.

   Centred ink is what makes the 45deg rotation read as a spin. transform-origin
   is the box centre, so ink that sits off-centre swings along an arc into the
   "x" instead of turning in place. */
.faq-ico{
  flex-shrink:0; width:30px; height:30px; border-radius:50%;
  display:grid; place-items:center; line-height:1;
  background:var(--paper-2);
  position:relative; font-size:0;
  transition:background .3s, transform .3s, color .3s;
}
.faq-ico::before,
.faq-ico::after{
  content:""; position:absolute; top:50%; left:50%;
  background:currentColor;
}
.faq-ico::before{ width:8px; height:2px; margin:-1px 0 0 -4px; }
.faq-ico::after { width:2px; height:8px; margin:-4px 0 0 -1px; }
.faq-q.open .faq-ico{ background:var(--accent); color:var(--accent-on); transform:rotate(45deg); }

/* Collapsed only once the script has claimed the rows, so the answers stay
   readable if it never runs. See the note above the section in index.html. */
.has-faq .faq-body{ overflow:hidden; max-height:0; transition:max-height .45s cubic-bezier(.2,.7,.2,1); }
.faq-body p{ margin:0; padding:8px 4px 26px; color:var(--ink-2); max-width:140ch; }

@media (prefers-reduced-motion:reduce){
  .has-faq .faq-body, .faq-ico{ transition:none; }
}

/* ---------- legal ---------- */
/* The section above the band has to fall off as far as it runs up. .sec.major
   gives a section a light foot on purpose, because it is designed to pair with
   the *next* section's run-up — the two together make the gap between one block
   of copy and the next.

   That pairing breaks against the band. Its 56px of top padding is inside the
   colour, so it does nothing on the light side, and the light foot became the
   entire gap between the FAQ's last hairline and a hard dark edge. Matching the
   run-up restores it, so the FAQ is symmetric and the edge has room to land.

   Reads --sec-runup rather than restating the clamp, so this tracks the run-up
   automatically: it was a flat 64px against a flat 64px top, and when the top
   became a clamp this would otherwise have quietly stopped being symmetric at
   every width above the clamp's floor. */
#faq{ padding-bottom:var(--sec-runup); }

/* Fine print closing the page, on a near-black band that runs the full width.
   Deliberately the quietest type on the page: small, dense, and set in --ink-2
   so it recedes without dropping below AA — 7.68:1 on this plate, which also
   clears AAA for body text, and the margin matters because this is the smallest
   type here at 12-13px. Labels and links step up to --ink so they stay findable.

   #151515 is a literal rather than --plate-dark. That token is for tiles that
   float on the light page and have to stay in step with each other; this is a
   full-bleed band whose job is to hand off to the #000 footer directly beneath
   it, so it is tuned to that seam, not to the tiles.

   Colours are remapped tokens, the same move .grid.c4 .card makes:
   .legal-item h3, p and a all paint with var(--ink)/var(--ink-2) already,
   so redefining the tokens here reaches every one of them without restating a
   colour per rule. --accent-text goes back to raw lime for the link hover —
   the light theme darkens it to #1a6a00 to clear AA on white, and that green
   would sink into this plate.

   Lines run the full content column rather than the 70ch the FAQ answers take.
   That is unusually long for body copy and it is the point: this is reference
   text to be searched, not read start to finish, and it matches the source. */
.sec.legal{
  --ink:   #f4f5f4;
  --ink-2: rgba(244,245,244,.66);
  --accent-text:var(--accent);
  background:#151515;
  color:var(--ink);
  padding:56px 0 44px;
}

/* The vendor footer carries margin-top:24px, drawn for a dark page where it
   shows a sliver of background between two dark blocks. Under this band it
   would instead strand 24px of light paper between the plate and the black
   footer, so it is closed up and the two dark surfaces meet directly. The
   21-point step from #151515 to #000 is what delineates them now.

   Not written as a sibling selector: .foot is a sibling of <main>, not of the
   section, so `.sec.legal + .foot` would never match. The page has exactly one
   footer and the legal band is always the last thing above it. */
.foot{ margin-top:0; }

/* The block has no visible title — it opens straight on the first label, as the
   source does. The heading exists only so the six labels below have something
   to hang off in the outline instead of attaching to the FAQ's h2. */
.legal-title{
  position:absolute; width:1px; height:1px;
  padding:0; margin:-1px; overflow:hidden;
  clip-path:inset(50%); white-space:nowrap; border:0;
}

.legal-item{ font-size:clamp(12px, .74vw, 13px); line-height:1.62; }
.legal-item + .legal-item{ margin-top:20px; }
.legal-item h3{
  font-size:inherit; font-weight:700; color:var(--ink);
  margin:0; line-height:inherit;
}
.legal-item p{ margin:0; color:var(--ink-2); }
.legal-item p + p{ margin-top:14px; }

/* line-height:0 keeps the raised numeral from opening up the line box it sits
   in, which would otherwise space one line of a paragraph wider than the rest. */
/* TGD: Remove styles that conflict with MWF */
/* .legal-item sup{ font-size:.75em; line-height:0; vertical-align:super; } */

.legal-item a{
  color:var(--ink); font-weight:600;
  text-decoration:underline; text-underline-offset:2px;
  text-decoration-thickness:from-font;
  transition:color .15s;
}
.legal-item a:hover{ color:var(--accent-text); text-decoration:underline; }

/* ---------- temporarily hidden chrome ---------- */
/* Both elements live inside the CHROME:START/END fence in index.html and are
   owned by dotcom-refresh, so they are hidden here rather than deleted from the
   markup — that keeps the verbatim copy intact and the chrome regenerable.
   - .util:     the "XBOX @ Gamescom 2026" countdown ticker above the nav. The
                header is `position:sticky; top:0`, so removing the bar above it
                does not affect the sticky offset. #countdown stays in the DOM,
                so the chrome.js timer keeps running harmlessly.
   - #themeBtn: the header's light/dark toggle. The tweaks panel carries the
                one this prototype uses, so this stays hidden to keep a single
                control rather than two that could disagree — chrome.js wires
                this one to a full page reload, which the panel's does not need. */
.util{ display:none; }
#themeBtn{ display:none; }

/* ---------- help agent orb ---------- */
/* The assistant's avatar. The generated chrome.css carries the older version of
   this — a lime radial gradient, a featureless ball — because it is sliced from
   the pinned submodule, and dotcom-refresh has since rebuilt it as the XBOX
   sphere on black. Ported here rather than into chrome.css, which is generated
   and regenerating it off the current pin would only put the gradient back.

   Straight from that source (its chrome.css .help-orb): black plate, the sphere
   masked at 64% of the circle so it sits with a margin rather than filling the
   edge, and the two shadows — an inset hairline that keeps the black disc from
   disappearing into the near-black panel behind it, and the existing accent
   glow. --accent-fg is upstream's name for the lime this sheet calls --accent,
   so the glyph is the same colour there and here.

   The mask image is the one thing that could not be referenced in place: it
   lives at uploads/xbox-sphere.png upstream, which the pinned submodule does
   not have, so the asset is vendored to media/ alongside the page's own art.
   Both properties are declared — Safari still needs the -webkit- prefix. */
.help-orb{
  position:relative;
  background:#000;
  box-shadow:
    inset 0 0 0 1px rgba(255,255,255,.10),
    0 0 12px -2px var(--accent-glow);
}
.help-orb::before{
  content:""; position:absolute; inset:0;
  background:var(--accent);
  -webkit-mask:url(https://cms-assets.xboxservices.com/assets/41/6e/416ea8ba-6645-4583-9513-789866fc6e2f.png?n=xbox-sphere.png) center/64% no-repeat;
          mask:url(https://cms-assets.xboxservices.com/assets/41/6e/416ea8ba-6645-4583-9513-789866fc6e2f.png?n=xbox-sphere.png) center/64% no-repeat;
}

/* ---------- tweaks panel ---------- */
/* Ported from dotcom-refresh (its index.html:432-453), which keeps this block
   inline. It lives here rather than in chrome.css because the generated sheet is
   sliced from fixed line ranges in the submodule and the tweaks panel is not one
   of them — see tools/build-chrome-css.js.

   Every colour is a token, and the panel sits outside <main>, so it resolves the
   dark values in both themes. That is deliberate and matches the chrome: the
   panel is a tool for looking at the page, not part of it, and a surface that
   flipped with the theme would be one more thing changing in a shot that exists
   to compare themes. */
.tweaks{
  position:fixed; right:18px; bottom:18px; z-index:200; width:300px;
  background:var(--paper-2); border:1px solid var(--rule); border-radius:16px;
  box-shadow:0 28px 70px -20px rgba(0,0,0,.85);
  overflow:hidden; font-family:var(--sans);
}
/* [hidden] rather than the vendor's .on class: the panel is closed in the markup
   and the attribute keeps it out of the accessible tree while it is, which the
   display:none default of [hidden] alone would not communicate as clearly. */
.tweaks[hidden]{ display:none; }
.tweaks .tk-head{
  display:flex; align-items:center; justify-content:space-between;
  padding:14px 16px; border-bottom:1px solid var(--rule-soft);
}
.tweaks .tk-head b{ font-size:13px; font-weight:700; letter-spacing:.01em; }
.tweaks .tk-head .x{
  width:26px; height:26px; border-radius:7px; display:grid; place-items:center;
  background:transparent; border:0; color:var(--ink-3); cursor:pointer;
}
.tweaks .tk-head .x:hover{ background:var(--paper-3); color:var(--ink); }
.tweaks .tk-body{ padding:16px; }
.tweaks .tk-label{
  font-family:var(--mono); font-size:10px; letter-spacing:.1em;
  text-transform:uppercase; color:var(--ink-3); margin-bottom:11px;
}
/* Fixed-width columns rather than a fraction split: with only two presets left,
   1fr columns would stretch each swatch to half the panel. auto-fill also means
   the size holds if the preset list ever changes again. */
.tweaks .sw-grid{ display:grid; grid-template-columns:repeat(auto-fill, 46px); gap:9px; margin-bottom:18px; }
.tweaks .sw{
  position:relative; aspect-ratio:1; border-radius:9px;
  border:1px solid rgba(255,255,255,.12); cursor:pointer; padding:0;
  transition:transform .14s;
}
.tweaks .sw:hover{ transform:translateY(-2px); }
.tweaks .sw.on{ box-shadow:0 0 0 2px var(--paper-2), 0 0 0 3.5px #fff; }
.tweaks .tk-foot{ display:flex; align-items:center; gap:9px; padding:0 16px 16px; }
.tweaks .chip-prev{
  display:flex; align-items:center; gap:8px; font-family:var(--mono); font-size:10px;
  letter-spacing:.08em; text-transform:uppercase; color:var(--ink-3);
}
.tweaks .chip-prev i{ width:14px; height:14px; border-radius:50%; background:var(--accent); display:inline-block; }
.tweaks .reset{ margin-left:auto; font-size:12px; font-weight:600; color:var(--ink-3); background:none; border:0; cursor:pointer; }
.tweaks .reset:hover{ color:var(--accent); }

/* The theme toggle, the one control this panel adds. A segmented pair rather
   than a switch: two named states, both visible, so which one is active is read
   off the control rather than inferred from which way a knob is thrown. */
.tweaks .tk-seg{
  display:grid; grid-template-columns:1fr 1fr; gap:4px; margin-bottom:18px;
  padding:4px; border-radius:10px; background:var(--paper-3);
  border:1px solid var(--rule-soft);
}
.tweaks .tk-seg button{
  padding:7px 0; border:0; border-radius:7px; background:transparent;
  font-family:var(--sans); font-size:12.5px; font-weight:600; color:var(--ink-3);
  cursor:pointer; transition:background .14s ease, color .14s ease;
}
.tweaks .tk-seg button:hover{ color:var(--ink); }
.tweaks .tk-seg button[aria-pressed="true"]{ background:var(--paper); color:var(--ink); }

/* ---------- light theme ---------- */
/* The chrome — mockup notice, countdown bar, header, sub-bar and footer — stays
   dark in both themes, exactly as dotcom-refresh does. Only the page body
   flips, so the token overrides are scoped to <main> and inherit from there. */
html.light main, html.light div[role="main"]{
  --paper:     #e7e8ea;
  --paper-2:   #ffffff;
  --paper-3:   #f3f4f6;
  --rule:      #d5d6db;
  --rule-soft: #e3e4e9;

  --ink:   #161718;
  --ink-2: #4b4d51;
  --ink-3: #6b6d72;
  --ink-4: #93959b;

  /* lime stays the fill colour (buttons); accent *text* darkens
     so it clears WCAG AA against white cards and the light page background.
     #1a6a00 is the same green dotcom-refresh uses for its light focus ring.
     Surfaces that are themselves dark remap this back to lime locally — see
     .panel-copy. */
  --accent-text: #1a6a00;
  --accent-tint: rgba(26,106,0,.11);
  --accent-glow: rgba(26,106,0,.30);

  /* The brighter, vivid end of the light theme's green ramp, for type large
     enough not to need --accent-text's 4.5:1. The step numerals were the
     other reader of this — they are dark glyphs on an acid plate now — so the
     hero's eyebrow is what it serves today. Set to clear 3.5:1 on white with
     headroom: at a fixed contrast the luminance is pinned, so the only freedom
     left is hue and saturation, and this is the fully saturated colour at the
     lime accent's own hue (87deg) that lands there. */
  --lime-ink: #549900;
  --olive-ink: #407500;

  background-color:var(--paper);
  color:var(--ink);
}
/* the shared design system pins these to the lime --accent */
html.light main .kicker.green, html.light div[role="main"] .kicker.green{ color:var(--accent-text); }
html.light main .badge, html.light div[role="main"] .badge{
  background:var(--accent-tint); color:var(--accent-text);
  border-color:color-mix(in oklab, var(--accent-text) 30%, transparent);
}
/* softer elevation — the dark theme's near-opaque shadow is far too heavy here */
html.light main .lift:hover, html.light div[role="main"] .lift:hover{ box-shadow:0 18px 38px -24px rgba(20,22,26,.45); }
/* The three-up cards lift on focus as well as hover, so the softened shadow has
   to reach that state too. Without this the two look different: focus keeps the
   vendor's near-opaque dark-theme shadow while hover gets this one, which is
   the drift this override exists to correct in the first place.

   Both selector forms, because the focus lift itself is declared twice -- once
   plain and once at a specificity that beats the reveal rule -- and the taller
   of those, at (0,6,0), out-ranks a single `html.light main .opt-row-3 ...` at
   (0,5,2). .has-reveal sits on <html> alongside .light, so the pair can be
   written on one compound and this lands at (0,7,2). Measured rather than
   reasoned: the first attempt here lost, and focus kept painting the dark
   shadow while hover painted the light one. */
html.light main .opt-row-3 .card.lift:focus-within,
html.light.has-reveal main .opt-row-3 .card.lift[data-reveal]:focus-within{
  box-shadow:0 18px 38px -24px rgba(20,22,26,.45);
}

/* The button needs the opposite correction to .lift above: the hover halo is
   lime, chosen against the dark page, and on a near-white field it is close to
   invisible. Re-cast in neutral ink it reads as elevation here. The lift and
   the scale come from .btn:hover and are left alone — only the halo is
   surface-dependent. The resting shadow stays green so the button keeps its
   accent at rest, and the sub-bar button is outside <main>, so it keeps the
   lime halo that does read against its dark bar. */
html.light main .btn:hover, html.light div[role="main"] .btn:hover{
  box-shadow:0 14px 26px -12px rgba(20,22,26,.55),
             0 4px 9px -5px rgba(20,22,26,.32);
}

/* ---------- responsive ---------- */
/* "What you can do" drops columns on its own schedule, ahead of everything else
   on the page. These tiles carry artwork rather than just text, so a column
   that merely still fits is not good enough — the art stops reading well before
   the words do, and the fix for a cramped tile is fewer of them, not smaller
   ones.

   The two breakpoints are derived from one number: hold every tile at roughly
   300px or wider. Four across inside 72px gutters and 24px gaps needs
   4*300 + 144 + 72, so the fourth column goes at 1360px; two across inside the
   40px gutters in force by then needs 2*300 + 80 + 24, so the second goes at
   760px. Both are well above where the tiles actually break, which is the
   point. */

/* The ratio is kept rather than swapped for a height: at two across the column
   runs from roughly 620px down to 340px, and any flat height across that span
   turns into a square or landscape tile at the wide end. These tiles are
   portrait by design — the copy sits at the top and the art claims the foot —
   so the shape has to hold. The ratio is eased off 3/4.4 to 1/1.2 because a
   620px column at the original would be over 900px tall; 1/1.2 stays clearly
   upright without running off the screen.

   Unlike a flat min-height this needs no min-width pairing, per the note on the
   base rule — a ratio alone implies no minimum width. */
@media (max-width:1360px){
  .grid.c4{ grid-template-columns:repeat(2, 1fr); }
  .grid.c4 .card{ aspect-ratio:1/1.5; }

  /* The overlay holds at every width — the image still runs all the way across.
     What changes is where the copy sits: side-by-side with a portrait-ish plate
     gets cramped, so the copy drops to the foot of the image and the scrim turns
     vertical to match, which is where the contrast is now needed. */
  /* width:100% is load-bearing. With an automatic inline size, the max-height
     cap transfers back through aspect-ratio and shrinks the *width*: at 768x900
     the 738px cap pulled the panel to 590px, leaving the rest of the column
     empty. That predates the tile treatment — it was just less visible when the
     panels ran to the viewport edge. A definite width blocks the transfer, so
     the ratio only ever drives the height and max-height simply clamps it. */
  .panel{
    min-height:0;
    width:100%;
    aspect-ratio:4/5;
    max-height:82vh;
    grid-template-areas:none;
    grid-template-rows:2fr 1fr;
  }
  /* The art is pinned to the panel rather than left as a grid item. As an item
     it is sized by the grid row, and the row only ever covers the ratio's
     height — so the moment the copy pushes the panel past that (three lines of
     heading on a phone does it), the extra is row the media never reaches and
     the black plate shows through beneath the picture. Positioned against the
     panel it covers the full box however tall the copy makes it.
     .panel-copy keeps z-index:2 and the scrim z-index:1, so taking the media
     out of flow does not change what paints over what. */
  .panel-media{ position:absolute; inset:0; }
  /* Centred here instead of dropped. The 4% slide exists to sit the subject low
     in a wide frame; in a portrait frame the copy has moved to the foot, so the
     subject wants the middle of the panel, not the bottom of it. Scale goes up
     a little to match — a narrow frame crops far more off the sides, and the
     subject reads small without it. */
  .panel{ --media-scale:1.2; --media-y:0; }
  .panel-deck{ padding:0 40px; }
  .pin-panels .panel-deck{ --deck-w0:calc(min(var(--maxw), var(--vw, 100vw)) - 80px); }
  /* Portrait tile, so the copy stops being a column beside the art and becomes
     a centred caption beginning at the top of the panel's bottom third. The
     second grid row can grow beyond one third when narrow-screen copy needs
     more room, keeping the text inside the panel instead of clipping it. The
     scrim turns with it: 180deg so it weights the bottom, and --scrim-span
     drops out of the gradient here because the per-panel values are tuned as
     horizontal reach past each photo's subject and mean nothing on this axis. */
  .panel-copy{
    grid-column:1;
    grid-row:2;
    align-self:start;
    justify-self:stretch;
    max-width:none;
    text-align:center;
    padding:0 clamp(24px, 4vw, 44px) 32px;
  }
  .panel-copy h2, .panel-copy p{ margin-inline:auto; }
  .panel::after{
    background:linear-gradient(180deg,
      color-mix(in srgb, var(--panel-scrim) 14%, transparent) 0%,
      color-mix(in srgb, var(--panel-scrim) 44%, transparent) 40%,
      color-mix(in srgb, var(--panel-scrim) 88%, transparent) 78%,
      color-mix(in srgb, var(--panel-scrim) 92%, transparent) 100%);
  }
}
@media (max-width:760px){
  /* One up, and the ratio carries straight through from the rule above. The
     column is the full page here, so 1/1.2 is the shape of the page itself
     rather than a crop of it — which is the point: these are portrait tiles at
     every column count, and a full-width one that sets its own height from the
     copy comes out square or wider and stops reading as part of the same set.

     Because the ratio never drops, the tiles keep an empty foot at every width,
     which is what the absolutely placed go disc, the cast tile's art and the
     sync tile's bottom bleed are all calibrated against. Those three had
     overrides down here for the no-ratio case; they are gone now. */
  .grid.c4{ grid-template-columns:1fr; }
}

/* The sub-bar collapses its four section links into a dropdown hung off the
   title once the row runs out of width. The trigger query is two-part: any
   genuinely narrow window, plus any portrait viewport up to tablet size, where
   the row would technically still fit but the bar reads as a cramped list
   rather than navigation.

   Nothing is swapped in the markup — the title is already a <button>, so this
   only has to give it back the affordances the wide layout suppressed and
   lift the link row out of flow. */
@media (max-width:900px), (orientation:portrait) and (max-width:1024px){
  .subnav-brand{ pointer-events:auto; cursor:pointer; }
  .subnav-brand .chev{ display:block; transition:transform .18s ease; }
  .subnav-links.open .chev{ transform:rotate(180deg); }
  .subnav-menu{
    position:absolute; top:calc(100% + 12px); left:0; z-index:60;
    display:none; flex-direction:column; align-items:stretch; gap:2px;
    min-width:236px; padding:8px;
    border:1px solid var(--rule-soft); border-radius:14px;
    background:rgba(10,10,12,.97);
    box-shadow:0 22px 48px -20px rgba(0,0,0,.85);
  }
  .subnav-links.open .subnav-menu{ display:flex; }
  .subnav-menu a{ padding:10px 12px; border-radius:10px; }
  .subnav-menu a:hover{ background:rgba(255,255,255,.07); }
}

/* Gutters are reduced on narrow viewports for every .wrap — header, main and
   footer alike — so the chrome and the page content stay on the same rails. */
@media (max-width:1280px){
  .wrap{ padding:0 40px; }
  /* .panel-deck rolls .wrap's gutters by hand (see the pinned sequence above),
     so it has to track them here too. --deck-w0 follows automatically. */
  .subnav .row2{ padding-left:40px; padding-right:40px; }
  /* The shared header row is the one rail chrome.css leaves at a flat 72px at
     every width, so it has to be stepped down by hand alongside the sub-bar it
     sits directly on top of. */
  .hdr .row1{ padding-left:40px; padding-right:40px; }
}
@media (width < 760px){
  .sec.major.sec-backdrop{
    aspect-ratio:19/35;
    min-height:640px;
  }
  .sec-backdrop .opt-row{ grid-template-columns:minmax(0, 1fr); }
  .sec-backdrop .opt{ min-width:0; }
  .sec-backdrop > .wrap{ padding-bottom:24px; }
  .sec-backdrop .sec-title{ margin-bottom:18px; }
  .sec-backdrop .opt-body{ padding:24px; }
}

@media (max-width:900px){
  .sec-backdrop .cta-row:has(.tlink-green + .tlink-green){
    display:grid;
    justify-items:center;
    gap:8px;
  }
  .sec-backdrop .cta-row:has(.tlink-green + .tlink-green) .tlink-green{
    margin-right:0;
  }
}

@media (760px <= width < 980px){
  .sec.major.sec-backdrop{
    aspect-ratio:49/40;
    min-height:620px;
  }
  .sec-backdrop > .wrap{ padding-bottom:clamp(34px, 6vw, 58px); }
}

@media (max-width:980px){
  .grid.c3, .grid.c2{ grid-template-columns:1fr; }
  .grid.opt-row.opt-row-3{ grid-template-columns:1fr; }
  /* Three columns hold up to about 980px, where each step drops near 260px.
     That threshold was set when step one carried a device list that started
     breaking every few words; the copy is gone, but a third of the row is still
     too little for a title beside a 56px numeral. One column from here down.

     Each step keeps its own numeral-plus-title gutter at every width — that is
     internal to the li, not to this row.

     The inline inset exists because the row's 900px cap stops doing any work
     here. Above 980 the cap is narrower than the wrap, so the row is centred
     with real space either side — 201px at 1280. Below it the cap never binds,
     the row fills the wrap exactly, and the numerals land hard on the wrap's
     own 20px. The head above looks inset at these widths, but its box is the
     same box: it is centred text, so only its ink is pulled in. Left-aligned
     content on the same measure has nothing pulling it in, which is why the
     two read so differently once the columns collapse.

     So the inset is given rather than inherited. It ramps with the viewport
     because the wrap's padding does, and it is capped at 40px so the step edge
     does not overshoot the 40px the three-column layout sits at just above the
     breakpoint — crossing 980 already changes the column count, and a large
     jump in the same move would read as the block sliding rather than
     reflowing. */
  .steps-row{
    grid-template-columns:1fr; gap:26px;
    padding-inline:clamp(24px, 7vw, 40px);
  }
}
/* Hold the steps to one measure on portrait phones, so all three titles wrap
   to two lines and the list reads as three blocks of equal weight.

   Same query the device panels' <picture> sources use, so the page has one
   definition of "portrait mobile" rather than two that could drift apart.

   It has to sit after the max-width:980px block rather than inside it: both
   rules are a single .steps-row class, so specificity ties and source order is
   what decides. Placed above, this would silently lose.

   A measure cap rather than more padding, which is what this started as. Line
   breaking depends on the column width and nothing else, so the widths at
   which all three titles take exactly two lines are the same 204–256px at
   every viewport — swept and confirmed from 360 to 900. Expressed as padding
   that same window runs from 8.9vw at 360px to 31.3vw at 900px, because the
   padding has to absorb all the width the measure is not taking. No ramp
   describes that; the constant was never the inset, it was the measure.

   230px is the middle of the window, leaving about 26px of slack at either end
   for font fallback and rounding. The window is bounded above by the shortest
   title, which has to stay too wide for one line, and below by the longest,
   which must not spill to three — so the middle is the only defensible pick.

   padding-inline goes back to 0 deliberately. box-sizing is border-box, so a
   cap and a padding would both apply and the measure would land at 230 minus
   twice the padding, which is outside the window. The auto margins on the base
   rule do the centring, and the inset is now whatever is left over. */
@media (orientation:portrait) and (max-width:900px){
  .steps-row{ padding-inline:0; max-inline-size:230px; }
}
@media (max-width:640px){
  .wrap{ padding:0 20px; }
  .panel-deck{ padding:0 20px; }
  .pin-panels .panel-deck{ --deck-w0:calc(min(var(--maxw), var(--vw, 100vw)) - 40px); }
  .subnav .row2{ padding-left:20px; padding-right:20px; }
  .hdr .row1{ padding-left:20px; padding-right:20px; }
}
