/* ============================================================================
   GreyStream marketing site.

   Hand-written, no build step, no framework - the same decision portal/styles.css
   records and for the same reasons: it deploys to Hostinger Premium as plain static
   files with no toolchain in the path, and a utility framework would mean the design
   system existed in two forms that had to be kept in sync by hand.

   Everything below is built on tokens.css, which is copied verbatim from the portal.
   ========================================================================== */

/* ------------------------------------------------------------------ typeface
   POPPINS, SELF-HOSTED. It used to be three <link> tags to Google on every page, and it
   never once loaded: this site's CSP is `default-src 'none'` with `style-src 'self'` and
   `font-src 'self'`, so the stylesheet request was refused before it was made, and the
   font files behind it would have been refused too. Every page rendered in system-ui
   while the product it sells renders in Poppins - the exact mismatch site-test.cjs was
   written to catch, and missed, because it asserted the <link> was PRESENT rather than
   that the font ARRIVED.

   Serving the files ourselves fixes that without touching the CSP, which is the right
   direction for this site: `font-src 'self'` is a stronger statement than any allow-list
   of Google hostnames, and it stays true without maintenance.

   It also closes a privacy hole that outlived the font. CSP governs fetches, not resource
   hints, so the two <link rel="preconnect"> tags were never blocked - every page view
   still opened DNS, TCP and TLS to fonts.googleapis.com and fonts.gstatic.com and handed
   Google the visitor's IP, in exchange for a font that never arrived. The privacy policy
   is a selling point here; this makes it true of the website too.

   THREE WEIGHTS, NOT FIVE. The old URL asked for 300;400;500;600;700. Nothing has ever
   used 300 or 500.

   NO DEVANAGARI SUBSET. Google serves one for Poppins and this site has no use for it.
   latin-ext IS kept: the planned translations include Turkish, whose dotless i and
   cedillas live there. Arabic will need a different family entirely - the TV app uses
   Cairo - not another Poppins subset.

   Unmodified Google builds of Poppins v24, under the SIL Open Font License 1.1.
   assets/fonts/OFL.txt is the licence and has to stay alongside them. To refresh, fetch
   https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700 with a browser
   User-Agent and pull the woff2 URLs out of it. Google's own filenames are hashes, so
   they are renamed here by weight and subset to stay readable.
   ========================================================================== */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 400;
  /* swap, not block: the fallback is a system sans and the page is readable in it
     at once. A FOIT would trade a visible page for a blank one to hide a reflow
     this layout barely shows. */
  font-display: swap;
  src: url('fonts/poppins-regular-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/poppins-regular-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('fonts/poppins-semibold-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('fonts/poppins-semibold-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('fonts/poppins-bold-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('fonts/poppins-bold-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---------------------------------------------------------- site-local tokens
   Three values the portal block does not carry, because the portal has none of the
   things that need them: text sitting ON the amber tint, pages of long-form prose, and
   a filled blue button.

   They are declared HERE, in one block, rather than written inline further down. A hex
   buried in a rule is a value that will not move when the portal retints, and two
   surfaces drift apart one forgotten literal at a time - so site-test.cjs fails on any
   hex found outside this block. Adding a colour therefore means adding it here, where
   the next person re-copying tokens.css will see what has to be re-derived.

   Each is a sibling of an existing token, mixed the same way the portal mixes its own:
   a light tint of the accent, at roughly the same distance from it as --ok-tx is from
   --ok and --sel-tx is from --sel. */
:root {
  --warn-tx:   #ffe6bd;  /* on --warn: the "Launch price" pill */
  --text-head: #b6cde2;  /* prose subheadings: --text cooled toward --sel so they sit
                            above body copy without going full white and shouting */
  --on-sel:    #05121f;  /* ink on a filled --sel button - near-black, not pure, so the
                            contrast is high without the hard edge white-on-blue gives */
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: 'Poppins', system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
  font-size: 17px;
  line-height: 1.65;
  /* The wallpaper is fixed behind everything and the page scrolls over it, so the hero
     image does not scroll away and leave a bare ground colour behind the content. */
  overflow-x: hidden;
}

img { max-width: 100%; height: auto; display: block; }
a { color: var(--sel); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ---------------------------------------------------------------- wallpaper
   DESKTOP ONLY, and deliberately.

   A 4K webp is a real download and a Ken Burns transform is a continuously composited
   layer. On a phone that is battery and data spent on something largely hidden behind
   content anyway, so the image is only fetched above 900px - `background-image` inside a
   media query is not requested at all when the query does not match, which a plain <img>
   would not give us.

   `prefers-reduced-motion` stops the drift outright. The animation is slow and decorative,
   which is exactly the kind a vestibular trigger does not need.
   ------------------------------------------------------------------------ */
.wall {
  position: fixed;
  inset: 0;
  z-index: -2;
  background: var(--bg);
}
.wall::before { content: none; }

@media (min-width: 900px) {
  .wall::before {
    content: '';
    position: absolute;
    /* Oversized so the slow scale/pan never exposes an edge. */
    inset: -6%;
    /* bg-hero.webp, NOT the 4K master. The master is 3840x2160 and 1.8MB; this layer is
       translucent, behind a wash, and behind content, so nothing above 2048px wide survives
       to be seen. Regenerate with:
         ffmpeg -i bg-full-05.webp -vf scale=2048:-2 -c:v libwebp -quality 76 bg-hero.webp */
    background-image: url('bg-hero.webp');
    background-size: cover;
    background-position: center 40%;
    /* Two layers dim this image - this opacity and the wash in ::after - and they MULTIPLY.
       At 0.30 under a 0.72 wash only about 8% of the picture reached the eye, which is why
       it read as noise rather than a photograph. */
    opacity: 0.78;
    transform-origin: 55% 45%;
    animation: kenburns 48s ease-in-out infinite alternate;
    will-change: transform;
  }
}

@keyframes kenburns {
  from { transform: scale(1.00) translate3d(0, 0, 0); }
  to   { transform: scale(1.12) translate3d(-1.5%, -1.2%, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .wall::before { animation: none; }
}

/* A vertical wash so type stays readable wherever the image happens to be bright.
   It deepens down the page on purpose: the hero is short, centred and set in large type that
   can hold its own over a picture, while everything below it is long-form body copy at
   --text-dim, which cannot. So the photograph is allowed to show through at the top, where
   it is doing the work of a hero image, and is progressively put away further down. */
.wall::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(180deg, rgba(11,15,20,0.26) 0%, rgba(11,15,20,0.56) 45%, rgba(11,15,20,0.86) 100%);
}

/* A second, LEFT-WEIGHTED scrim, on top of the vertical wash.

   Section headings and their intro lines sit directly on the photograph - they are not inside
   a card, so nothing protects them - and measured against the rendered pixels they were coming
   in at 3.0:1 to 4.5:1 over the lit parts of the image. The obvious fix is to dim the whole
   wallpaper again, but that undoes the thing it is here to do.

   Text on this page is left-aligned inside a 1120px column, and the subject of the photograph
   is on the right. So the protection is put where the words are and faded out before it
   reaches the picture: strong down the left, gone by about two-thirds across. The wallpaper
   ends up MORE visible than a uniform dimming would leave it, and the type is legible.

   Fixed rather than absolute so it travels with the fixed wallpaper instead of scrolling off
   it, and z-index -1 keeps it above .wall (-2) and below everything else. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(96deg,
    rgba(11,15,20,0.80) 0%, rgba(11,15,20,0.66) 34%, rgba(11,15,20,0.28) 56%, rgba(11,15,20,0) 72%);
}

/* ------------------------------------------------------------------ layout
   The gutter breathes with the viewport instead of sitting at a fixed 20px. Text running
   almost to the edge of a phone is the single most common way a desktop-first layout betrays
   itself when it is shrunk, and it is uncomfortable to read besides - the eye needs somewhere
   to land at the end of a line.

   clamp gives 18px on the narrowest phone (where every pixel of measure is worth having),
   grows through the tablet range where there is room to spare, and stops at 32px so the
   1120px content column is never pushed around on a desktop. */
.wrap { width: 100%; max-width: 1120px; margin: 0 auto; padding: 0 clamp(18px, 4.5vw, 32px); }

/* Vertical rhythm has to come down with it. 56px of air between sections reads as generous on
   a desktop and as scrolling past nothing on a phone, where the same gap is a fifth of the
   screen. */
@media (max-width: 640px) {
  .section { padding-block: 38px; }
  .hero { padding-block: 54px 42px; }
}

/* ---------------------------------------------------------------------- nav */
.nav {
  position: sticky; top: 0; z-index: 20;
  background: rgba(11,15,20,0.82);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line-soft);
}
/* min-height, not height, and wrap always allowed: whatever the breakpoint below is set to,
   the bar grows a second row rather than clipping its last item off the right edge. */
.nav-in { display: flex; align-items: center; gap: 18px; min-height: 64px; flex-wrap: wrap; }
/* NO gap. The brand is an icon followed by one word set in two weights - "GREYSTREAM", not
   "GREY STREAM" - and a flex gap does not know the difference: it spaced the icon from the
   text and the two halves of the word from each other, splitting the name in the logo. The
   icon gets an explicit margin instead, which is the only place a gap was ever wanted. */
.brand { display: flex; align-items: center; gap: 0; font-weight: 600; letter-spacing: 0.4px; color: var(--text); }
.brand:hover { text-decoration: none; }
.brand img { width: 28px; height: 28px; border-radius: 7px; margin-right: 10px; }
.brand span { color: var(--text-dim); font-weight: 400; }
.nav-links { display: flex; gap: 6px; margin-left: auto; align-items: center; }
.nav-links a {
  color: var(--text-dim); padding: 8px 12px; border-radius: var(--radius-sm);
  font-size: 15px; min-height: var(--tap); display: inline-flex; align-items: center;
}
.nav-links a:hover { color: var(--text); background: var(--bg-btn); text-decoration: none; }
.nav-links a[aria-current='page'] { color: var(--text); background: var(--bg-btn); }

/* THE ONE CONTROL IN THE NAV, and the site's only route to the companion. The home page
   spends four steps describing it - "Scan the code with your phone", "it opens the companion
   page already filled in" - and until now linked to it from nowhere at all, which stranded
   the exact person the steps are written for: someone whose phone will not scan the code and
   who wants the documented fallback of typing the ID and PIN into a browser.

   It says "Set up your TV", NOT "Connect TV". A first-time visitor who has not installed
   anything would arrive at a form asking for a device ID and a PIN their television has never
   shown them; the label has to say who the page is for. The companion's own first line agrees
   - "Enter the code shown on your TV to manage its playlists from this device."

   Filled rather than outlined because every other item up here is plain text, so this is the
   only thing in the bar that looks pressable, and that is the whole point. It matches the nav
   links' --tap height so the two align on the same baseline box. */
.nav-cta {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap); padding: 0 16px;
  border-radius: var(--radius-sm);
  background: var(--sel); border: 1px solid var(--sel); color: var(--on-sel);
  font-size: 15px; font-weight: 600; white-space: nowrap;
}
.nav-cta:hover {
  background: var(--sel-dk); border-color: var(--sel-dk); color: var(--sel-tx);
  text-decoration: none;
}

/* The links WRAP onto a second row rather than collapsing into a hamburger or a sideways
   scroller. Six destinations is not enough to justify a menu the visitor has to open, and
   the scroller this replaced had a worse problem: its scrollbar is hidden, so the links
   past the edge were not merely off-screen, they were undiscoverable. Wrapping keeps all
   six visible and one tap away at any width. */
/* 1040px, not 720px. Six links and a brand fit a single row down to about 720px, which is
   what this was set for - and then the "Set up your TV" button was added beside them and
   pushed the row's real requirement to roughly 960px, so between there and 720px the button
   ran off the right edge. The number now has headroom over what the row actually needs
   rather than being the width the OLD row needed. */
@media (max-width: 1040px) {
  /* padding-block, NOT `padding: 10px 0`. The shorthand set the horizontal padding to zero
     and threw away .wrap's gutter, so the brand sat flush against the window edge on every
     narrow screen. This only ever wanted to add vertical room. */
  .nav-in { padding-block: 10px; }
  /* .nav-links takes the full width to wrap, which would push the CTA onto a THIRD
     row below it. Ordering puts it back on the first row beside the brand, where it
     is reachable without scrolling - the row the six links wrap underneath. */
  .brand { order: -1; }
  .nav-cta { order: 0; margin-left: auto; }
  .nav-links { order: 1; margin-left: 0; width: 100%; flex-wrap: wrap; row-gap: 2px; }
  .nav-links a { white-space: nowrap; }
}

/* Landscape phones have little vertical room and enough width for the five product links.
   Keep the header on one 64px row instead of spending a large part of the viewport on two. */
@media (max-width: 1040px) and (orientation: landscape) {
  .nav-in { min-height: 64px; padding-block: 0; flex-wrap: nowrap; gap: 8px; }
  .brand { order: 0; flex: none; font-size: 14px; }
  .brand img { width: 24px; height: 24px; margin-right: 6px; }
  .nav-links { order: 0; width: auto; margin-left: auto; flex-wrap: nowrap; gap: 0; }
  .nav-links a { min-height: 40px; padding-inline: 6px; font-size: 14px; }
  .nav-cta { order: 0; margin-left: 0; min-height: 40px; padding-inline: 10px; font-size: 14px; }
}
@media (max-width: 640px) and (orientation: landscape) {
  .brand b, .brand span { display: none; }
  .nav-links a { padding-inline: 3px; font-size: 13px; }
  .nav-cta { padding-inline: 6px; font-size: 0; }
  .nav-cta::after { content: 'Set up'; font-size: 13px; }
}

/* Below ~420px the five links no longer fit, and because the scrollbar is hidden the strip
   gives no sign that it scrolls - at 390px the last link was clipped by eight pixels, which
   does not read as "there is more", it reads as a bug.

   Tightening the horizontal padding buys back the ~40px needed to fit all five on the
   narrowest phone worth designing for. The strip stays scrollable as the fallback, so adding
   a sixth link later degrades to sideways scrolling rather than breaking the layout. The
   VERTICAL padding is untouched: it is what holds the tap target at --tap, and trading that
   away to fit text would fix an appearance and break a target. */
@media (max-width: 420px) {
  .nav-links { gap: 2px; }
  .nav-links a { padding-left: 8px; padding-right: 8px; }
}

/* --------------------------------------------------------------------- hero
   The hero is the one place where text sits directly over the brightest part of the
   photograph, and letting the picture through cost contrast: measured against the rendered
   pixels, the blue eyebrow fell to 2.9:1 and the lede to 3.8:1 over a lit patch of cloud -
   both under the 4.5:1 floor.

   The fix is a scrim shaped like the text rather than a darker wash over everything. An
   ellipse centred on the hero copy pulls the background down exactly where the words are and
   fades out well before the edges, so the photograph stays visible around it and the page
   keeps the openness that raising the opacity bought. Darkening the whole top of the wash
   instead would have undone that change to fix a problem confined to about 600px of width.

   z-index -1 sits it above .wall (-2) and below the hero's own content.

   THE ELLIPSE WAS RAISED from 64% tall centred at 46% to 72% at 50%, and the fade pushed from
   78% to 82%, because the scrim was shaped around a TWO-LINE lede. features.html runs to three
   lines and its third sat below the strong part of the gradient at 4.35:1 - already under the
   floor before anything was added to it, and marketing-render-test could not report that
   because the suite had been pointing at a directory that no longer existed since the repo
   was consolidated. Vertical only: the note below explains why widening it is not an option.

   The horizontal inset is 0, not negative. Bleeding the scrim sideways past .hero looked
   better in isolation but made the element wider than its container, and an absolutely
   positioned child that overflows still counts toward the document's scroll width - the page
   gained 108px of sideways scroll on every screen above 720px. The ellipse is widened to 82%
   instead, which puts the soft edge in the same place without anything leaving the box. */
.hero { padding-block: 84px 60px; text-align: center; position: relative; }
.hero::before {
  content: '';
  position: absolute;
  inset: -6% 0;
  z-index: -1;
  background: radial-gradient(ellipse 82% 72% at 50% 50%,
    rgba(11,15,20,0.78) 0%, rgba(11,15,20,0.55) 50%, rgba(11,15,20,0) 82%);
  pointer-events: none;
}
.hero h1 {
  margin: 0 0 14px;
  /* clamp() rather than breakpoints: the headline is the one element that has to look
     deliberate at every width, and stepping it at two or three sizes leaves the ones
     between looking like an accident. */
  font-size: clamp(34px, 6vw, 62px);
  line-height: 1.1; font-weight: 600; letter-spacing: -0.5px;
}
.hero .lede {
  margin: 0 auto 26px; max-width: 46ch;
  font-size: clamp(17px, 2.2vw, 21px); color: var(--text-dim);
}
.hero-cta { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }

/* ------------------------------------------------------------- hero product shot
   A flat screenshot on a dark page reads as a screenshot. The frame is what makes it read as
   a TELEVISION, which is the claim the headline above it is making - so the bezel is doing
   work, not decoration. Pure CSS: no device mock-up image to ship, rescale or replace when
   the capture changes.

   Narrower than .wrap's 1120px on purpose. At full width the shot competes with the headline
   for the eye; held in, it sits under it. */
/* The bezel ONLY. The gutter is the .wrap <div> around it in the markup, because a class
   sitting on the same element as .wrap and setting the `padding` shorthand silently wins -
   same specificity, later in the file - and takes the site's horizontal gutter with it.
   That is what put the frame within 10px of the window edge. Keep the two on separate
   elements and neither can eat the other. */
.hero-shot {
  max-width: 900px;
  margin: 0 auto;
  padding: 10px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--bg-panel);
  /* Lifted off the page rather than outlined - a long soft shadow below and a tight dark
     halo to separate the bezel from the wallpaper behind it. */
  box-shadow: 0 28px 60px -24px rgba(0,0,0,0.75), 0 0 0 1px rgba(0,0,0,0.45);
}
/* THE SMALL-SCREEN SLIDESHOW. Twelve captures stacked in one box, cross-fading on a shared
   sixty-second loop with each slide offset five seconds behind the last. The overlap is
   the point: at 4% in and 20% out every slide spends about a second dissolving into the next,
   so there is never a frame of empty box between them.

   ONE ANIMATION, TWELVE DELAYS. Writing separate keyframe tracks would be twelve places for the
   timing to drift; a negative delay starts each slide already partway through the same track,
   which is what keeps them evenly spaced by construction rather than by arithmetic. */
.hero-strip {
  position: relative;
  /* The capture is 16:9 and the box is fluid, so the ratio is declared rather than left to the
     intrinsic size - the reserved box then survives a slow or failed image. */
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.hero-strip img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: var(--radius-sm);
  opacity: 0;
  animation: heroFade 60s linear infinite;
}
/* --- the small-screen strip timing, generated by build-hero-deck.cjs --- */
.hero-strip img { animation-duration: 50s; }
.hero-strip img:nth-child(1) { animation-delay: 0s; }
.hero-strip img:nth-child(2) { animation-delay: -5s; }
.hero-strip img:nth-child(3) { animation-delay: -10s; }
.hero-strip img:nth-child(4) { animation-delay: -15s; }
.hero-strip img:nth-child(5) { animation-delay: -20s; }
.hero-strip img:nth-child(6) { animation-delay: -25s; }
.hero-strip img:nth-child(7) { animation-delay: -30s; }
.hero-strip img:nth-child(8) { animation-delay: -35s; }
.hero-strip img:nth-child(9) { animation-delay: -40s; }
.hero-strip img:nth-child(10) { animation-delay: -45s; }

@keyframes heroFade {
  0%   { opacity: 0; }
  1.8% { opacity: 1; }
  8.16% { opacity: 1; }
  10% { opacity: 0; }
  100% { opacity: 0; }
}


@media (prefers-reduced-motion: reduce) {
  /* A slideshow that changes on its own is exactly the motion this preference is about. The
     first slide stays; the rest are removed rather than left stacked invisibly. */
  .hero-strip img { animation: none; opacity: 0; }
  .hero-strip img:nth-child(1) { opacity: 1; }
}

/* ---------------------------------------------------------------- the 3D deck
   Twelve captures fanned into a perspective. Click anywhere on it and the front one fades
   away while the rest step forward; the one that left returns silently at the back.

   NO JAVASCRIPT, AND NOT AS A COMPROMISE. This site's CSP is `default-src 'none'` with no
   `script-src` at all, so a script would not merely be unusual here, it would be REFUSED -
   and adding `script-src 'self'` to buy a hero effect trades a genuinely strong header for
   decoration.

   THE CLICK STATE IS TWELVE RADIO BUTTONS. Only one is checked, and each state's rules place
   every card in the slot `(card - state) mod 12` - so one click moves the whole stack by one
   position and the arithmetic wraps for free. The rules are GENERATED, not hand-typed:
   thirty-six placements is exactly the kind of mechanical list where one transposed number
   is invisible. The generator is in the commit that added this.

   ONE HIT LAYER PER STATE, each pointing at the next. A label's `for` is static in the
   markup, so "advance by one" cannot be a single element; instead twelve full-deck labels are
   stacked and only the current state's is displayed. Clicking it checks the next radio.

   THE POINTER TILT lives inside those labels: nine transparent zones per hit layer, each
   hovering a different pair of custom properties onto the deck, which inherit down to the
   stage. Stepped rather than continuous, but a 420ms transition rides between the steps.
   They are inside the label on purpose - a click on a child of a label still activates it,
   so hovering and clicking do not have to fight over the same pixels.

   DESKTOP ONLY, on the same reasoning `.wall` records above: six decoded images and a 3D
   layer is battery and data, and on a phone the deck has to collapse to one picture anyway.
   Below 900px this is a single static shot, which is what the hero was before.

   REDUCED MOTION keeps the stack usable but removes the slow autonomous float, rim pulse and
   card travel. A click still rearranges the deck; it simply arrives instead of gliding.
   -------------------------------------------------------------------------- */
.deck { display: none; }
/* One picture or the other, never both: above 900px the deck is the hero shot. */
@media (min-width: 900px) { .hero-shot { display: none; } }

@media (min-width: 900px) {
  .deck {
    display: block;
    position: relative;
    left: 50%;
    width: calc(100vw - 48px);
    max-width: 1180px;
    margin: 0;
    transform: translateX(-50%);
    transform-origin: 50% 48%;
    animation: deckbreathe 16s ease-in-out infinite;
    /* The 3D depth all the cards are placed in. Larger than it looks it should be: at a
       short perspective the receding cards skew hard enough to read as broken, not distant. */
    perspective: 1600px;
    perspective-origin: 50% 45%;
    /* Reserves the box before any image lands, so nothing below shunts down. */
    aspect-ratio: 16 / 8.9;
    /* THE STACK RECEDES UPWARD as well as backward - each slot sits a little higher than the
       one in front - so the far cards were meeting the top of this box while empty space sat
       under the front one. `top` moved down and the box grew taller; the clipping below is
       what makes that visible rather than merely tight.

       CLIPPED, because the stack deliberately runs off both ends: a card waits beyond the
       right edge and the one that leaves parks past the left, and at 1024px those overhangs
       pushed the PAGE wide enough to scroll sideways. Clipping to the deck's own box fixes
       it at every width rather than at the one that was measured. It costs nothing visually
       - the pictures already dissolve at their edges - and it does not flatten the 3D,
       because preserve-3d lives on .deck-stage inside, not here. */
    overflow: hidden;
  }

  /* The state itself. Off screen rather than display:none, because a display:none radio
     cannot be checked by its label in every engine. */
  .deck-state {
    position: absolute;
    opacity: 0;
    pointer-events: none;
    width: 1px;
    height: 1px;
  }

  .deck-stage {
    position: absolute;
    inset: 0;
    transform-style: preserve-3d;
    transform: rotateY(var(--ry, 0deg)) rotateX(var(--rx, 0deg));
    transition: transform 420ms cubic-bezier(.22,.61,.36,1);
    will-change: transform;
  }

  .deck-card {
    position: absolute;
    top: 17%;
    left: 50%;
    width: 58%;
    margin: 0;
    border-radius: var(--radius);
    transform-style: preserve-3d;
    /* The whole travel. Slower than the tilt because this one is a deliberate act rather
       than a reaction, and it should read as weight rather than as a snap. */
    transition: transform 640ms cubic-bezier(.22,.61,.36,1), opacity 640ms ease;
    will-change: transform, opacity;
  }
  /* NO MASK ON THE CARD, and an earlier version had one. `mask-image` masks an element's
     pseudo-elements too, so an ellipse meant to soften the card was erasing ::after - the
     lit rim - precisely at the edges where the rim IS. Replacing it with a radial overlay
     across the whole deck was worse: a gradient ending in an opaque page colour still ends
     at the element's RECTANGLE, so it painted a visible dark box over the wallpaper and
     across the section below. Masking the IMG has neither problem. */
  .deck-card img {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    border-radius: var(--radius);
    filter: saturate(1.02) contrast(1.02);
    /* Gentle: solid across most of the frame and only dissolving at the extreme edge, so the
       screenshot stays legible and only its border melts. rgba rather than #000 because this
       file takes its colours from tokens and keeps raw hex out of rules. */
    -webkit-mask-image: radial-gradient(ellipse 86% 90% at 50% 48%,
                          rgba(0,0,0,1) 72%, rgba(0,0,0,0) 100%);
    mask-image: radial-gradient(ellipse 86% 90% at 50% 48%,
                  rgba(0,0,0,1) 72%, rgba(0,0,0,0) 100%);
  }

  /* ---- the lit edge ----
     A BLOOM, NOT AN OUTLINE. With the picture's own edge dissolving, a crisp rule around it
     would sit in mid-air where the image has already faded out. Breathing on a long cycle,
     each card offset in phase so the deck never pulses in unison - which is what would make
     it read as a blinking light rather than as reflected room light. This is the only thing
     on the deck that moves by itself, and it moves slowly. */
  .deck-card::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: var(--radius);
    pointer-events: none;
    animation: deckglow 11s ease-in-out infinite;
  }
  .deck-card:nth-child(1)::after { animation-delay: 0s;    }
  .deck-card:nth-child(2)::after { animation-delay: -1.6s; }
  .deck-card:nth-child(3)::after { animation-delay: -3.1s; }
  .deck-card:nth-child(4)::after { animation-delay: -4.7s; }
  .deck-card:nth-child(5)::after { animation-delay: -6.3s; }
  .deck-card:nth-child(6)::after { animation-delay: -7.9s; }
  .deck-card:nth-child(7)::after { animation-delay: -9.4s; }
  .deck-card:nth-child(8)::after { animation-delay: -0.8s; }
  .deck-card:nth-child(9)::after { animation-delay: -2.4s; }
  .deck-card:nth-child(10)::after { animation-delay: -4.0s; }
  .deck-card:nth-child(11)::after { animation-delay: -5.5s; }
  .deck-card:nth-child(12)::after { animation-delay: -7.1s; }
  /* Pointing at the deck wakes the rim up. Cheap, and it is the only feedback the effect
     gives that it is reacting to a person rather than to a clock. */
  .deck:hover .deck-card::after { animation-duration: 5s; }

  @keyframes deckglow {
    0%, 100% { box-shadow: 0 0 38px -14px rgba(122,178,255,0.30),
                           0 34px 70px -34px rgba(0,0,0,0.9); }
    50%      { box-shadow: 0 0 62px -6px rgba(122,178,255,0.58),
                           0 34px 70px -34px rgba(0,0,0,0.9); }
  }

  /* A room-scale object should not feel pinned to the page. The whole deck rises by eight
     pixels and grows by less than one per cent over sixteen seconds: visible in peripheral
     vision, calm when watched directly, and independent of the pointer tilt on the stage. */
  @keyframes deckbreathe {
    0%, 100% { transform: translateX(-50%) translateY(0) scale(1); }
    50%      { transform: translateX(-50%) translateY(-8px) scale(1.008); }
  }

  /* --- the carousel click states, generated by build-hero-deck.cjs --- */
  #deck-s0:checked ~ .deck-stage .deck-card:nth-child(1) { transform: translate3d(-64%, 0%, 0px) rotateY(-26deg) scale(1); opacity: 1; z-index: 10; }
  #deck-s0:checked ~ .deck-stage .deck-card:nth-child(2) { transform: translate3d(-36%, -3%, -195px) rotateY(-26deg) scale(0.98); opacity: 0.87; z-index: 9; }
  #deck-s0:checked ~ .deck-stage .deck-card:nth-child(3) { transform: translate3d(-10%, -6%, -380px) rotateY(-26deg) scale(0.96); opacity: 0.62; z-index: 8; }
  #deck-s0:checked ~ .deck-stage .deck-card:nth-child(4) { transform: translate3d(16%, -9%, -560px) rotateY(-26deg) scale(0.94); opacity: 0.38; z-index: 7; }
  #deck-s0:checked ~ .deck-stage .deck-card:nth-child(5) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 6; }
  #deck-s0:checked ~ .deck-stage .deck-card:nth-child(6) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 5; }
  #deck-s0:checked ~ .deck-stage .deck-card:nth-child(7) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 4; }
  #deck-s0:checked ~ .deck-stage .deck-card:nth-child(8) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 3; }
  #deck-s0:checked ~ .deck-stage .deck-card:nth-child(9) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 2; }
  #deck-s0:checked ~ .deck-stage .deck-card:nth-child(10) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 1; }

  #deck-s1:checked ~ .deck-stage .deck-card:nth-child(1) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 1; }
  #deck-s1:checked ~ .deck-stage .deck-card:nth-child(2) { transform: translate3d(-64%, 0%, 0px) rotateY(-26deg) scale(1); opacity: 1; z-index: 10; }
  #deck-s1:checked ~ .deck-stage .deck-card:nth-child(3) { transform: translate3d(-36%, -3%, -195px) rotateY(-26deg) scale(0.98); opacity: 0.87; z-index: 9; }
  #deck-s1:checked ~ .deck-stage .deck-card:nth-child(4) { transform: translate3d(-10%, -6%, -380px) rotateY(-26deg) scale(0.96); opacity: 0.62; z-index: 8; }
  #deck-s1:checked ~ .deck-stage .deck-card:nth-child(5) { transform: translate3d(16%, -9%, -560px) rotateY(-26deg) scale(0.94); opacity: 0.38; z-index: 7; }
  #deck-s1:checked ~ .deck-stage .deck-card:nth-child(6) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 6; }
  #deck-s1:checked ~ .deck-stage .deck-card:nth-child(7) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 5; }
  #deck-s1:checked ~ .deck-stage .deck-card:nth-child(8) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 4; }
  #deck-s1:checked ~ .deck-stage .deck-card:nth-child(9) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 3; }
  #deck-s1:checked ~ .deck-stage .deck-card:nth-child(10) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 2; }

  #deck-s2:checked ~ .deck-stage .deck-card:nth-child(1) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 2; }
  #deck-s2:checked ~ .deck-stage .deck-card:nth-child(2) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 1; }
  #deck-s2:checked ~ .deck-stage .deck-card:nth-child(3) { transform: translate3d(-64%, 0%, 0px) rotateY(-26deg) scale(1); opacity: 1; z-index: 10; }
  #deck-s2:checked ~ .deck-stage .deck-card:nth-child(4) { transform: translate3d(-36%, -3%, -195px) rotateY(-26deg) scale(0.98); opacity: 0.87; z-index: 9; }
  #deck-s2:checked ~ .deck-stage .deck-card:nth-child(5) { transform: translate3d(-10%, -6%, -380px) rotateY(-26deg) scale(0.96); opacity: 0.62; z-index: 8; }
  #deck-s2:checked ~ .deck-stage .deck-card:nth-child(6) { transform: translate3d(16%, -9%, -560px) rotateY(-26deg) scale(0.94); opacity: 0.38; z-index: 7; }
  #deck-s2:checked ~ .deck-stage .deck-card:nth-child(7) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 6; }
  #deck-s2:checked ~ .deck-stage .deck-card:nth-child(8) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 5; }
  #deck-s2:checked ~ .deck-stage .deck-card:nth-child(9) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 4; }
  #deck-s2:checked ~ .deck-stage .deck-card:nth-child(10) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 3; }

  #deck-s3:checked ~ .deck-stage .deck-card:nth-child(1) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 3; }
  #deck-s3:checked ~ .deck-stage .deck-card:nth-child(2) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 2; }
  #deck-s3:checked ~ .deck-stage .deck-card:nth-child(3) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 1; }
  #deck-s3:checked ~ .deck-stage .deck-card:nth-child(4) { transform: translate3d(-64%, 0%, 0px) rotateY(-26deg) scale(1); opacity: 1; z-index: 10; }
  #deck-s3:checked ~ .deck-stage .deck-card:nth-child(5) { transform: translate3d(-36%, -3%, -195px) rotateY(-26deg) scale(0.98); opacity: 0.87; z-index: 9; }
  #deck-s3:checked ~ .deck-stage .deck-card:nth-child(6) { transform: translate3d(-10%, -6%, -380px) rotateY(-26deg) scale(0.96); opacity: 0.62; z-index: 8; }
  #deck-s3:checked ~ .deck-stage .deck-card:nth-child(7) { transform: translate3d(16%, -9%, -560px) rotateY(-26deg) scale(0.94); opacity: 0.38; z-index: 7; }
  #deck-s3:checked ~ .deck-stage .deck-card:nth-child(8) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 6; }
  #deck-s3:checked ~ .deck-stage .deck-card:nth-child(9) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 5; }
  #deck-s3:checked ~ .deck-stage .deck-card:nth-child(10) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 4; }

  #deck-s4:checked ~ .deck-stage .deck-card:nth-child(1) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 4; }
  #deck-s4:checked ~ .deck-stage .deck-card:nth-child(2) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 3; }
  #deck-s4:checked ~ .deck-stage .deck-card:nth-child(3) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 2; }
  #deck-s4:checked ~ .deck-stage .deck-card:nth-child(4) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 1; }
  #deck-s4:checked ~ .deck-stage .deck-card:nth-child(5) { transform: translate3d(-64%, 0%, 0px) rotateY(-26deg) scale(1); opacity: 1; z-index: 10; }
  #deck-s4:checked ~ .deck-stage .deck-card:nth-child(6) { transform: translate3d(-36%, -3%, -195px) rotateY(-26deg) scale(0.98); opacity: 0.87; z-index: 9; }
  #deck-s4:checked ~ .deck-stage .deck-card:nth-child(7) { transform: translate3d(-10%, -6%, -380px) rotateY(-26deg) scale(0.96); opacity: 0.62; z-index: 8; }
  #deck-s4:checked ~ .deck-stage .deck-card:nth-child(8) { transform: translate3d(16%, -9%, -560px) rotateY(-26deg) scale(0.94); opacity: 0.38; z-index: 7; }
  #deck-s4:checked ~ .deck-stage .deck-card:nth-child(9) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 6; }
  #deck-s4:checked ~ .deck-stage .deck-card:nth-child(10) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 5; }

  #deck-s5:checked ~ .deck-stage .deck-card:nth-child(1) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 5; }
  #deck-s5:checked ~ .deck-stage .deck-card:nth-child(2) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 4; }
  #deck-s5:checked ~ .deck-stage .deck-card:nth-child(3) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 3; }
  #deck-s5:checked ~ .deck-stage .deck-card:nth-child(4) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 2; }
  #deck-s5:checked ~ .deck-stage .deck-card:nth-child(5) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 1; }
  #deck-s5:checked ~ .deck-stage .deck-card:nth-child(6) { transform: translate3d(-64%, 0%, 0px) rotateY(-26deg) scale(1); opacity: 1; z-index: 10; }
  #deck-s5:checked ~ .deck-stage .deck-card:nth-child(7) { transform: translate3d(-36%, -3%, -195px) rotateY(-26deg) scale(0.98); opacity: 0.87; z-index: 9; }
  #deck-s5:checked ~ .deck-stage .deck-card:nth-child(8) { transform: translate3d(-10%, -6%, -380px) rotateY(-26deg) scale(0.96); opacity: 0.62; z-index: 8; }
  #deck-s5:checked ~ .deck-stage .deck-card:nth-child(9) { transform: translate3d(16%, -9%, -560px) rotateY(-26deg) scale(0.94); opacity: 0.38; z-index: 7; }
  #deck-s5:checked ~ .deck-stage .deck-card:nth-child(10) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 6; }

  #deck-s6:checked ~ .deck-stage .deck-card:nth-child(1) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 6; }
  #deck-s6:checked ~ .deck-stage .deck-card:nth-child(2) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 5; }
  #deck-s6:checked ~ .deck-stage .deck-card:nth-child(3) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 4; }
  #deck-s6:checked ~ .deck-stage .deck-card:nth-child(4) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 3; }
  #deck-s6:checked ~ .deck-stage .deck-card:nth-child(5) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 2; }
  #deck-s6:checked ~ .deck-stage .deck-card:nth-child(6) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 1; }
  #deck-s6:checked ~ .deck-stage .deck-card:nth-child(7) { transform: translate3d(-64%, 0%, 0px) rotateY(-26deg) scale(1); opacity: 1; z-index: 10; }
  #deck-s6:checked ~ .deck-stage .deck-card:nth-child(8) { transform: translate3d(-36%, -3%, -195px) rotateY(-26deg) scale(0.98); opacity: 0.87; z-index: 9; }
  #deck-s6:checked ~ .deck-stage .deck-card:nth-child(9) { transform: translate3d(-10%, -6%, -380px) rotateY(-26deg) scale(0.96); opacity: 0.62; z-index: 8; }
  #deck-s6:checked ~ .deck-stage .deck-card:nth-child(10) { transform: translate3d(16%, -9%, -560px) rotateY(-26deg) scale(0.94); opacity: 0.38; z-index: 7; }

  #deck-s7:checked ~ .deck-stage .deck-card:nth-child(1) { transform: translate3d(16%, -9%, -560px) rotateY(-26deg) scale(0.94); opacity: 0.38; z-index: 7; }
  #deck-s7:checked ~ .deck-stage .deck-card:nth-child(2) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 6; }
  #deck-s7:checked ~ .deck-stage .deck-card:nth-child(3) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 5; }
  #deck-s7:checked ~ .deck-stage .deck-card:nth-child(4) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 4; }
  #deck-s7:checked ~ .deck-stage .deck-card:nth-child(5) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 3; }
  #deck-s7:checked ~ .deck-stage .deck-card:nth-child(6) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 2; }
  #deck-s7:checked ~ .deck-stage .deck-card:nth-child(7) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 1; }
  #deck-s7:checked ~ .deck-stage .deck-card:nth-child(8) { transform: translate3d(-64%, 0%, 0px) rotateY(-26deg) scale(1); opacity: 1; z-index: 10; }
  #deck-s7:checked ~ .deck-stage .deck-card:nth-child(9) { transform: translate3d(-36%, -3%, -195px) rotateY(-26deg) scale(0.98); opacity: 0.87; z-index: 9; }
  #deck-s7:checked ~ .deck-stage .deck-card:nth-child(10) { transform: translate3d(-10%, -6%, -380px) rotateY(-26deg) scale(0.96); opacity: 0.62; z-index: 8; }

  #deck-s8:checked ~ .deck-stage .deck-card:nth-child(1) { transform: translate3d(-10%, -6%, -380px) rotateY(-26deg) scale(0.96); opacity: 0.62; z-index: 8; }
  #deck-s8:checked ~ .deck-stage .deck-card:nth-child(2) { transform: translate3d(16%, -9%, -560px) rotateY(-26deg) scale(0.94); opacity: 0.38; z-index: 7; }
  #deck-s8:checked ~ .deck-stage .deck-card:nth-child(3) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 6; }
  #deck-s8:checked ~ .deck-stage .deck-card:nth-child(4) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 5; }
  #deck-s8:checked ~ .deck-stage .deck-card:nth-child(5) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 4; }
  #deck-s8:checked ~ .deck-stage .deck-card:nth-child(6) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 3; }
  #deck-s8:checked ~ .deck-stage .deck-card:nth-child(7) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 2; }
  #deck-s8:checked ~ .deck-stage .deck-card:nth-child(8) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 1; }
  #deck-s8:checked ~ .deck-stage .deck-card:nth-child(9) { transform: translate3d(-64%, 0%, 0px) rotateY(-26deg) scale(1); opacity: 1; z-index: 10; }
  #deck-s8:checked ~ .deck-stage .deck-card:nth-child(10) { transform: translate3d(-36%, -3%, -195px) rotateY(-26deg) scale(0.98); opacity: 0.87; z-index: 9; }

  #deck-s9:checked ~ .deck-stage .deck-card:nth-child(1) { transform: translate3d(-36%, -3%, -195px) rotateY(-26deg) scale(0.98); opacity: 0.87; z-index: 9; }
  #deck-s9:checked ~ .deck-stage .deck-card:nth-child(2) { transform: translate3d(-10%, -6%, -380px) rotateY(-26deg) scale(0.96); opacity: 0.62; z-index: 8; }
  #deck-s9:checked ~ .deck-stage .deck-card:nth-child(3) { transform: translate3d(16%, -9%, -560px) rotateY(-26deg) scale(0.94); opacity: 0.38; z-index: 7; }
  #deck-s9:checked ~ .deck-stage .deck-card:nth-child(4) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 6; }
  #deck-s9:checked ~ .deck-stage .deck-card:nth-child(5) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 5; }
  #deck-s9:checked ~ .deck-stage .deck-card:nth-child(6) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 4; }
  #deck-s9:checked ~ .deck-stage .deck-card:nth-child(7) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 3; }
  #deck-s9:checked ~ .deck-stage .deck-card:nth-child(8) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 2; }
  #deck-s9:checked ~ .deck-stage .deck-card:nth-child(9) { transform: translate3d(-84%, 2%, 150px) rotateY(-26deg) scale(1.04); opacity: 0; z-index: 1; }
  #deck-s9:checked ~ .deck-stage .deck-card:nth-child(10) { transform: translate3d(-64%, 0%, 0px) rotateY(-26deg) scale(1); opacity: 1; z-index: 10; }


  /* Only the hit-layer for the CURRENT state accepts the click, and it points at the
     next one. This is what makes a click anywhere on the deck advance it by one. */
  /* --- the click layer per state, generated by build-hero-deck.cjs --- */
  #deck-s0:checked ~ .deck-hits .deck-hit-0 { display: grid; }
  #deck-s1:checked ~ .deck-hits .deck-hit-1 { display: grid; }
  #deck-s2:checked ~ .deck-hits .deck-hit-2 { display: grid; }
  #deck-s3:checked ~ .deck-hits .deck-hit-3 { display: grid; }
  #deck-s4:checked ~ .deck-hits .deck-hit-4 { display: grid; }
  #deck-s5:checked ~ .deck-hits .deck-hit-5 { display: grid; }
  #deck-s6:checked ~ .deck-hits .deck-hit-6 { display: grid; }
  #deck-s7:checked ~ .deck-hits .deck-hit-7 { display: grid; }
  #deck-s8:checked ~ .deck-hits .deck-hit-8 { display: grid; }
  #deck-s9:checked ~ .deck-hits .deck-hit-9 { display: grid; }

  /* ---- the hit layers ---- */
  .deck-hits { position: absolute; inset: 0; z-index: 9; }
  .deck-hit {
    display: none;               /* the current state's is switched to grid below */
    position: absolute;
    inset: 0;
    cursor: pointer;
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: repeat(3, 1fr);
  }
  .deck-hit span { display: block; }

  /* Column drives rotateY, row drives rotateX. :has() reaches UP from the hovered zone, so
     the two axes are set as custom properties on the deck and INHERIT down to the stage -
     which also composes them, so a column and a row cannot overwrite each other. Where
     :has() is missing the properties never change, the stage keeps its 0deg fallbacks, and
     the deck simply does not tilt; the click still works. */
  .deck:has(.deck-hit span:nth-child(3n+1):hover) { --ry: 8deg;  }
  .deck:has(.deck-hit span:nth-child(3n+2):hover) { --ry: 0deg;  }
  .deck:has(.deck-hit span:nth-child(3n+3):hover) { --ry: -8deg; }
  .deck:has(.deck-hit span:nth-child(-n+3):hover) { --rx: -4deg; }
  .deck:has(.deck-hit span:nth-child(n+7):hover)  { --rx: 4deg;  }
}

@media (prefers-reduced-motion: reduce) {
  /* The rim stops breathing and the cards stop gliding. The deck still WORKS - a click
     rearranges it exactly as before, it simply arrives rather than travels. */
  .deck-card { transition: none; }
  .deck-card::after { animation: none; box-shadow: 0 0 44px -12px rgba(122,178,255,0.36),
                                                   0 34px 70px -34px rgba(0,0,0,0.9); }
  .deck-stage { transition: none; }
  .deck { animation: none; transform: translateX(-50%); }
}

@media (max-width: 720px) {
  /* A 10px bezel on a 360px screen is a fifth of the frame. It comes down with everything
     else, and the shot keeps its full width because on a phone it is the only product
     picture above the fold - the wallpaper is desktop-only. */
  .hero-shot { padding: 6px; border-radius: var(--radius-sm); }
  .hero-shot img { border-radius: 6px; }
}

/* ------------------------------------------------------------------ buttons */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: var(--tap); padding: 12px 22px;
  border-radius: var(--radius-sm); border: 1px solid var(--line-input);
  background: var(--bg-btn); color: var(--text);
  font: inherit; font-size: 16px; font-weight: 600; cursor: pointer;
}
.btn:hover { text-decoration: none; border-color: var(--sel); }
.btn-primary { background: var(--sel); border-color: var(--sel); color: var(--on-sel); }
.btn-primary:hover { background: var(--sel-dk); border-color: var(--sel-dk); color: var(--sel-tx); }
.btn-ghost { background: transparent; }

/* -------------------------------------------------------------------- cards
   Same surface as the portal's panels: --bg-panel on a --line border at --radius.
   A visitor who installs the app meets the identical object on their phone. */
/* ------------------------------------------------------------------- glass
   ONE definition, applied to every raised surface on the site. Feature cards, pricing,
   screenshot frames and FAQ rows are all the same material, and writing that material three
   times is how three surfaces end up subtly different after one of them gets tweaked.

   It is the wallpaper that earns the effect: a translucent panel over a flat colour is just a
   lighter flat colour, but over a photograph it separates the card from the page while still
   letting the picture read behind it. The TV app does the same thing with .glass-panel, so
   this is the product's own idiom rather than a web-design flourish.

   Written as color-mix over --bg-panel rather than a fresh rgba() so the panel still tracks
   the portal when it retints. The plain `background` above it is the fallback for anything
   without color-mix - it degrades to exactly the solid panel this replaced, not to nothing.

   backdrop-filter is what makes it glass rather than a stain: without the blur, wallpaper
   detail shows through sharp and text sits on top of visible edges. The blur is heavier than
   the transparency alone would need, precisely so the panel can be this transparent and still
   be something to read against. It costs nothing where there is no wallpaper (mobile),
   because there the backdrop is flat. */
.card, .shot, details.faq {
  background: var(--bg-panel);
  background: color-mix(in srgb, var(--bg-panel) 46%, transparent);
  -webkit-backdrop-filter: blur(20px) saturate(125%);
  backdrop-filter: blur(20px) saturate(125%);
  border: 1px solid var(--line);
  /* A one-pixel inner highlight along the top edge - the cue that reads as "lit from above"
     and stops a translucent panel looking like a hole in the page. */
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.07), 0 10px 30px rgba(0,0,0,0.22);
}

.card {
  border-radius: var(--radius);
  padding: 22px;
}
.card h3 { margin: 0 0 8px; font-size: 19px; font-weight: 600; }
.card p  { margin: 0; color: var(--text-dim); font-size: 16px; }
.card-ic {
  width: 40px; height: 40px; margin-bottom: 14px;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm);
  background: rgba(58,160,255,0.12); border: 1px solid rgba(58,160,255,0.28);
  color: var(--sel);
}
.card-ic svg { width: 21px; height: 21px; }

/* auto-fit rather than fixed columns, so the grid reflows on its own. The 300px minimum keeps
   the homepage's nine feature cards in a balanced 3x3 grid inside the 1120px wrap, then steps
   down naturally to two and one columns as the viewport narrows. */
.grid { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); }
/* The seventh radio feature is the deliberate twist rather than an orphaned card: after two
   balanced rows it closes the story at full width. On one-column screens this changes nothing. */
.card-ambient { grid-column: 1 / -1; }

/* ------------------------------------------------------------------ sections */
.section { padding-block: 56px; }
.section > h2 {
  margin: 0 0 6px; font-size: clamp(24px, 3.4vw, 34px); font-weight: 600; letter-spacing: -0.3px;
}
.section > .sub { margin: 0 0 26px; color: var(--text-dim); max-width: 60ch; }
.eyebrow {
  display: inline-block; margin-bottom: 10px;
  font-size: 13px; font-weight: 700; letter-spacing: 1.4px; text-transform: uppercase;
  color: var(--sel);
}

/* -------------------------------------------------------------------- pills */
.pill {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 12px; border-radius: var(--pill);
  font-size: 13px; font-weight: 700; letter-spacing: 0.4px;
  border: 1px solid var(--hairline); color: var(--text-dim);
  /* An inline-flex box aligns on the BASELINE of its own first line by default, so a 13px
     pill next to a 30px heading sat visibly low - its text baseline lined up with the
     heading's while its box hung below. Centring the box against the heading's mid-height
     is what actually reads as aligned. Only affects inline contexts, so the standalone
     pills elsewhere are untouched. */
  vertical-align: middle;
}
/* The heading itself has to stop pushing the line box around: an h2 line-height of ~1.2 on
   30px type leaves the pill controlling the line box otherwise. */
h1 > .pill, h2 > .pill, h3 > .pill { position: relative; top: -0.06em; }
.pill-ok   { color: var(--ok-tx);     background: rgba(80,210,130,0.14);  border-color: rgba(80,210,130,0.4); }
.pill-sel  { color: var(--sel-tx);    background: rgba(58,160,255,0.14);  border-color: rgba(58,160,255,0.4); }
.pill-warn { color: var(--warn-tx);          background: rgba(240,178,60,0.14);  border-color: rgba(240,178,60,0.4); }

/* -------------------------------------------------------------------- steps */
.steps { counter-reset: step; display: grid; gap: 14px; }
.step { display: flex; gap: 16px; align-items: flex-start; }
.step-n {
  flex: 0 0 auto; width: 34px; height: 34px; border-radius: var(--pill);
  background: var(--sel); color: var(--on-sel);
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 15px;
}
.step h3 { margin: 4px 0 4px; font-size: 18px; font-weight: 600; }
.step p  { margin: 0; color: var(--text-dim); font-size: 16px; }

/* ------------------------------------------------------------------ pricing */
.price-row { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(min(250px, 100%), 1fr)); }
.price { position: relative; }
.price .amount { font-size: 40px; font-weight: 600; letter-spacing: -1px; }
.price .per { color: var(--text-dim); font-size: 15px; }
/* "Regular price", never "was". Nobody has been charged the standard figure yet, and both
   the EU/UK Omnibus Directive and the US FTC require a reference price to be one that was
   genuinely in effect. See the note in portal/config.js - the two must not disagree. */
.price .ref { color: var(--text-mute); font-size: 14px; text-decoration: line-through; }
.price ul { margin: 14px 0 0; padding: 0 0 0 20px; color: var(--text-dim); font-size: 15px; }
.price li { margin-bottom: 6px; }
.price-best { border-color: rgba(58,160,255,0.45); }

/* ---------------------------------------------------------------- faq / prose */
/* Glass comes from the shared rule above; only the shape is local. */
details.faq { border-radius: var(--radius-sm); padding: 0; margin-bottom: 10px; }
details.faq > summary {
  cursor: pointer; list-style: none; padding: 16px 18px;
  font-weight: 600; min-height: var(--tap); display: flex; align-items: center; gap: 10px;
}
details.faq > summary::-webkit-details-marker { display: none; }
details.faq > summary::after {
  content: '+'; margin-left: auto; color: var(--sel); font-size: 20px; line-height: 1;
}
details.faq[open] > summary::after { content: '\2212'; }
details.faq > div { padding: 0 18px 16px; color: var(--text-dim); }

.prose { max-width: 72ch; }
.prose h2 { margin: 32px 0 10px; font-size: 22px; font-weight: 600; }
.prose h3 { margin: 22px 0 8px; font-size: 18px; font-weight: 600; color: var(--text-head); }
.prose p, .prose li { color: var(--text-dim); }
.prose ul { padding-left: 22px; }

/* `.draft` lived here, and went with the banner it styled. The legal pages no longer carry a
   "not yet reviewed" caption - see build-legal.cjs for why, and note that the documents are
   no more reviewed than they were. Recoverable from git if the banner ever returns. */

/* The legal pages are GENERATED from tizen/legal.js, so these two notes are emitted by
   build-legal.cjs rather than typed into the page. See that file for why there is only one
   source of legal text in this repository. */

/* "This is the same document the television shows." Deliberately quiet - it is reassurance
   rather than a warning, and it sits under the effective date without competing with it. */
.legal-same {
  border-left: 2px solid var(--hairline); padding: 2px 0 2px 14px;
  margin: 0 0 22px; font-size: 15px;
}

/* The English-only note, carried verbatim from the app. First thing inside every document,
   so it needs space beneath it before the first heading. */
.prose .legal-note { margin: 0 0 24px; font-size: 15px; color: var(--text-mute); }

/* --------------------------------------------------------------- screenshots */
/* Two columns, not three. These are captures of a television interface, and the whole point
   of showing them is that a visitor can read the guide, the posters and the station list. At
   three across they are 348px wide and the UI text inside them is illegible; at two they are
   532px and it is not. Fewer, larger screenshots beat more, smaller ones.

   min(420px, 100%) rather than a bare 420px, and every auto-fit grid on this page is written
   the same way. A minmax() track floor is a HARD floor: auto-fit will drop to one column, but
   that one column is still 420px wide, so inside a 320px phone the grid runs 100px off the
   side of the screen and the whole page scrolls sideways. min() lets the floor collapse to the
   container once the container is the smaller of the two. It cost this page a real overflow at
   360px and 390px before the render suite caught it. */
.shots { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr)); }
/* Glass comes from the shared rule above; only the shape is local. */
.shot { border-radius: var(--radius); overflow: hidden; }
.shot figcaption { padding: 12px 16px; color: var(--text-dim); font-size: 15px; border-top: 1px solid var(--line-soft); }
/* aspect-ratio as well as width/height on the <img>: the attributes reserve the box before
   the file arrives, so a lazily-loaded gallery does not shunt the page around as it fills in. */
.shot img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; background: var(--bg-row); }

/* -------------------------------------------------------------------- footer */
.footer { border-top: 1px solid var(--line-soft); padding: 34px 0 44px; margin-top: 40px; color: var(--text-mute); font-size: 15px; }
.footer-in { display: flex; flex-wrap: wrap; gap: 18px 34px; }
.footer h4 { margin: 0 0 8px; font-size: 14px; letter-spacing: 0.6px; text-transform: uppercase; color: var(--text-dim); }
.footer a { color: var(--text-dim); display: block; padding: 3px 0; }
.footer .cols { display: flex; flex-wrap: wrap; gap: 34px; margin-left: auto; }
/* Social marks, one row, no handles. All three accounts are @gogreystream, so printing the
   name beside each icon repeated the same word three times and told the reader nothing.
   The row overrides `.footer a { display: block }` for its own children only - inline-flex
   so they sit side by side, and a slightly larger tap target than the 15px mark, because a
   thumb needs more than an icon's worth of surface.
   The marks inherit currentColor, so they take the same hover as every other footer link
   with no second colour to keep in step, and cost no extra request under the site's
   default-src 'none' CSP. */
.footer .socials { display: flex; align-items: center; gap: 6px; margin-top: 2px; }
.footer .socials a { display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0; border-radius: 8px; opacity: 0.8; }
.footer .socials svg { width: 17px; height: 17px; }
.footer .socials a:hover { opacity: 1; background: var(--bg-row); }
.footer .fine { width: 100%; border-top: 1px solid var(--line-soft); padding-top: 16px; margin-top: 6px; }

/* Focus is visible and it is the app's blue. Never removed: this is a site people will
   reach by keyboard, and the TV app's whole navigation model is a focus ring. */
:focus-visible { outline: 2px solid var(--sel); outline-offset: 2px; border-radius: 4px; }

/* Skip link. Off-screen until focused, which is the point: a keyboard user should not have to
   tab through six nav items on every page to reach the content, and a mouse user should never
   see it. Positioned rather than `display:none` because a hidden element cannot take focus. */
.skip {
  position: absolute; left: 12px; top: -60px; z-index: 100;
  padding: 10px 16px; border-radius: var(--radius-sm);
  background: var(--bg-card); color: var(--text-head);
  border: 1px solid var(--line); font-size: 15px;
  transition: top 0.12s ease;
}
.skip:focus { top: 12px; }
