/* ==========================================================================
   app.css — the component set
   --------------------------------------------------------------------------
   ONE set, used by all four screens. Screens differ by DATA (js/data.js),
   never by a duplicated component.

   EVERY SIZE IN HERE IS FROM THE FIGMA FILE. The nodes each block was read
   from are named in its header comment, so a value can be checked rather
   than trusted. Where a number is a decision rather than a measurement it
   says so.

   TOUCH DEVICE. There is not one :hover rule in this file. A phone has no
   hover state, and building one puts behaviour in the prototype that cannot
   exist in the product. Feedback is PRESSED instead — `.is-press`, set by
   js/app.js on pointerdown and cleared as soon as the pointer moves far
   enough to be a scroll rather than a tap.

   ASSET PATHS ARE ROOT-RELATIVE. A relative URL inside CSS url() resolves
   against the STYLESHEET, not the document — `assets/x.jpg` here would be
   fetched from /demos/apollo-companion/css/assets/x.jpg and fail silently.
   Everything is `/components/...`, served from the repo root by serve.py.
   ========================================================================== */

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

html {
  background: var(--stage-ground);
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100vh;
  font-family: var(--type-family);
  font-size: var(--type-body-m-size);
  line-height: var(--type-line-height);
  color: var(--color-on-surface);
  background: var(--stage-ground);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, p, ul, ol, figure { margin: 0; padding: 0; }
ul, ol { list-style: none; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; }
a { color: inherit; text-decoration: none; }
img { display: block; max-width: 100%; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ==========================================================================
   THE STAGE AND THE DEVICE
   --------------------------------------------------------------------------
   The screen is 390 x 844 — the artboard the Figma frames are drawn at — and
   it is shown inside a flat simulation of the handset rather than filling the
   browser. Running a phone layout full bleed on a desktop display reads as
   claustrophobic because nothing around it says how big the thing is.

   The device renders at TRUE SIZE and is scaled to fit the window. The
   wrapper carries the SCALED size so the stage can still centre it — a
   transform on its own leaves the layout box at full height and the centring
   goes wrong. Same approach as the arena-journey prototype.
   ========================================================================== */

.stage {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
}

/* The stage is a flat ground, deliberately. A glow behind the device reads
   as a product surface of its own and starts competing with the screen. */

.device-fit {
  position: relative;
  flex: none;
  width:  calc(var(--frame-width) * var(--s, 1));
  height: calc(var(--frame-height) * var(--s, 1));
}

.device {
  position: absolute;
  top: 0; left: 0;
  width:  var(--frame-width);
  height: var(--frame-height);
  transform: scale(var(--s, 1));
  transform-origin: top left;
}

/* The bezel. Flat — a drawn slab, not a photographic mock-up, because a
   glossy render would start competing with the screen it frames. */
.device__shell {
  position: absolute;
  inset: -12px;
  border-radius: 60px;
  background: linear-gradient(158deg, #3b3d46, #15161b 42%, #2d2f38);
  box-shadow: 0 40px 90px rgba(0, 0, 0, 0.55),
              inset 0 0 0 1px rgba(255, 255, 255, 0.07);
}

/* The screen itself. Everything a screen renders lives inside this, and it
   is the containing block for the tab bar, the composer and the sheet. */
.phone {
  position: absolute;
  inset: 0;
  border-radius: 48px;
  background: var(--color-background);
  overflow: hidden;
  isolation: isolate;
}

/* On a handset the device chrome is not decoration — it is the actual
   hardware — so the frame is dropped and the screen fills the viewport. */
@media (max-width: 460px), (max-height: 560px) {
  .stage { position: static; display: block; }
  .stage::before { display: none; }
  .device-fit, .device {
    position: static;
    width: 100%; height: 100vh;
    transform: none;
  }
  .device__shell { display: none; }
  .phone { border-radius: 0; }
}

/* The one scrolling element. Keeping the scroll inside the frame rather than
   on the document is what lets the tab bar, the composer and the sheet sit
   still while content moves under them. */
.scroll {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.scroll::-webkit-scrollbar { width: 0; height: 0; }

/* Screens that carry a solid top bar rather than a hero start below it. */
.scroll--below { top: calc(var(--statusbar-height) + 56px); }

.scroll__end { height: var(--space-3xl); }
.scroll__end--tabbar   { height: 128px; }
.scroll__end--composer { height: 158px; }

/* ==========================================================================
   THE TOUCH CURSOR
   --------------------------------------------------------------------------
   Carried over from the arena-journey prototype. On a machine with a mouse
   the arrow is hidden and a fingertip is drawn in its place — a soft disc
   that contracts on press. It only exists where a real mouse has moved:
   `has-mouse` is set by js/app.js on the first mouse-type pointer event,
   because a media query is not enough (a touch laptop reports coarse and
   would keep its arrow; a tablet reports fine when a stylus is paired).
   ========================================================================== */

html.has-mouse,
html.has-mouse body,
html.has-mouse a,
html.has-mouse button,
html.has-mouse input,
html.has-mouse [role="link"],
html.has-mouse [tabindex],
html.has-mouse .scroll,
html.has-mouse .rail { cursor: none; }

.touch {
  position: fixed;
  top: 0; left: 0;
  z-index: 95;
  width: 46px; height: 46px;
  margin: -23px 0 0 -23px;
  border-radius: 50%;
  background: radial-gradient(circle at 34% 30%,
              rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.22) 62%, rgba(255, 255, 255, 0.1));
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.35), 0 3px 14px rgba(0, 0, 0, 0.35);
  pointer-events: none;
  opacity: 0;
  display: none;

  /* POSITION IS NOT TRANSITIONED, and this is the whole point of splitting
     `translate` and `scale` out of `transform` rather than composing one
     matrix. While both lived on `transform`, the 130ms transition needed for
     the press also applied to every movement — so the disc re-eased toward
     the pointer on every frame and trailed it by a tenth of a second on an
     ease-in-out curve. It read as a dot being dragged on a piece of elastic.

     Separate properties let the position land instantly (js/app.js writes
     --tx/--ty once per frame on rAF, which is as fast as anything can be)
     while the press still eases. */
  translate: var(--tx, -100px) var(--ty, -100px);
  scale: 1;
  transition: opacity 220ms var(--ease-standard),
              scale 130ms var(--ease-standard);
  will-change: translate, scale;
}
html.has-mouse .touch { display: block; }
.touch.is-on { opacity: 1; }
.touch.is-down {
  scale: 0.72;
  background: radial-gradient(circle at 34% 30%,
              rgba(255, 255, 255, 0.74), rgba(255, 255, 255, 0.42) 62%, rgba(255, 255, 255, 0.2));
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.55), 0 2px 8px rgba(0, 0, 0, 0.4);
}

/* ==========================================================================
   PRESSED
   --------------------------------------------------------------------------
   The only interaction state in the prototype. `.is-press` is set on the
   nearest pressable ancestor on pointerdown and removed on pointerup — or
   the moment the pointer travels more than a few pixels, so dragging a rail
   never leaves a card looking tapped.
   ========================================================================== */

.pressable { transition: transform var(--dur-fast) var(--ease-standard),
                         opacity var(--dur-fast) var(--ease-standard),
                         background-color var(--dur-fast) var(--ease-standard); }
.pressable.is-press { transform: scale(0.965); opacity: 0.85; }

/* A card scales less than a button: at 144 x 216 the same ratio reads as a
   lurch, and the artwork has a border that would visibly detach. */
.card.is-press, .cw.is-press, .tile.is-press, .att.is-press { transform: scale(0.982); opacity: 1; }
.row.is-press { background: var(--color-white-5); transform: none; opacity: 1; }

:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }

/* ==========================================================================
   MEDIA SLOT
   --------------------------------------------------------------------------
       background-image: var(--src), var(--ph);

   The real file layers OVER the placeholder, so a missing file degrades to a
   quiet tonal panel instead of a dead box. --src defaults to `none`, which is
   a legal background layer. Set it inline:

       style="--src:url(/components/britbox/hero-bluelights.jpg)"

   POSITION IS `center`, always. The design centres every image fill; the
   earlier build offset artwork upward to "protect faces" and that is what
   made the heroes look crooked and cropped their subjects out of view.
   ========================================================================== */

.media {
  --src: none;
  --pos: center;
  /* Artwork fades up once the file has actually decoded — js/app.js preloads
     each --src and sets .is-loaded. A slot with no artwork is never armed,
     so the placeholder shows immediately rather than fading in from nothing. */
  transition: opacity var(--dur-media) var(--ease-rise);
  background-image: var(--src), var(--ph);
  background-size: cover, cover;
  background-position: var(--pos), center;
  background-repeat: no-repeat, no-repeat;
  background-color: var(--color-surface);
}

/* NOT a second fade. .rise already brings the block in, and js/app.js holds
   that arrival until the artwork has decoded, so the picture and its
   container land together. An opacity transition here as well meant every
   image faded twice — once as part of its block and once on its own, over a
   different duration. The class stays because app.js uses it to know what it
   is waiting for; it just no longer animates. */
.media.is-waiting { opacity: 1; }

/* Title treatments and brand marks are transparent PNGs, so they suppress
   both the placeholder layer and the ground — otherwise an opaque plate is
   painted around the lettering. */
.art {
  --src: none;
  background-image: var(--src);
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
}

/* ==========================================================================
   STATUS BAR  ·  HOME INDICATOR
   StatusBar 47, Home Indicator 32 (31431:766431). The indicator bar is
   128 x 6 at white@50 (I31431:766439;8502:54250).
   ========================================================================== */

.statusbar {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 8;
  height: var(--statusbar-height);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--space-2xl) 0 28px;
  font-size: var(--type-body-l-size);
  font-weight: var(--type-weight-semi-bold);
  letter-spacing: -0.02em;
  color: #fff;
  pointer-events: none;
}
.statusbar__glyphs { display: flex; align-items: center; gap: 6px; }
.statusbar__glyphs svg { display: block; }

.home-indicator {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 40;
  height: var(--home-indicator);
  display: grid;
  place-items: center;
  pointer-events: none;
}
.home-indicator::after {
  content: "";
  width: 128px;
  height: 6px;
  border-radius: var(--radius-full);
  background: var(--color-white-50);
}

/* ==========================================================================
   APP BAR   I31431:766433;4220:39493
   px 16, py 12. Logo container 120 x 40. Icons container gap 20, icon 24,
   avatar 32 on #28cffb with #334655 lettering.
   ========================================================================== */

.chrome-scrim {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 5;
  height: calc(var(--statusbar-height) + 64px);
  background: var(--color-background);
  opacity: 0;
  /* Sits under the sticky tab strip, over everything else. */
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-standard);
}
.is-scrolled .chrome-scrim { opacity: 1; }

/* THE TOP CHROME IS ALWAYS LEGIBLE, NOT ONLY ONCE SCROLLED.
   The clock, the brand mark, the cast button and the avatar sit ON the
   artwork, and the artwork is whatever the editorial team put there that
   week: Vera is dark under the title safe area and the chrome reads, but a
   bright sky or a pale interior behind the same white icons leaves nothing
   to read. The scrim above only arrives on scroll, which is exactly the
   moment the problem has gone away.

   So a gradient is fixed over the top of every screen that has artwork
   under it — black at the very top where the status bar is, gone by the
   bottom of the app bar. It is a shadow under the chrome, not a bar: there
   is no edge to it, so nothing reads as a panel that is not one, and the
   image is untouched a third of the way down.

   Taller than the chrome it protects, because a ramp that ends ON the thing
   it is protecting ends in a visible line. */
.chrome-shade {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 4;
  height: calc(var(--statusbar-height) + 64px + var(--space-3xl));
  pointer-events: none;
  background: linear-gradient(to bottom,
              rgba(0, 10, 20, 0.72) 0%,
              rgba(0, 10, 20, 0.52) 32%,
              rgba(0, 10, 20, 0.24) 64%,
              rgba(0, 10, 20, 0) 100%);
  /* It has done its job once the ground behind the chrome is solid; fading
     it out with the scrim's arrival stops the two stacking into a band
     darker than either. */
  transition: opacity var(--dur-base) var(--ease-standard);
}
.is-scrolled .chrome-shade { opacity: 0; }

.appbar {
  position: absolute;
  top: var(--statusbar-height);
  left: 0; right: 0;
  z-index: 6;
  height: 64px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-m) var(--frame-gutter);
}

/* The brand mark is an ASSET, not type. The slot is the 120 x 40 the design
   gives it; until the file is dropped in, the placeholder shows and the name
   is carried by the visually-hidden label — no hand-set wordmark stands in
   for a brand mark. See ASSETS.md. */
.brandmark {
  width: 120px;
  height: 40px;
  background-position: left center;
}
.brandmark--empty {
  border: 1px dashed var(--color-surface-variant);
  border-radius: var(--radius-s);
  display: grid;
  place-items: center;
  font-size: var(--type-body-xs-size);
  letter-spacing: 0.08em;
  color: var(--color-surface-variant);
}

.appbar__actions { display: flex; align-items: center; gap: var(--space-xl); }

.avatar {
  width: var(--avatar-xs);
  height: var(--avatar-xs);
  display: grid;
  place-items: center;
  border-radius: var(--radius-full);
  background: var(--color-primary-muted);
  color: var(--color-surface-container);
  font-size: var(--type-body-m-size);
  font-weight: var(--type-weight-semi-bold);
}

.iconbtn {
  position: relative;
  width: var(--icon-s);
  height: var(--icon-s);
  display: grid;
  place-items: center;
  flex: none;
  color: var(--color-on-surface);
}


/* Detail screens: back and cast on a disc, so they stay legible over any
   frame of artwork. */
.navbar__right { display: flex; align-items: center; gap: var(--space-s); }

.navbar {
  position: absolute;
  top: var(--statusbar-height);
  left: 0; right: 0;
  z-index: 6;
  height: 64px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-m) var(--frame-gutter);
}

/* Detail App Bar  I31431:766443;4266:46227 — 32 x 32 on `surface`, not a
   40px blurred overlay disc. */
.disc {
  width: 32px; height: 32px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-full);
  background: var(--color-surface);
  color: var(--color-on-surface);
}

/* The solid header the floor and the companion use instead of a hero. */
.topbar {
  position: absolute;
  top: var(--statusbar-height);
  left: 0; right: 0;
  z-index: 7;
  height: 56px;
  display: flex;
  align-items: center;
  gap: var(--space-m);
  padding: 0 var(--frame-gutter);
  /* Glass rather than a solid bar. The thread runs under it and a solid
     slab across the top of a dark screen reads as a cut, where a blurred
     one reads as depth — and it is the same treatment the tab bar already
     uses, so the two screens agree. */
  background: rgba(0, 24, 43, 0.72);
  -webkit-backdrop-filter: blur(18px) saturate(1.3);
  backdrop-filter: blur(18px) saturate(1.3);
  border-bottom: 1px solid var(--color-hairline);
}
.topbar__back {
  width: 32px; height: 32px;
  display: grid; place-items: center;
  flex: none; margin-left: -6px;
  color: var(--color-on-surface);
}
.topbar__main { flex: 1 1 auto; min-width: 0; }
.topbar__title {
  font-size: var(--type-body-l-size);
  font-weight: var(--type-weight-semi-bold);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.topbar__sub {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--type-body-s-size);
  color: var(--color-on-surface-variant);
  font-variant-numeric: tabular-nums;
}
.topbar__sub .pulse { color: var(--accent-active); }
.topbar__act {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0 var(--space-m);
  border: 1px solid var(--color-hairline);
  border-radius: var(--radius-full);
  background: var(--color-surface);
  color: var(--color-on-surface);
  font-size: var(--type-body-s-size);
  font-weight: var(--type-weight-semi-bold);
}
.topbar__act svg { color: var(--accent-active); }

/* ==========================================================================
   HERO   I31431:766433;4220:37850
   Hero plate 390 x 520. Action Section absolute at the bottom: px 16, pb 24,
   gap 12, centred. Logo aspect 326/108, full width. Meta 14 at white@80 with
   4px round dividers. Tagline 16 at on-surface. Buttons gap 16.
   ========================================================================== */

.hero { position: relative; padding-bottom: var(--space-2xl); }

.hero__plate {
  position: relative;
  height: var(--hero-height);
  overflow: hidden;
}
/* THE FADE IS PAINTED OVER THE IMAGE, NOT MASKED OUT OF IT.
   -------------------------------------------------------------------------
   The frame's masks are transcribed stop for stop in tokens.css and the
   result on screen is identical, but they are applied as a GRADIENT OVER the
   artwork rather than as mask-image on it. The reason is the hero looked
   pixelated and oddly textured at full resolution on a 1440 x 2160 source:

     - a mask-image forces the element onto its own composited layer, and
       inside the device's `transform: scale()` that layer gets rasterised
       once and then scaled, so the photograph is resampled twice;
     - an alpha gradient resolved against a photograph bands, and the banding
       reads as texture in the picture rather than as a fade.

   Painting the same ramp in the page's own background colour on top is one
   paint, no extra layer, no resampling of the bitmap, and the artwork stays
   at the resolution it was shipped at. It is only valid because what sits
   behind the hero IS --color-background; if the hero ever moves onto another
   ground, this has to become a mask again. */
.hero__plate .media {
  position: absolute;
  inset: 0;
}
/* A light wash under the app bar on screens whose hero DOES run to the top
   (home). On a detail page the status bar has its own ground and this only
   has the nav row to cover. */
.hero__plate::before {
  content: "";
  position: absolute;
  z-index: 1;
  left: 0; right: 0; top: 0;
  height: 140px;
  background: linear-gradient(to bottom, rgba(0, 24, 43, 0.55) 0%, rgba(0, 24, 43, 0) 100%);
  pointer-events: none;
}

/* THE RAMP, and it is the whole of what the mask used to do: carry the type
   over the lower half of the artwork and land on the page colour at the
   bottom edge, so the plate ends without an edge. Taller and stronger than
   the wash it replaces, because the mask was also thinning the photograph
   itself down there and a wash alone was leaving the meta line on open sky.
   Transcribed from hero-mask-3x4.svg — see the note on .hero__plate .media
   for why it is painted rather than masked. */
.hero__plate::after {
  content: "";
  position: absolute;
  /* `top: auto` is not decoration. A duplicate of this rule earlier in the
     file carried `inset: 0`, which pinned the top — and with top AND bottom
     set, `height` is ignored, so the ramp stretched over the whole 520px
     plate instead of its bottom 72%. That is why two rounds of "fix the
     hero" changed nothing: the stops were being applied, just across four
     times the distance. The duplicate is gone; this stays explicit. */
  top: auto;
  left: 0; right: 0; bottom: 0;
  height: 72%;
  background: linear-gradient(to bottom,
    rgba(0, 24, 43, 0) 0%,
    rgba(0, 24, 43, 0.34) 44%,
    rgba(0, 24, 43, 0.8) 72%,
    #00182b 92%);
  pointer-events: none;
}

/* hero-mask-2x3.svg: a longer, earlier fall, because the detail hero carries
   far more type over its lower half. */
.hero--detail .hero__plate::after {
  height: 72%;
  background: linear-gradient(to bottom,
    rgba(0, 24, 43, 0) 0%,
    rgba(0, 24, 43, 0.3) 40%,
    rgba(0, 24, 43, 0.78) 70%,
    #00182b 92%);
}

/* z-index matters: .hero__plate::after is generated as the plate's LAST
   child, so without it the wash paints over the title, the buttons and the
   meta line and the whole action section reads as dimmed. */
.hero__action {
  position: absolute;
  z-index: 1;
  left: 0; right: 0; bottom: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-m);
  padding: 0 var(--frame-gutter) var(--space-2xl);
}
/* Tighter inside the lockup than between the blocks. */
.hero__info { display: flex; flex-direction: column; gap: var(--space-m); width: 100%; }
/* A rhythm rather than one gap. Title and caption are one thing, the
   synopsis is the next, and the button is the thing you do after reading
   both — so the distances go 6, 14, 18 rather than 8, 8, 8. */

/* Title treatment. The design gives it the full content width at 326/108,
   so the height follows the width instead of being fixed. */
.hero__logo {
  width: 100%;
  aspect-ratio: 326 / 108;
}

.meta {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  font-size: var(--type-body-m-size);
  color: var(--color-white-80);
}
/* The separator in the design is a 4px disc, not a middot. */
.meta__dot {
  width: 3px; height: 3px;
  flex: none;
  border-radius: var(--radius-full);
  background: currentColor;
  opacity: 0.55;
}

.hero__tagline {
  width: 100%;
  font-size: var(--type-body-l-size);
  color: var(--color-on-surface);
  text-align: center;
}

.hero__buttons {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-l);
  width: 100%;
}

/* ---- The detail hero  I31431:766443;4266:46226 ------------------------
   A taller plate (585, not 520) with rounded top corners, and a content
   block that is LEFT aligned rather than centred: title treatment 218 x 73,
   a full-width 44px primary button, three round actions, then the clamped
   description and the meta line. */

/* The content is IN FLOW here, not pinned to the bottom of the plate, with
   the plate sitting behind it. That is what makes opening the synopsis push
   the meta line and the rest of the page DOWN instead of shoving the title
   and buttons up — an absolutely bottom-anchored block can only grow
   upwards, which is exactly the wrong direction. */
.hero--detail {
  position: relative;
  padding-top: 313px;
  padding-bottom: var(--space-l);
}

/* ---- the top of a detail page  31431:766518 ----------------------------
   THE HERO DOES NOT RUN TO THE TOP EDGE. In the frame the status bar sits on
   its own ground and the artwork begins underneath it; only the nav row —
   back, cast, close — overlays the picture. The plate was running full
   bleed, which put the clock and the battery on whatever the artwork
   happened to be doing and lost the top 47px of every hero to a scrim.

   The scroller starts below the status bar instead of the plate being
   inset, so the artwork still reaches the left and right edges and the
   whole page scrolls under a clean strip. */
.scroll--inset { top: var(--statusbar-height); }
.phone:has(.scroll--inset) { background: var(--color-background); }
.hero--detail .hero__plate {
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 585px;
  border-radius: var(--radius-l) var(--radius-l) 0 0;
}
/* relative + z-index, or the absolutely-positioned plate paints over the
   static content it is supposed to sit behind. */
.hero--detail .hero__action {
  position: relative;
  z-index: 1;
  align-items: flex-start;
  gap: var(--space-m);
  padding-bottom: 0;
}
.hero--detail .hero__info {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xl);
  width: 100%;
  padding-bottom: var(--space-m);
}
/* THE BOX MATCHES THE ARTWORK. tt-bluelights-detail.png is 1610 x 537, so a
   218 x 73 box is almost exactly right and there is no dead space in it. */
.hero--detail .hero__logo { width: 232px; height: auto; aspect-ratio: 1610 / 537; }

/* ---- the trailer hero's lockup  (The Hardacres) ------------------------
   These have to sit after .hero--detail .hero__logo, which is the same
   specificity — an earlier rule loses to it silently, which is exactly what
   happened the first time.

   The meta line is a caption to the title treatment here rather than a
   footnote under the synopsis: centred, tight under the logo, with the logo
   given more room to be the thing it is. The two read as one lockup. */
/* tt-hardacres.png is 1576 x 282 — five and a half to one. In a 268 x 88 box
   it rendered 48px tall and left FORTY pixels of empty space above and below
   it, which is the whole of the gap that looked like bad spacing. The box is
   the artwork's own ratio now, and wider, so the lettering is bigger as well
   as tighter. */
.hero--trailer .hero__logo {
  width: 308px; height: auto;
  aspect-ratio: 1576 / 282;
  margin: 0 auto;
}

/* THE LOCKUP'S RHYTHM, in one place and after the rule it has to beat.
   .hero--detail .hero__info sets gap: 20px at the same specificity and
   earlier in the file, so a `gap: 0` written above it loses silently — which
   is what left 20px of flex gap stacked on top of every margin here and made
   the block twice as tall as it reads in the design.

   Title and caption are one thing; the synopsis is the next; the button is
   what you do after reading both. 6, 14, 18 — and nothing else contributing. */
.hero--trailer .hero__info {
  gap: 0;
  padding-bottom: 0;
}
.hero--trailer .hero__info > .meta          { margin: 6px 0 0; justify-content: center; }
.hero--trailer .hero__info > .readmore      { margin-top: 14px; }
.hero--trailer .hero__info > .hero__buttons { margin-top: 18px; }
.hero--detail .hero__buttons { flex-direction: column; gap: var(--space-l); }
.hero--detail .btn--primary {
  width: 100%;
  height: 44px;
  padding: var(--space-m) var(--space-xl);
  font-size: var(--type-body-l-size);
}
.hero--detail .meta { justify-content: flex-start; width: 100%; color: var(--color-on-surface); }
.hero--detail .meta .meta__dot { background: var(--color-on-surface); }

/* The MORE affordance sits ON the last line with a short fade to its left,
   so the truncation reads as deliberate rather than as a clipped paragraph.
   103 x 19, background → overlay-variant, leftwards. */
.readmore__fade {
  position: absolute;
  right: 0; bottom: 0;
  width: 103px; height: 19px;
  width: 132px;
  background: linear-gradient(to left,
              var(--color-background) 0%,
              var(--color-background) 42%,
              rgba(0, 24, 43, 0) 100%);
  pointer-events: none;
}

/* Pagination dots  I31431:766433;4220:37851 — five 8px dots, gap 8, py 16. */
.dots {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-s);
  padding: var(--space-l) 0;
  filter: drop-shadow(0 0 8px var(--color-overlay));
}
.dots i {
  width: 8px; height: 8px;
  border-radius: var(--radius-full);
  background: var(--color-white-50);
  transition: background var(--dur-base) var(--ease-standard);
}
.dots i.on { background: var(--color-on-surface); }

/* ==========================================================================
   BUTTONS
   Primary: h 40, px 20, py 8, radius 8, gap 12, icon 20, label 14 semibold.
   Icon button: p 10, radius full, bg surface, border 1px primary@30.
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-m);
  height: 40px;
  padding: var(--space-s) var(--space-xl);
  border-radius: var(--radius-s);
  font-size: var(--type-body-m-size);
  font-weight: var(--type-weight-semi-bold);
  white-space: nowrap;
}
.btn svg { display: block; flex: none; }
.btn--primary { background: var(--accent-action); color: var(--color-on-primary); }
.btn--ghost   { background: var(--color-surface); border: 1px solid var(--color-primary-30); color: var(--color-on-surface); }
.btn--grow    { flex: 1 1 0; min-width: 0; }
.btn--block   { display: flex; width: 100%; }

/* NO PADDING ON A CENTRED GRID.
   The disc is 40, the icon is 20, and place-items:center is the whole job.
   The 10px padding that used to be here left a content box of 18px inside a
   border-box of 40 — so a 20px icon overflowed it, and an overflow in a
   fixed-height border box spills DOWNWARD rather than evenly. Every icon in
   the row sat about a pixel low, which is what the three of them side by
   side made obvious. */
.btn-round {
  width: 40px; height: 40px;
  display: grid; place-items: center;
  flex: none;
  border: 1px solid var(--color-primary-30);
  border-radius: var(--radius-full);
  background: var(--color-surface);
  color: var(--color-primary);
}
.btn-round svg { display: block; }
.btn-row { display: flex; justify-content: center; gap: var(--space-l); }

/* ==========================================================================
   RAIL   31431:766434 / 766435 / 766436
   Outer: pl 16, pb 24 (16 on the Continue Watching and Top 10 rails).
   Header: pb 12, pr 16 — title 18 semibold, chevron 24, subtitle 12.
   Track: gap 12.
   ========================================================================== */

.rail { padding-left: var(--frame-gutter); padding-bottom: var(--space-2xl); }
.rail--tight { padding-bottom: var(--space-l); }

.rail__head {
  padding-right: var(--frame-gutter);
  padding-bottom: var(--space-m);
}
.rail__top {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}
.rail__title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--type-body-xl-size);
  font-weight: var(--type-weight-semi-bold);
  color: var(--color-on-surface);
}
.rail__more { flex: none; width: var(--icon-s); height: var(--icon-s); color: var(--color-on-surface); }
.rail__sub {
  font-size: var(--type-body-s-size);
  color: var(--color-on-surface-variant);
}

.rail__track {
  display: flex;
  align-items: flex-start;
  gap: var(--space-m);
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  scroll-snap-type: x proximity;
}
.rail__track::-webkit-scrollbar { width: 0; height: 0; }
.rail__track > * { flex: none; scroll-snap-align: start; }
.rail__track--mid { align-items: center; }

/* The last card needs the right gutter, or a rail scrolled to its end sits
   flush against the bezel. A pseudo-element rather than padding, because
   padding-right on a flex scroller is dropped by WebKit. */
.rail__track::after { content: ""; flex: none; width: var(--frame-gutter); height: 1px; }

/* ==========================================================================
   CARDS
   Vertical Main Card 144 x 216, radius 8, border 1px white@20 — and NO
   caption underneath. The design labels a card with an optional plate INSIDE
   the artwork, not with type below it.
   ========================================================================== */

.card {
  display: block;
  width: var(--card-2x3-width);
  height: var(--card-2x3-height);
  position: relative;
  border: 1px solid var(--color-white-20);
  border-radius: var(--radius-s);
  overflow: hidden;
}
.card .media { position: absolute; inset: 0; }

/* Label Container  I31431:766434;2651:17366;1141:13219 — primary plate on the
   bottom edge, px 4, py 2, radius 2 on the top corners, 10px semibold. */
.card__label {
  position: absolute;
  left: 50%;
  bottom: -1px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 4px;
  padding: var(--space-2xs) var(--space-xs);
  border-radius: var(--radius-2xs) var(--radius-2xs) 0 0;
  background: var(--accent-active);
  color: var(--color-on-primary);
  font-size: var(--type-body-xs-size);
  font-weight: var(--type-weight-semi-bold);
  white-space: nowrap;
}
.card__label svg { display: block; }

/* Top 10  I31431:766436;1278:15567 — card 111 x 167, numeral 80px in a
   50 x 65 box pulled 10px into the artwork, bottom aligned. */
.rank-card { display: flex; align-items: flex-end; }
.rank-card .card { width: 111px; height: 167px; }
.rank-card { align-items: flex-end; }
.rank-card .card { position: relative; z-index: 1; }
.rank-card__n {
  position: relative;
  z-index: 0;
  width: 50px; height: 65px;
  margin-right: -10px;
  flex: none;
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
  font-size: 80px;
  line-height: 0.82;
  font-weight: var(--type-weight-semi-bold);
  color: transparent;
  -webkit-text-stroke: 2px var(--color-on-surface-variant);
  user-select: none;
}

/* Continue Watching — "Enriched Card"  I31431:766435;2651:17127
   256 wide on surface, border white@20, radius 8. Art 256/144. Progress 4px
   on surface-variant. Info p 8, gap 4. Play icon 48 at left -1, top 79. */
.cw {
  display: block;
  width: 256px;
  border: 1px solid var(--color-white-20);
  border-radius: var(--radius-s);
  background: var(--color-surface);
  overflow: hidden;
  position: relative;
  text-align: left;
}
.cw__art {
  position: relative;
  width: 100%;
  aspect-ratio: 256 / 144;
  border-bottom: 1px solid var(--color-white-20);
  overflow: hidden;
}
.cw__art .media { position: absolute; inset: 0; }
.cw__play {
  position: absolute;
  left: -1px;
  top: 79px;
  padding: var(--space-s);
  color: var(--color-on-surface);
  z-index: 1;
}
.cw__play svg { display: block; width: 48px; height: 48px; }

.progress {
  display: block;
  height: 4px;
  width: 100%;
  background: var(--color-surface-variant);
  overflow: hidden;
}
.progress > i {
  display: block;
  height: 100%;
  width: var(--pct, 0%);
  background: var(--accent-progress);
}

.cw__info {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-s);
}
.cw__text { flex: 1 1 auto; min-width: 0; }
.cw__title {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--type-body-l-size);
  font-weight: var(--type-weight-semi-bold);
  color: var(--color-on-surface);
}
.cw__title .meta__dot { background: var(--color-on-surface); }
.cw__title span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cw__sub {
  font-size: var(--type-body-s-size);
  color: var(--color-on-surface-variant);
}
.cw__more { flex: none; width: var(--icon-m); height: var(--icon-m); display: grid; place-items: center; color: var(--color-on-surface); }

/* Brand block  31431:766437 — a 390/520 plate with a 230-wide title
   treatment, a left-aligned tagline and three poster cards, all inside an
   action section 23.61px off the bottom. */
.brandblock { position: relative; padding-bottom: var(--space-l); }
.brandblock__plate {
  position: relative;
  width: 100%;
  aspect-ratio: 390 / 520;
  overflow: hidden;
}
/* Painted, not masked — see the note on .hero__plate. */
.brandblock__plate .media {
  position: absolute;
  inset: 0;
}
/* color/overlay-variant over the art, per the frame, plus the same ramp the
   hero uses so the lockup at the bottom holds up. */
.brandblock__plate::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to bottom,
      rgba(0, 24, 43, 0.6) 0%,
      rgba(0, 24, 43, 0) 30%,
      rgba(0, 24, 43, 0.9) 80%,
      var(--color-background) 100%),
    var(--color-overlay-variant);
}
.brandblock__plate::before {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 58%;
  z-index: 1;
  background: linear-gradient(to bottom, rgba(0, 24, 43, 0) 0%, rgba(0, 24, 43, 0.72) 62%, #00182b 100%);
}
/* z-index 2 clears the plate's own wash (::before, z-index 1). The lockup
   and the poster cards are drawn OVER the masked art — they are not part of
   it, and inheriting its fade is what made the Shetland cards look washed. */
.brandblock__action {
  position: absolute;
  z-index: 2;
  left: 0; right: 0;
  bottom: 23.61px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-m);
  padding: 0 var(--frame-gutter);
}
.brandblock__logo { width: 230px; height: 69.385px; background-position: left center; }
.brandblock__tagline { font-size: var(--type-body-l-size); color: var(--color-on-surface); }
.brandblock__cards { display: flex; gap: var(--space-m); }

/* ==========================================================================
   TAB BAR   31431:766439
   Container pt 16, pb 25, px 25. The pill is a glass plate: black at 60% over
   white at 6%, 67% opacity, radius 1000. Tab 54 tall, icon 24, label 10.
   The selected tab carries a #121212 plate and an iOS-blue label — that is
   what the component specifies, and it is deliberately not the brand accent.
   ========================================================================== */

.tabbar {
  position: absolute;
  left: 0; right: 0;
  bottom: var(--home-indicator);
  z-index: 30;
  padding: var(--space-l) 25px 25px;
  pointer-events: none;
}
/* The plate. The frame builds it from four stacked layers (a colour-burn
   wash, two gradients, a screen-blend glass pass and a 0 8px 40px shadow) at
   67% over a -4px bleed. Reproduced here as a blurred gradient with the same
   bleed, an inner hairline for the glass edge, and a deeper shadow — a flat
   two-stop fill was what made it read as a drawn rectangle rather than as a
   piece of floating chrome. */
.tabbar__pill {
  position: relative;
  display: flex;
  align-items: stretch;
  padding: 0 2px;
  border-radius: 1000px;
  background: linear-gradient(rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.03)),
              linear-gradient(rgba(0, 0, 0, 0.62), rgba(0, 0, 0, 0.72));
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.45),
              0 1px 0 rgba(255, 255, 255, 0.10) inset,
              0 0 0 0.5px rgba(255, 255, 255, 0.12);
  -webkit-backdrop-filter: blur(24px) saturate(1.4);
  backdrop-filter: blur(24px) saturate(1.4);
  pointer-events: auto;
}

.tabbar__item {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  height: 62px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 8px 8px 9px;
  color: #f5f5f5;
}
.tabbar__item svg { display: block; position: relative; z-index: 1; }
.tabbar__item span {
  position: relative;
  z-index: 1;
  font-size: 10px;
  line-height: 12px;
  font-weight: 600;
  text-align: center;
}
.tabbar__item[aria-current="page"] { color: var(--tab-selected); }
.tabbar__item[aria-current="page"]::before {
  content: "";
  position: absolute;
  inset: 0 -1px;
  border-radius: 100px;
  background: #121212;
}

/* ==========================================================================
   SEGMENTED CONTROL
   The active tab is an underline and a weight change, never a filled pill —
   a pill here would spend the accent on navigation.

   The indicator TRAVELS: js/app.js measures the outgoing and incoming tabs
   and animates a single bar between them, and scrolls the strip so the
   selected tab is always in view. That is the horizontal movement a tab
   change is supposed to have.
   ========================================================================== */

/* Detail Segmented Control  I31431:766444;3608:38439 — items carry their own
   px 16 / py 12 and there is NO gap between them; the indicator is 3px with
   rounded top corners and spans the whole item, padding included. */
.segmented {
  position: relative;
  display: flex;
  overflow-x: auto;
  border-bottom: 1px solid var(--color-hairline);
  scrollbar-width: none;
  scroll-behavior: smooth;
  transition: border-color var(--dur-base) var(--ease-standard);
}
.segmented::-webkit-scrollbar { width: 0; height: 0; }

.segmented__tab {
  flex: none;
  padding: var(--space-m) var(--frame-gutter);
  font-size: var(--type-body-l-size);
  color: var(--color-on-surface-variant);
  white-space: nowrap;
  transition: color var(--dur-base) var(--ease-standard);
}
.segmented__tab[aria-selected="true"] {
  color: var(--color-on-surface);
  font-weight: var(--type-weight-semi-bold);
}

.segmented__bar {
  position: absolute;
  bottom: 0;
  height: 3px;
  border-radius: var(--radius-full) var(--radius-full) 0 0;
  background: var(--accent-active);
  transform: translateX(var(--x, 0));
  width: var(--w, 0);
  transition: transform var(--dur-tab) var(--ease-tab),
              width var(--dur-tab) var(--ease-tab);
}
/* ---- the tab strip sticks under the nav row ---------------------------
   These panels run to two and a half thousand pixels, and losing the tabs
   off the top of a list that long means scrolling back up to change what
   you are looking at. It pins directly under the back/cast row.

   64px is that row's height. The scroller already starts below the status
   bar (.scroll--inset), so the sticky offset is measured from there and not
   from the top of the handset.

   The background is the page's own colour, so at rest — sitting under the
   hero with the page behind it — it is invisible, and it only reads as a
   bar once there is content passing beneath it. The nav row above gets its
   ground from .chrome-scrim, which already fades in on .is-scrolled. */
.segmented-wrap {
  position: sticky;
  top: 64px;
  z-index: 6;
  background: var(--color-background);
}

/* NO LINE ONCE IT IS AGAINST ITS STOP.
   At rest the hairline under the tabs is the design file's and it separates
   the strip from the panel below it. Stuck, it is a rule drawn across the
   screen with the page sliding under it — and that is the one place it reads
   as a seam rather than as a separator, because there is already a nav row
   above it and the two together make a box nothing asked for.

   What replaces it is a short fade off the bottom edge of the strip: the
   strip's own ground going to nothing over sixteen pixels, so content
   dissolves as it passes under instead of being cut off at a line. Over the
   page at rest it is invisible — it is the same colour as what is behind it. */
.phone.is-tabs-stuck .segmented { border-bottom-color: transparent; }
.segmented-wrap::after {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 100%;
  height: var(--space-l);
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(to bottom,
              var(--color-background), var(--color-background-0));
  transition: opacity var(--dur-base) var(--ease-standard);
}
.phone.is-tabs-stuck .segmented-wrap::after { opacity: 1; }
/* NO PADDING AND NO SECOND RULE. .segmented already draws a 1px bottom
   border; the hairline added here was a second line 16px below the first,
   with a band of sticky background between them — which is exactly what it
   looked like. The breathing room the panel needs belongs to the PANEL, so
   it scrolls away instead of sticking. */
.panels { padding-top: var(--space-l); }

/* The panel slides in from the side the new tab sits on, so the movement
   agrees with the direction the indicator travelled. */
.panels { position: relative; overflow: hidden; }
.panel[hidden] { display: none; }
.panel.is-in-right { animation: slide-in-right var(--dur-tab) var(--ease-tab) both; }
.panel.is-in-left  { animation: slide-in-left  var(--dur-tab) var(--ease-tab) both; }
@keyframes slide-in-right { from { opacity: 0; transform: translateX(28px); } to { opacity: 1; transform: none; } }
@keyframes slide-in-left  { from { opacity: 0; transform: translateX(-28px); } to { opacity: 1; transform: none; } }

/* Chips — the companion's lens filter. */
.chips {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  gap: var(--space-s);
  overflow-x: auto;
  padding: var(--space-m) var(--frame-gutter);
  background: var(--color-background);
  scrollbar-width: none;
  scroll-behavior: smooth;
}
.chips::-webkit-scrollbar { width: 0; height: 0; }

.chip {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 34px;
  padding: 0 var(--space-m);
  border: 1px solid var(--color-hairline);
  border-radius: var(--radius-full);
  background: var(--color-surface);
  color: var(--color-on-surface-variant);
  font-size: var(--type-body-s-size);
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard);
}
.chip svg { display: block; }
.chip.is-press { transform: scale(0.96); }
/* SELECTED IS FILLED. An accent border on a dark pill is a difference you
   have to go looking for; a filled pill with dark text is one you cannot
   miss, and it is the same accent-with-on-primary pairing the buttons use. */
.chip[aria-pressed="true"] {
  background: var(--accent-active);
  border-color: var(--accent-active);
  color: var(--color-on-primary);
  font-weight: var(--type-weight-semi-bold);
}
.chip[aria-pressed="true"] svg { color: var(--color-on-primary); }

/* CATCH ME UP. Not one of the example questions — the one thing a second
   screen gets asked for more than anything else — so it is tinted, it keeps
   first place in the row, and a hairline separates it from the examples. */
.chip--catchup {
  position: relative;
  margin-right: var(--space-s);
  padding-right: var(--space-m);
  background: var(--lens-tint);
  border-color: rgba(166, 226, 245, 0.35);
  color: var(--color-primary);
  font-weight: var(--type-weight-semi-bold);
}
.chip--catchup svg { color: var(--color-primary); }
.chip--catchup::after {
  content: "";
  position: absolute;
  right: calc(var(--space-s) * -1);
  top: 6px; bottom: 6px;
  width: 1px;
  background: var(--color-hairline);
}

/* Icons shipped as files. An externally referenced SVG never sees the page's
   CSS, so each one carries the colour the frame gives it; `.ic` only has to
   stop it being squeezed by a flex parent. */
.ic { display: block; flex: none; }

/* OPTICAL CENTRING, MEASURED.
   -------------------------------------------------------------------------
   Every one of these is a 20 x 20 export and every one is centred in its own
   box — but the INK is not. Reading each file's path bounds gives the real
   centre of what you can see:

     bb-plus        10.00, 10.00     centred
     bb-download     9.95, 10.19     0.19 low
     bb-share        9.38, 10.06     0.62 left
     bb-back         9.49, 10.00     0.51 left
     bb-cast         9.63, 10.44     0.37 left, 0.44 low

   Half a pixel is nothing on its own; three of these side by side in 40px
   discs is a row that does not line up, which is what it looked like. The
   nudge is a PERCENTAGE of the icon's own box, so it holds at 20 and at 24.

   bb-play is deliberately NOT corrected: its ink centre sits 0.26 right,
   which is a triangle optically centred — moving it to the geometric centre
   would make it look left. */
.ic[src$="bb-share-20.svg"]    { transform: translate(3.1%, -0.3%); }
.ic[src$="bb-back-20.svg"]     { transform: translateX(2.55%); }
.ic[src$="bb-cast-20.svg"]     { transform: translate(1.85%, -2.2%); }
.ic[src$="bb-download-20.svg"] { transform: translateY(-0.95%); }

/* Dropdown  I31431:766446;4486:33219 — surface, radius 8, item px16/py8 with
   a 16 semibold label, and a 20px chevron sitting on pr 12. */
.dropdown-wrap { padding-bottom: var(--space-l); }
.dropdown {
  display: flex;
  align-items: center;
  width: 100%;
  padding-right: var(--space-m);
  border-radius: var(--radius-s);
  background: var(--color-surface);
  overflow: hidden;
}
.dropdown__item {
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--space-s) var(--frame-gutter);
  text-align: left;
  font-size: var(--type-body-l-size);
  font-weight: var(--type-weight-semi-bold);
  color: var(--color-on-surface);
}

/* ==========================================================================
   LIST ROWS
   ========================================================================== */

.list { padding: 0 var(--frame-gutter); }

.row {
  display: flex;
  align-items: center;
  gap: var(--space-m);
  width: 100%;
  padding: var(--space-m) 0;
  border-bottom: 1px solid var(--color-hairline);
  text-align: left;
}
.row:last-child { border-bottom: 0; }

.row__main { flex: 1 1 auto; min-width: 0; }
.row__name {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  font-size: var(--type-body-l-size);
  font-weight: var(--type-weight-semi-bold);
  color: var(--color-on-surface);
}
.row__note { font-size: var(--type-body-s-size); color: var(--color-on-surface-variant); }
.row__value {
  flex: none;
  font-size: var(--type-body-m-size);
  color: var(--color-on-surface-variant);
  text-align: right;
}

.row__thumb {
  width: 44px; height: 44px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: var(--radius-full);
  overflow: hidden;
  background-color: var(--color-surface);
  color: var(--color-on-surface-variant);
  font-size: var(--type-body-m-size);
  font-weight: var(--type-weight-semi-bold);
}
.row__thumb--square { border-radius: var(--radius-s); width: 56px; height: 56px; }
/* The cast treatment. Every portrait is cropped face-centred and square by
   tools/headshot.swift before it ships, so the row only has to put it in a
   circle at one size and the column reads as one set rather than six
   different photographs cropped six different ways. */
.row__thumb--round { width: 52px; height: 52px; border-radius: var(--radius-full); }
.row__thumb[style*="--src:url"] > span { display: none; }

/* ONE SCORE, ONE COVER.
   The Hardacres' music is an original score with no release — MusicBrainz
   has no release group for it, so there is no cover art to source and the
   five rows were carrying five episode stills instead, which made a cue
   sheet look like an episode list and borrowed photography for a job
   photography was not doing.

   A score is one record, so its cues get the one cover every music app
   would show them under, and for a score that cover is the series' own key
   art — which is what a soundtrack release actually ships with. The hairline
   is the only thing this adds: it reads as a sleeve rather than as another
   still in a column of stills. */
.row__thumb--score { box-shadow: inset 0 0 0 1px var(--color-hairline); }

/* "Playing now". An accent marker on one row, not a restyle of it — the
   list stays one kind of thing and the current item is still findable at a
   glance. */

/* THE ROW THAT IS HAPPENING NOW.
   Under the name, not beside it. A pill on the title line competes with the
   title for the same pixels and wraps anything longer than three words onto
   a second line — which is exactly what the chapter list looked like. This
   is a note, and notes go where notes go. */
.row__now {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 3px;
  font-size: var(--type-body-s-size);
  font-weight: var(--type-weight-semi-bold);
  color: var(--accent-active);
}
.row__now::before {
  content: "";
  width: 5px; height: 5px;
  border-radius: var(--radius-full);
  background: currentColor;
  animation: pulse 2.4s var(--ease-standard) infinite;
}

.list__head {
  padding: var(--space-l) var(--frame-gutter) var(--space-s);
  font-size: var(--type-body-l-size);
  font-weight: var(--type-weight-semi-bold);
  color: var(--color-on-surface);
}

/* Locked rows — nothing past the timecode the viewer has reached. */
.row--locked .row__name, .row--locked .row__note { color: var(--color-surface-variant); }
.row--locked .row__thumb { background-image: none; background: var(--color-surface); color: transparent; }
.redact {
  display: inline-block;
  height: 10px;
  width: var(--w, 120px);
  border-radius: var(--radius-2xs);
  background: var(--color-surface-variant);
  opacity: 0.45;
  vertical-align: middle;
}

/* ==========================================================================
   EPISODE
   ========================================================================== */

/* Episode List Item. The SAME component carries Episodes and Trailers &
   Extras — the list gap is 24, the internal gaps are 8, and there is no
   hairline between items. */
.episodes { display: flex; flex-direction: column; gap: var(--space-2xl); padding-bottom: var(--space-l); }
.episode { display: flex; flex-direction: column; gap: var(--space-s); }

/* Episode Card — 358 x 202.04 on surface, 1px white@20, radius 8. */
.episode__still {
  position: relative;
  width: 100%;
  height: 202.04px;
  border: 1px solid var(--color-white-20);
  border-radius: var(--radius-s);
  background: var(--color-surface);
  overflow: hidden;
}
.episode__still .media { position: absolute; inset: 0; }
.episode__play {
  position: absolute;
  z-index: 1;
  left: -1px; bottom: -1px;
  padding: var(--space-m);
  line-height: 0;
}
.episode__progress { position: absolute; left: 0; right: 0; bottom: 0; }

.episode__meta-box { display: flex; flex-direction: column; gap: var(--space-s); }

/* Container: the info block and the 24px action side by side, gap 16, top
   aligned — the action tracks the TITLE, not the whole block. */
.episode__row { display: flex; align-items: flex-start; gap: var(--space-l); }
.episode__info { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: var(--space-s); }
.episode__act { flex: none; width: var(--icon-s); height: var(--icon-s); }

.episode__title {
  height: 24px;
  font-size: var(--type-body-l-size);
  font-weight: var(--type-weight-semi-bold);
  color: var(--color-on-surface);
}
.episode__meta {
  display: flex; align-items: center; gap: var(--space-s);
  font-size: var(--type-body-m-size);
  color: var(--color-on-surface-variant);
}
.episode__synopsis { font-size: var(--type-body-m-size); color: var(--color-on-surface-variant); }

/* Tag — on the page ground with an outline border, not a tinted chip. */
.tag {
  padding: var(--space-2xs) var(--space-s);
  border: 1px solid var(--color-outline);
  border-radius: var(--radius-xs);
  background: var(--color-background);
  font-size: var(--type-body-xs-size);
  font-weight: var(--type-weight-semi-bold);
  color: var(--color-on-surface);
}

/* The clamped description with its MORE affordance. */
.readmore { position: relative; width: 100%; text-align: left; }
.readmore p {
  font-size: var(--type-body-m-size);
  color: var(--color-on-surface);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.readmore.is-open p { -webkit-line-clamp: unset; display: block; }
/* I31431:766443;…;4236:34623 — surface-variant plate, px 8 / py 2, radius 4,
   10px semibold. */
.readmore__more {
  position: absolute;
  right: 0; bottom: 0;
  padding: var(--space-2xs) var(--space-s);
  border-radius: var(--radius-xs);
  background: var(--color-surface-variant);
  color: var(--color-on-surface);
  font-size: var(--type-body-xs-size);
  font-weight: var(--type-weight-semi-bold);
}
.readmore.is-open .readmore__more { position: static; display: inline-block; margin-top: var(--space-s); }
.readmore.is-open .readmore__fade { display: none; }

/* ==========================================================================
   NEW FUNCTIONALITY — the companion
   Everything below this line is the concept rather than the design file. It
   is built from the same tokens and the same components, but it has no Figma
   node behind it and is the part still open for review.
   ========================================================================== */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px var(--space-s);
  border-radius: var(--radius-xs);
  background: var(--color-surface-container);
  color: var(--color-on-surface);
  font-size: var(--type-body-xs-size);
  font-weight: var(--type-weight-semi-bold);
  white-space: nowrap;
}

.pulse {
  position: relative;
  width: 6px; height: 6px; flex: none;
  border-radius: var(--radius-full);
  background: currentColor;
  animation: pulse 2.4s var(--ease-standard) infinite;
}
/* A ring going out from it. The dot on its own just blinks; the ring is what
   makes it read as a signal coming from somewhere else — which, since the
   television is the somewhere else, is exactly the point. */
.pulse::after {
  content: "";
  position: absolute;
  inset: -3px;
  border-radius: var(--radius-full);
  border: 1px solid currentColor;
  animation: pulse-ring 2.4s var(--ease-standard) infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@keyframes pulse-ring {
  0%   { transform: scale(0.6); opacity: 0.7; }
  70%  { transform: scale(2.1); opacity: 0; }
  100% { transform: scale(2.1); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .pulse, .pulse::after { animation: none; }
  .pulse::after { opacity: 0; }
}

/* ==========================================================================
   THE TRAILER HERO
   --------------------------------------------------------------------------
   A show with a trailer leads with it. 390 x 219 is 16:9 at the frame's own
   width, so the container is the video's shape rather than a crop of it —
   which is the whole reason this is not just the poster plate with a play
   button on top.

   MINIMAL CONTROLS, and minimal is the specification, not an economy: play
   or pause, mute, and a bar you can drag. The <video> element's own chrome
   is off — it is a different design language, a different size on every
   platform, and it carries fullscreen, captions, speed and a download menu
   that have no business on a trailer in a page.
   ========================================================================== */

.trailer {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--color-background);
  /* NOT `overflow: hidden`, and that is what lets the bar sit on the bottom
     edge with a handle centred on it. Nothing in here actually needs
     clipping — the video and the still are both inset to the box and
     object-fit keeps them there — so the only thing the clip was doing was
     slicing the lower half off the scrub handle. */
  overflow: visible;
}
.trailer__video,
.trailer__still {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
/* The still sits OVER the video and fades off when the trailer rolls, so
   the cut from one to the other is a dissolve rather than a swap. It comes
   straight back when the trailer ends. */
/* The still crossfades in BOTH directions. It used to fade out and then snap
   back at the end of the clip, which is the jump: a dissolve one way and a
   cut the other reads as a glitch, not as a transition. The video keeps
   playing its last frame underneath for the length of the fade, so there is
   never a frame of empty ground between the two. */
.trailer__still {
  z-index: 1;
  opacity: 1;
  /* Long and symmetrical, with a short hold before it begins. A dissolve
     this slow reads as the image giving way to the trailer rather than as a
     cut dressed up — and the pause before it starts is what stops the two
     changes (the trailer starting, the image leaving) landing at once. */
  transition: opacity 1.7s cubic-bezier(0.45, 0, 0.25, 1) 0.35s;
}
.trailer.is-rolling .trailer__still { opacity: 0; }
.trailer.is-ended .trailer__still {
  opacity: 1;
  transition: opacity 1.35s cubic-bezier(0.45, 0, 0.25, 1) 0.15s;
}

/* The hero's type sits under the video, so the bottom of the frame has to
   resolve into the page rather than stop at an edge. */
.trailer__scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(to bottom,
    rgba(0, 24, 43, 0.3) 0%, rgba(0, 24, 43, 0) 26%,
    rgba(0, 24, 43, 0) 62%, rgba(0, 24, 43, 0.8) 100%);
}

/* ---- the one control ----------------------------------------------------
   NO CONTAINER AROUND IT. The whole frame is the hit area and the glyph is
   a mark on the picture, centred, with a soft shadow doing the work a
   circular plate used to. It fades out while the trailer is running and
   comes back on a tap — the same behaviour every video player on a phone
   has, and the reason a pause button never sits on top of moving picture. */
.trailer__tap {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: grid;
  place-items: center;
  background: none;
  cursor: pointer;
}
.trailer__glyph {
  display: grid;
  place-items: center;
  color: #fff;
  filter: drop-shadow(0 2px 10px rgba(0, 0, 0, 0.55));
  transition: opacity var(--dur-base) var(--ease-standard),
              transform var(--dur-base) var(--ease-grow);
}
/* Hidden during the hold as well as while it runs. For the first three
   seconds this is a hero image, not a player, and a play mark on it is an
   invitation to interrupt the thing that is about to happen on its own. */
.trailer:not(.is-rolling):not(.is-ended) .trailer__glyph { opacity: 0; }
.trailer.is-playing .trailer__glyph { opacity: 0; transform: scale(0.88); }
/* Asked for: js/app.js sets .is-showing on a tap and clears it after a beat. */
.trailer.is-playing.is-showing .trailer__glyph { opacity: 1; transform: none; }
.trailer__tap:active .trailer__glyph { transform: scale(0.94); }

/* One button, two glyphs, and the state picks. Swapping innerHTML on every
   play/pause would throw away the pressed state mid-tap. */
.trailer__icon-pause, .trailer.is-playing .trailer__icon-play { display: none; }
.trailer.is-playing .trailer__icon-pause { display: block; }
.trailer__icon-mute, .trailer.is-muted .trailer__icon-vol { display: none; }
.trailer.is-muted .trailer__icon-mute { display: block; }
/* Nothing to play or pause once it has finished — the end state owns the
   middle of the frame. */
.trailer.is-ended .trailer__glyph { opacity: 0; pointer-events: none; }

/* ---- the end state ------------------------------------------------------ */
.trailer__end {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: grid;
  place-items: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-standard);
}
.trailer.is-ended .trailer__end { opacity: 1; pointer-events: auto; }
/* A button, with the SAME treatment as the back and cast discs sitting a
   few pixels above it: the surface fill and the on-surface ink from the
   palette, no blur and no bespoke glass recipe of its own. */
.trailer__again {
  display: inline-flex;
  align-items: center;
  gap: var(--space-s);
  padding: var(--space-m) var(--space-xl);
  border-radius: var(--radius-full);
  background: var(--color-surface);
  color: var(--color-on-surface);
  font-size: var(--type-body-m-size);
  font-weight: var(--type-weight-semi-bold);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
  transition: transform var(--dur-fast) var(--ease-standard);
}
.trailer__again svg { display: block; }
.trailer__again.is-press { transform: scale(0.96); }

/* ---- mute --------------------------------------------------------------
   Small, neutral and pinned to the bottom-right corner. It is a preference,
   not an action, so it does not get the accent and it does not get a filled
   plate — a thin ring on a blur is enough to find it over any frame. */
.trailer__mute {
  position: absolute;
  right: var(--space-m);
  bottom: var(--space-m);
  z-index: 6;
  width: 28px; height: 28px;
  display: grid; place-items: center;
  border-radius: var(--radius-full);
  background: rgba(0, 0, 0, 0.42);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
  color: var(--color-white-80);
  transition: transform var(--dur-fast) var(--ease-standard);
}
.trailer__mute svg { display: block; }
.trailer__mute.is-press { transform: scale(0.9); }

/* ---- NOTHING IS A CONTROL UNTIL THERE IS SOMETHING TO CONTROL -----------
   For the first three seconds the hero is a photograph. A mute button and a
   scrub bar on a photograph are controls for a thing that is not happening:
   they advertise the trailer before it arrives and they are the first thing
   the eye goes to, which is the opposite of the point of holding the still.

   Both arrive with the picture, on the same curve as the dissolve and a
   little behind it, so the frame becomes a player rather than starting as
   one. The scrub bar keeps its hit area either way; it is the ink that
   waits. */
.trailer__mute,
.trailer__track::before,
.trailer__fill,
.trailer__knob {
  opacity: 0;
  transition: opacity 0.9s var(--ease-standard) 0.5s,
              transform var(--dur-fast) var(--ease-standard);
}
.trailer.is-rolling .trailer__mute,
.trailer.is-rolling .trailer__track::before,
.trailer.is-rolling .trailer__fill,
.trailer.is-rolling .trailer__knob { opacity: 1; }
.trailer:not(.is-rolling) .trailer__track { pointer-events: none; }

/* ---- the progress bar --------------------------------------------------
   Flush to the bottom edge of the container, full width, nothing beside it.
   It had been floating in a padded row with a timecode and a play button,
   which is a player's control bar; this is a progress line on a hero, and a
   hero's progress line belongs on the edge. */
.trailer__track {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 7;
  height: 24px;              /* hit area — much taller than the bar it draws */
  display: flex;
  align-items: flex-end;
  cursor: pointer;
  touch-action: none;
}

/* Flush with the bottom edge of the video container. */
.trailer__track::before {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 3px;
  background: rgba(255, 255, 255, 0.22);
}
.trailer__fill {
  position: absolute;
  left: 0; bottom: 0;
  width: var(--pct, 0%);
  height: 3px;
  background: var(--accent-progress);
}
.trailer__knob {
  position: absolute;
  left: var(--pct, 0%);
  /* CENTRED ON THE LINE, and free to hang below the container because the
     container no longer clips. The bar's centre is 1.5px up from the bottom
     edge; an 11px circle at -4px has its centre there too. */
  bottom: -4px;
  width: 11px; height: 11px;
  margin-left: -5.5px;
  border-radius: var(--radius-full);
  background: var(--accent-progress);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
  transform: scale(1);
  transition: transform var(--dur-fast) var(--ease-grow);
}
/* Always there, and bigger under a thumb. A handle that only appears once
   you are already dragging cannot tell you the bar is draggable, which is
   the one thing it is for. */
.trailer__track:active .trailer__knob,
.trailer__track:focus-visible .trailer__knob { transform: scale(1.5); }
.trailer__track:focus-visible { outline: none; }

/* The trailer hero has no 520px plate to clear, so the action block sits
   directly under the video instead of being pushed down by one. */
.hero--trailer { padding-top: 0; }
.hero--trailer .hero__action { position: static; padding-top: var(--space-l); }


/* ==========================================================================
   THE COMPANION ENTRY  ·  .tvcard
   --------------------------------------------------------------------------
   The whole panel is the link. There is no button inside it, because the
   page already has a primary action — Resume — and a filled accent block
   sitting directly under it was two controls asking for the same tap at the
   same volume.

   It is the one element on the title page that is not from the design file,
   so it is allowed to look like a live readout rather than a content card:
   an accent hairline along the top edge, a lit surface, and the six lens
   marks along the foot saying what is waiting inside.
   ========================================================================== */

.tvcard {
  position: relative;
  display: block;
  margin: 0 var(--frame-gutter) var(--space-2xl);
  padding: var(--space-l) var(--space-l) var(--space-m);
  border-radius: var(--radius-l);
  background:
    linear-gradient(180deg, rgba(166, 226, 245, 0.07), rgba(166, 226, 245, 0) 38%),
    var(--color-surface);
  box-shadow: var(--edge-light), var(--shadow-raise);
  overflow: hidden;
  transition: transform var(--dur-fast) var(--ease-standard);
}
/* NO ACCENT RULE ALONG THE TOP. It was meant to say "this one is live"; what
   it actually did was draw a bright line across a card that already says
   PLAYING ON LIVING ROOM TV with a pulsing dot beside it. The pulse carries
   it, and the panel reads as a panel rather than as something with a seam. */
.tvcard.is-press { transform: scale(0.985); }

.tvcard__top {
  display: flex; align-items: center; gap: var(--space-s);
  font-size: var(--type-body-xs-size);
  font-weight: var(--type-weight-semi-bold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-on-surface-variant);
}
.tvcard__where { display: flex; align-items: center; gap: var(--space-s); }
.tvcard__where .pulse { color: var(--accent-active); }
.tvcard__at {
  margin-left: auto;
  color: var(--color-on-surface);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
}
.tvcard__now {
  display: block;
  margin: var(--space-m) 0 var(--space-m);
  font-size: var(--type-body-xl-size);
  font-weight: var(--type-weight-semi-bold);
  letter-spacing: -0.015em;
  color: var(--color-on-surface);
}
.tvcard .progress { border-radius: var(--radius-full); background: var(--color-white-20); }
.tvcard__left {
  display: block;
  margin-top: var(--space-s);
  font-size: var(--type-body-s-size);
  color: var(--color-on-surface-variant);
}
.tvcard__foot {
  display: flex; align-items: center;
  gap: var(--space-m);
  margin-top: var(--space-l);
  padding-top: var(--space-m);
  border-top: 1px solid var(--color-hairline);
}
/* The six lenses, stacked rather than laid out in a row: six 26px discs
   with gaps between them is 186px of a 326px line, which left the call to
   action wrapping onto two. Overlapped they are 116px, and they read as one
   group — which is what they are. */
.tvcard__marks { display: flex; align-items: center; flex: none; }
.tvcard__mark {
  width: 26px; height: 26px;
  display: grid; place-items: center;
  border-radius: var(--radius-full);
  background: var(--color-surface-container);
  box-shadow: 0 0 0 2px var(--color-surface);
  color: var(--color-primary);
}
.tvcard__mark + .tvcard__mark { margin-left: -8px; }
.tvcard__cta {
  display: flex; align-items: center; gap: var(--space-s);
  margin-left: auto;
  white-space: nowrap;
  font-size: var(--type-body-m-size);
  font-weight: var(--type-weight-semi-bold);
  color: var(--accent-action);
}
.tvcard__chev {
  width: 24px; height: 24px;
  display: grid; place-items: center;
  border-radius: var(--radius-full);
  background: var(--lens-tint);
}

/* ==========================================================================
   THE THREAD
   --------------------------------------------------------------------------
   Two registers, and the difference between them is the point.

   A BUBBLE is conversation: the viewer's question, and the companion's reply
   to it. Nothing else is a bubble. Making every arrival one was what made the
   surface read as a chat log with pictures stuck to it.

   A CARD is something the companion brought over on its own. It leads with
   artwork at a size worth looking at, and each lens gives its card a
   different shape — a place is wide, a track is a sleeve and a row, a person
   opens onto what else they are in. Same tokens, same accent budget, six
   different things to look at.
   ========================================================================== */

.thread { padding: var(--space-m) 0 0; }
.turn { margin-bottom: var(--space-2xl); padding: 0 var(--frame-gutter); }

/* A card that carries a full-bleed element cancels the thread's gutter and
   re-applies it inside, so the overflowing rail cuts to the screen edge
   rather than stopping short of it.

   IT HAS TO GIVE THE GUTTER BACK TO EVERYTHING THAT IS NOT THE CARD. Only
   the stamp was getting it, so a reply whose attached card happened to be a
   cast card — the one kind that bleeds — had its text running to both edges
   of the screen while the identical reply above it sat on the 16px grid.
   That is the answers-not-respecting-margins bug: it was never the answer
   component, it was the wrapper it happened to be in. */
.turn--bleed { padding: 0; }
.turn--bleed > :not(.turn__attached):not(.lcard),
.turn--bleed .turn__stamp { padding-left: var(--frame-gutter); padding-right: var(--frame-gutter); }
.turn--bleed .turn__attached { padding: 0; }

.turn--new {
  animation: arrive var(--dur-arrive) var(--ease-grow) both;
  animation-delay: calc(var(--i, 0) * var(--stagger));
}
/* A shade of scale with the travel. A card that only slides reads as a row
   in a list; one that also grows the last 1.5% reads as something landing. */
@keyframes arrive {
  from { opacity: 0; transform: translateY(16px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}

/* The recap unfurls: the summary lands first and the beats follow it one at
   a time, which is the order they are read in anyway. */
.turn--new .recap__beat {
  animation: rise 0.5s var(--ease-rise) both;
}
.turn--new .recap__beat:nth-child(1) { animation-delay: 0.26s; }
.turn--new .recap__beat:nth-child(2) { animation-delay: 0.34s; }
.turn--new .recap__beat:nth-child(3) { animation-delay: 0.42s; }
.turn--new .recap__beat:nth-child(n+4) { animation-delay: 0.5s; }
.turn--new .recap__note { animation: rise 0.5s var(--ease-rise) 0.56s both; }

@media (prefers-reduced-motion: reduce) {
  .turn--new, .turn--new .recap__beat, .turn--new .recap__note { animation: none; }
}

.turn__stamp {
  display: flex; align-items: center; gap: var(--space-s);
  margin-bottom: var(--space-s);
  font-size: var(--type-body-xs-size);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-on-surface-variant);
  font-variant-numeric: tabular-nums;
}
.turn__stamp svg { color: var(--accent-active); }
.turn--mine .turn__stamp { justify-content: flex-end; }

/* ---- the lens card ------------------------------------------------------
   A rich row. Leads with the thing, says what it is, offers the one action
   that lens ends in.

   FEWER CONTAINERS. The card is a single soft-cornered surface with no
   outline and no internal rules — the earlier version drew a border round
   the card, another round the thumbnail and a hairline above the foot, which
   is three lines to say one thing. Separation comes from the surface and the
   spacing now.
   ========================================================================== */

.lcard {
  display: block;
  width: 100%;
  /* radius-s, AND IT IS A TOKEN. A 20px corner on a 358px card is a rounded
     rectangle you notice before you read what is in it, and twenty of them
     down a thread is what made the screen look generated. 14 fixed the look
     and broke the system — the ladder is 2 / 4 / 8 / 20 / full and nothing
     may sit between its rungs. 8 is the rung the design file puts its own
     cards on (see .cw), so it is the one this uses. */
  border-radius: var(--radius-s);
  /* LIT, not flat. A surface a shade lighter than the ground is not enough
     separation on a palette this dark — the cards and the background were
     within four points of each other and the thread read as one slab. A
     short top-light, a 1px inner highlight on the top edge and a real
     shadow underneath is what makes a card look like an object. */
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0) 42%),
    var(--companion-surface);
  box-shadow: var(--edge-light), var(--shadow-card);
  overflow: hidden;
  text-align: left;
  transition: transform var(--dur-fast) var(--ease-standard);
}
.lcard.is-press { transform: scale(0.985); }
.turn--bleed .lcard { border-radius: 0; box-shadow: none; }

.lcard__head {
  display: flex;
  align-items: center;
  gap: var(--space-m);
  padding: var(--space-m) var(--space-l);
}

.lcard__thumb {
  display: block;
  position: relative;
  width: 64px; height: 64px;
  flex: none;
  border-radius: var(--radius-s);
  overflow: hidden;
  background: var(--color-background);
}
.lcard__thumb .media { position: absolute; inset: 0; }

/* A sleeve is square and hard-edged; a face is a face; an object wants a
   ground behind it rather than a crop. Three different jobs, three
   treatments, one class. */
.lcard__thumb--sleeve { width: 60px; height: 60px; border-radius: var(--radius-xs); }
.lcard__thumb--face   { width: 60px; height: 60px; border-radius: var(--radius-full); }
.lcard__thumb--object {
  width: 76px; height: 68px;
  background:
    radial-gradient(90% 90% at 50% 0%, rgba(255, 255, 255, 0.1), transparent 70%),
    var(--color-surface-container);
}

/* ---- the stage ----------------------------------------------------------
   Where the subject is something you LOOK at — a street, a plate, a wall —
   the photograph is the card and the words sit on it. Same height as the
   old thumbnail row, four times the picture. */

.lcard__stage {
  position: relative;
  display: block;
  height: 168px;
  overflow: hidden;
  background: var(--color-background);
}
.lcard__stage--short { height: 104px; }
.lcard__stage .media { position: absolute; inset: 0; }
/* The scrim the type sits on. Drawn here rather than baked into the image
   so one file serves the card, the sheet and the collection tile. */
.lcard__stage::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top,
    rgba(0, 10, 20, 0.88) 0%, rgba(0, 10, 20, 0.45) 34%,
    rgba(0, 10, 20, 0) 62%);
}
.lcard__stage--short::after {
  background: linear-gradient(to top,
    rgba(0, 24, 43, 0.9) 0%, rgba(0, 24, 43, 0) 70%);
}

.lcard__over {
  position: absolute;
  left: var(--space-l); right: var(--space-l); bottom: var(--space-m);
  z-index: 1;
}
/* ONE TITLE SIZE IN THE THREAD. There were two — 18 over a picture, 16
   beside a thumbnail — for the same thing said in the same place, and the
   18 was the one that made every card read as a headline rather than as an
   answer. See the note on .answer .lead. */
.lcard__over .lcard__title { margin: 0 0 2px; }
.lcard__over .lcard__line { color: var(--color-white-80); }

/* The lens mark, on the picture. Glass rather than a filled pill: the badge
   has to be legible over a photograph it has never seen. */
.lcard__badge {
  position: absolute;
  left: var(--space-m); top: var(--space-m);
  z-index: 1;
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px var(--space-s) 4px 7px;
  border-radius: var(--radius-full);
  /* A solid tint, not a blur. A badge this small gains nothing visible from
     backdrop-filter and costs a composited layer that is re-blurred on every
     scrolled frame — and there is one of these on most cards in the thread. */
  background: rgba(0, 18, 32, 0.72);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);
  font-size: var(--type-body-xs-size);
  font-weight: var(--type-weight-semi-bold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-primary);
}

.lcard__body { display: block; padding: var(--space-m) var(--space-l) 0; }

/* THE GLYPH PLATE. Where no photograph of the subject exists — a record
   sleeve, a product, a dish — the slot gets a designed treatment rather than
   a still of something else. A large lens mark on a brand-tinted ground
   reads as deliberate; an unrelated photograph reads as a mistake, and in
   the cast case it would be a claim about who is in the picture. */
.lcard__plate {
  display: grid;
  place-items: center;
  background:
    radial-gradient(120% 100% at 30% 0%, rgba(166, 226, 245, 0.18), transparent 70%),
    var(--color-surface-container);
  color: var(--color-primary);
}
.lcard__plate svg { opacity: 0.9; }

.lcard__meta { flex: 1 1 auto; min-width: 0; }
.lcard__lens {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: var(--type-body-xs-size);
  font-weight: var(--type-weight-semi-bold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-primary);
}
.lcard__title {
  display: block;
  margin: 5px 0 3px;
  font-size: var(--type-body-l-size);
  font-weight: var(--type-weight-semi-bold);
  line-height: 1.3;
  letter-spacing: -0.01em;
  color: var(--color-on-surface);
}
.lcard__line {
  display: block;
  font-size: var(--type-body-s-size);
  color: var(--color-on-surface-variant);
}

.lcard__action {
  width: 44px; height: 44px;
  flex: none;
  display: grid; place-items: center;
  border-radius: var(--radius-full);
  background: var(--accent-action);
  color: var(--color-on-primary);
}

/* The foot. A hairline rather than a slab of a different colour: the old
   version painted a dark bar across the bottom of every card, which read as
   a second container stuck to the first. */
.lcard__foot {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  margin: var(--space-m) var(--space-l) 0;
  padding: var(--space-m) 0;
  border-top: 1px solid var(--color-hairline);
  font-size: var(--type-body-s-size);
  color: var(--color-on-surface-variant);
}
.lcard--still .lcard__foot { margin-top: 0; }
.lcard__src { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lcard__price {
  margin-left: auto;
  padding: 2px var(--space-s);
  border-radius: var(--radius-full);
  background: var(--color-surface-container);
  color: var(--color-on-surface);
  font-size: var(--type-body-s-size);
  font-weight: var(--type-weight-semi-bold);
  white-space: nowrap;
}
.lcard__head .lcard__price { flex: none; align-self: center; }
.lcard__go {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--accent-action);
  font-weight: var(--type-weight-semi-bold);
}
.lcard__foot .lcard__go { margin-left: auto; }
/* The chevron in its own tinted disc. A bare arrow beside a word is a link;
   an arrow in a well is a control, which is what this is. */
.lcard__chev {
  width: 22px; height: 22px;
  flex: none;
  display: grid; place-items: center;
  border-radius: var(--radius-full);
  background: var(--lens-tint);
  color: var(--accent-action);
}
.lcard__head > .lcard__chev { margin-left: auto; color: var(--color-on-surface-variant); background: none; }

/* CAST — the one card that earns extra height, because the rail under it is
   the answer to "where do I know them from". It runs to the screen edge. */
.lcard__also { padding: 0 0 var(--space-l); }
/* A SECTION, NOT A SENTENCE. "Where else you have seen them" was a line of
   12px grey floating over a row of posters with no names on them: no rule
   above it, no weight, nothing saying where the card stopped being about
   the person and started being about the work. A hairline, a label, a
   count — and the posters carry their titles, because in a rail answering
   "where do I know them from" the name IS the answer. */
.lcard__also-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-s);
  margin: 0 var(--space-l) var(--space-m);
  padding-top: var(--space-m);
  border-top: 1px solid var(--color-hairline);
}
.lcard__also-label {
  font-size: var(--type-body-xs-size);
  font-weight: var(--type-weight-semi-bold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-on-surface);
}
.lcard__also-count {
  font-size: var(--type-body-xs-size);
  color: var(--color-on-surface-variant);
}

/* A poster with its name under it. */
.titled { width: 84px; }
.titled__name {
  display: block;
  margin-top: 6px;
  font-size: var(--type-body-xs-size);
  line-height: 1.35;
  color: var(--color-on-surface-variant);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.lcard__also-rail {
  display: flex;
  gap: var(--space-m);
  overflow-x: auto;
  overflow-y: hidden;
  padding: 0 var(--space-l);
  scrollbar-width: none;
}
.lcard__also-rail::-webkit-scrollbar { width: 0; height: 0; }
.lcard__also-rail > * { flex: none; }
.lcard__also-rail .card { width: 84px; height: 126px; border: 0; }
.lcard__also-rail .titled { flex: none; }

/* CONTEXT — an idea, so it leads with the idea. The picture is a strip
   along the top rather than the card itself: a photograph of a wall is
   evidence for the idea, not the subject of it. */
.lcard--context {
  background:
    linear-gradient(180deg, rgba(166, 226, 245, 0.085), rgba(166, 226, 245, 0.02) 60%),
    var(--companion-raised);
}
.lcard--context .lcard__title { margin-top: 0; }
.lcard--context .lcard__standfirst {
  display: block;
  margin-top: var(--space-s);
  font-size: var(--type-body-m-size);
  line-height: 1.55;
  color: var(--color-on-surface);
}

/* ==========================================================================
   WHAT JUST HAPPENED  ·  .recap
   --------------------------------------------------------------------------
   Not a lens card and not a message. It is the surface summarising the
   television, so it gets a register of its own: a tinted panel with an
   accent edge down the left, the summary set large, and the beats behind it
   as timecoded rows that reach back to the set.

   Every beat is behind EPISODE.atSec. That is not a restriction here — it is
   the reason a recap can be offered mid-episode at all.
   ========================================================================== */

.recap {
  position: relative;
  display: block;
  padding: var(--space-l) var(--space-l) var(--space-m);
  border-radius: var(--radius-l);
  background:
    linear-gradient(180deg, rgba(166, 226, 245, 0.075), rgba(166, 226, 245, 0.015) 55%),
    var(--companion-surface);
  box-shadow: var(--edge-light), var(--shadow-card);
  overflow: hidden;
}
.recap::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0; width: 3px;
  background: linear-gradient(to bottom, var(--accent-active), rgba(166, 226, 245, 0.1));
}
.recap__eyebrow {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--type-body-xs-size);
  font-weight: var(--type-weight-semi-bold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-primary);
}
.recap__lead {
  margin-top: var(--space-s);
  font-size: var(--type-body-xl-size);
  font-weight: var(--type-weight-semi-bold);
  line-height: 1.38;
  letter-spacing: -0.015em;
  color: var(--color-on-surface);
}
.recap__beats { margin-top: var(--space-m); }
.recap__beat {
  display: flex;
  align-items: flex-start;
  gap: var(--space-m);
  width: 100%;
  padding: var(--space-s) 0;
  border-top: 1px solid var(--color-hairline);
  text-align: left;
  transition: opacity var(--dur-fast) var(--ease-standard);
}
.recap__beat.is-press { opacity: 0.6; }
.recap__at {
  flex: none;
  min-width: 42px;
  padding-top: 1px;
  font-size: var(--type-body-s-size);
  font-weight: var(--type-weight-semi-bold);
  font-variant-numeric: tabular-nums;
  color: var(--accent-action);
}
.recap__text {
  flex: 1 1 auto;
  font-size: var(--type-body-m-size);
  line-height: 1.5;
  color: var(--color-on-surface-variant);
}
.recap__note {
  margin-top: var(--space-s);
  padding-top: var(--space-s);
  border-top: 1px solid var(--color-hairline);
  font-size: var(--type-body-xs-size);
  letter-spacing: 0.02em;
  color: var(--color-surface-variant);
}

/* ==========================================================================
   ASKED BY DESCRIPTION  ·  .match
   --------------------------------------------------------------------------
   When the question named nobody, the answer shows its working: the terms it
   resolved on, and the timecode of the frame it resolved them in. An
   identification the viewer cannot check is worth less than one they can.
   ========================================================================== */

.match {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: var(--space-s);
  font-size: var(--type-body-xs-size);
  color: var(--color-on-surface-variant);
}
.match > svg { color: var(--accent-active); flex: none; }
.match__terms { display: flex; gap: 5px; }
.match__terms b {
  padding: 2px var(--space-s);
  border-radius: var(--radius-full);
  background: var(--lens-tint);
  color: var(--color-primary);
  font-weight: var(--type-weight-semi-bold);
}
.match__at { font-variant-numeric: tabular-nums; }

/* ---- the two voices -----------------------------------------------------
   ONE BUBBLE, and it belongs to the viewer. A question is a message, so it
   looks like one. The companion's answer is not a message — it is the
   surface talking — so it is set as type on the page with no container at
   all. Boxing both made the thread a stack of containers and buried the
   thing you actually wanted to read.
   ========================================================================== */

.bubble {
  max-width: 84%;
  margin-left: auto;
  padding: var(--space-m) var(--space-l);
  border-radius: var(--radius-l) var(--radius-l) var(--radius-2xs) var(--radius-l);
  background: var(--accent-action);
  color: var(--color-on-primary);
  font-size: var(--type-body-m-size);
  font-weight: 500;
  line-height: 1.5;
}

/* The answer. The first line IS the answer and is set to be read on its own;
   what follows is the reason, quieter. */
/* No padding of its own. It sits inside the turn's gutter like the cards
   and the bubbles do, so all three line up down the left AND the right —
   the stray 8px here was making every reply 8px narrower on one side only. */
.answer { padding: 0; }
/* THE ANSWER IS A SENTENCE, NOT A HEADLINE.
   At 18 semibold over 14 regular this was a pull quote: the first line of
   every reply set two steps above everything around it, which is the house
   style of a chatbot and not of a programme. The same 16 the cards use, and
   the weight alone carries the emphasis. */
.answer .lead {
  display: block;
  font-size: var(--type-body-l-size);
  font-weight: var(--type-weight-semi-bold);
  line-height: 1.4;
  letter-spacing: -0.005em;
  color: var(--color-on-surface);
}
.answer p {
  margin-top: var(--space-s);
  font-size: var(--type-body-m-size);
  line-height: 1.55;
  color: var(--color-on-surface-variant);
}
.answer .cite {
  display: block;
  margin-top: var(--space-m);
  font-size: var(--type-body-xs-size);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-surface-variant);
}
.turn__attached { margin-top: var(--space-m); }

.idle {
  display: flex; align-items: center; gap: var(--space-m);
  padding: var(--space-xl) var(--frame-gutter);
  color: var(--color-on-surface-variant);
  font-size: var(--type-body-s-size);
}
.idle::before, .idle::after { content: ""; flex: 1 1 auto; height: 1px; background: var(--color-hairline); }

.held {
  margin: var(--space-l) var(--frame-gutter);
  padding: var(--space-xl);
  border-radius: var(--radius-l);
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0) 42%),
    var(--companion-surface);
  box-shadow: var(--edge-light), var(--shadow-card);
  text-align: center;
}
.held__glyph { color: var(--color-on-surface-variant); margin-bottom: var(--space-m); }
.held__title { font-size: var(--type-body-l-size); font-weight: var(--type-weight-semi-bold); margin-bottom: var(--space-s); }
.held__body { font-size: var(--type-body-m-size); color: var(--color-on-surface-variant); }
.held__note {
  margin-top: var(--space-l);
  padding-top: var(--space-m);
  border-top: 1px solid var(--color-hairline);
  font-size: var(--type-body-s-size);
  color: var(--color-on-surface-variant);
}

/* The dock and the composer: solid, with a short fade above. */
/* ==========================================================================
   JUMP TO THE LATEST  ·  .tolatest
   --------------------------------------------------------------------------
   The thread only follows the bottom while you are at the bottom. Scroll up
   to read something and it lets go — so this is the way back, and the dot is
   how the thread says something arrived while you were up there. A card
   landing no longer drags anyone out of what they were reading.

   It sits above the composer rather than inside it, clears the suggestions,
   and is small: it is a way back, not an announcement.
   ========================================================================== */

.tolatest {
  position: absolute;
  right: var(--frame-gutter);
  /* CLEAR OF THE COMPOSER, which measures 148px with its suggestions. At 124
     it was sitting behind it — the composer is z-index 30 and this is 29, so
     the control existed, reported a size, and could not be seen. */
  bottom: 160px;
  z-index: 29;
  width: 36px; height: 36px;
  display: grid; place-items: center;
  border-radius: var(--radius-full);
  background: var(--companion-raised);
  box-shadow: var(--shadow-raise), inset 0 0 0 1px var(--color-hairline);
  color: var(--color-on-surface);
  opacity: 0;
  transform: translateY(6px) scale(0.94);
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-standard),
              transform var(--dur-base) var(--ease-rise);
}
.tolatest.is-on {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
.tolatest.is-press { transform: scale(0.92); }

/* Something arrived while you were reading. The accent, a dot, and nothing
   else — a count would be a notification badge, and this is a thread, not an
   inbox. */
.tolatest__dot {
  position: absolute;
  top: 1px; right: 1px;
  width: 9px; height: 9px;
  border-radius: var(--radius-full);
  background: var(--accent-active);
  box-shadow: 0 0 0 2px var(--companion-raised);
  opacity: 0;
  transform: scale(0.4);
  transition: opacity var(--dur-fast) var(--ease-standard),
              transform var(--dur-base) var(--ease-rise);
}
.tolatest.has-new .tolatest__dot { opacity: 1; transform: none; }

.dock, .composer {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 30;
  padding: var(--space-m) var(--frame-gutter) calc(var(--home-indicator) + var(--space-xs));
  background: var(--color-background);
}
.dock::before, .composer::before {
  content: "";
  position: absolute;
  left: 0; right: 0; top: -28px;
  height: 28px;
  background: linear-gradient(to bottom, rgba(0, 24, 43, 0), #00182b);
  pointer-events: none;
}
.dock__line { margin-bottom: var(--space-s); font-size: var(--type-body-s-size); color: var(--color-on-surface-variant); text-align: center; }

.composer__box {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  padding: var(--space-s) var(--space-s) var(--space-s) var(--space-l);
  border: 1px solid var(--color-hairline);
  border-radius: var(--radius-full);
  background: var(--color-surface);
  transition: border-color var(--dur-base) var(--ease-standard),
              box-shadow var(--dur-base) var(--ease-standard);
}
/* The one place the viewer types. It should look awake when they are in it. */
.composer__box:focus-within {
  border-color: rgba(166, 226, 245, 0.55);
  box-shadow: 0 0 0 3px rgba(40, 207, 251, 0.12);
}
.composer__input {
  flex: 1 1 auto; min-width: 0;
  border: 0; background: none;
  color: var(--color-on-surface);
  font: inherit;
  font-size: var(--type-body-m-size);
}
.composer__input::placeholder { color: var(--color-on-surface-variant); }
.composer__input:focus { outline: none; }
.composer__send {
  width: 34px; height: 34px;
  display: grid; place-items: center; flex: none;
  border-radius: var(--radius-full);
  background: var(--accent-action);
  color: var(--color-on-primary);
}
.composer__mic {
  width: 34px; height: 34px;
  display: grid; place-items: center; flex: none;
  border-radius: var(--radius-full);
  color: var(--color-on-surface-variant);
}
.composer__mic[aria-pressed="true"] { color: var(--accent-active); }

.prompts {
  display: flex;
  gap: var(--space-s);
  overflow-x: auto;
  overflow-y: hidden;
  /* The suggestions are a separate offer from the field, not a label on it.
     8px read as one control in two parts; 14 lets them be two things. */
  margin: 0 calc(var(--frame-gutter) * -1) 14px;
  padding: 0 var(--frame-gutter);
  scrollbar-width: none;
}
.prompts::-webkit-scrollbar { width: 0; height: 0; }
.prompts::after { content: ""; flex: none; width: var(--frame-gutter); }
.prompts .chip { background: var(--color-background); }

/* The sheet — one template, six payloads. */
.sheet-scrim {
  position: absolute;
  inset: 0;
  z-index: 50;
  background: var(--color-overlay-variant);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-standard);
}
.sheet-scrim[hidden] { display: none; }
.sheet-scrim.is-open { opacity: 1; }

.sheet {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 51;
  max-height: 88%;
  display: flex;
  flex-direction: column;
  border-radius: var(--radius-l) var(--radius-l) 0 0;
  background: var(--color-background);
  box-shadow: var(--shadow-sheet);
  transform: translateY(100%);
  transition: transform var(--dur-sheet) var(--ease-morph-in);
}
.sheet[hidden] { display: none; }
.sheet.is-open { transform: none; }
.sheet:focus { outline: none; }

/* ---- one sheet becoming another -----------------------------------------
   Tapping a chapter inside the chapter list used to replace the sheet's
   contents in a single frame: a different panel, at a different height, with
   no relation to the one that was there. It read as the sheet being yanked
   away and a new one thrown up in its place.

   It is one surface, so it moves like one. The outgoing content drops a few
   pixels and fades, the sheet's own height travels between the two content
   heights (js/app.js pins it, because `auto` will not animate), and the new
   content rises into the box that is already the right size. */
.sheet { transition: transform var(--dur-sheet) var(--ease-morph-in),
                     height 0.34s var(--ease-grow); }
.sheet__scroll { transition: opacity 0.16s var(--ease-standard),
                             transform 0.16s var(--ease-standard); }
.sheet.is-swapping .sheet__scroll { opacity: 0; transform: translateY(6px); }
.sheet.is-swapped  .sheet__scroll { animation: enter-up 0.34s var(--ease-rise) both; }

.sheet__grip { flex: none; padding: var(--space-s) 0 var(--space-xs); display: grid; place-items: center; }
.sheet__grip::after { content: ""; width: 44px; height: 4px; border-radius: var(--radius-full); background: var(--color-surface-variant); }
.sheet__scroll { overflow-y: auto; padding-bottom: calc(var(--home-indicator) + var(--space-l)); scrollbar-width: none; }
.sheet__scroll::-webkit-scrollbar { width: 0; height: 0; }

/* THE PICTURE TAKES ITS OWN SHAPE.
   A fixed 200px band is right for a photograph and wrong for everything
   else. `--ar` is the file's own ratio, so a 16:9 still fills a 16:9 slot
   and a square sleeve gets a square one. Nothing is cropped to a shape it
   was not shot in. */
.sheet__art {
  aspect-ratio: var(--ar, 16 / 9);
  margin: 0 var(--frame-gutter);
  border-radius: var(--radius-s);
  overflow: hidden;
}

/* A THING, NOT A SCENE. Packshots and record sleeves are photographed
   against nothing, so they are shown WHOLE on a plate of their own with air
   around them — the way a shop or a music app shows them. Cover-cropping one
   of these cuts the hem off a coat and the top off a sleeve.

   The plate is light because the pictures are: a studio white cut out on a
   navy ground has a hard edge all the way round it, and this gives it
   somewhere to sit instead. */
.sheet__art--object {
  --pad: var(--space-l);
  aspect-ratio: var(--ar, 1 / 1);
  max-width: 230px;
  margin-inline: auto;
  padding: var(--pad);
  background-color: #f2f1ee;
  background-image: var(--src);
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  background-origin: content-box;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sheet__head { padding: var(--space-l) var(--frame-gutter) 0; }
.sheet__lens { display: flex; align-items: center; gap: 6px; font-size: var(--type-body-xs-size); letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-on-surface-variant); }
.sheet__title { margin: var(--space-s) 0 var(--space-xs); font-size: 24px; line-height: 1.25; font-weight: var(--type-weight-semi-bold); letter-spacing: -0.01em; }
.sheet__standfirst { font-size: var(--type-body-m-size); color: var(--color-on-surface-variant); }
.sheet__body { padding: var(--space-l) var(--frame-gutter) 0; font-size: var(--type-body-m-size); }
.sheet__body p + p { margin-top: var(--space-m); }

.facts { margin: var(--space-l) var(--frame-gutter) 0; }
.facts .row { padding: var(--space-s) 0; }
.facts .row__name { font-size: var(--type-body-m-size); font-weight: var(--type-weight-regular); color: var(--color-on-surface-variant); }
.facts .row__value { color: var(--color-on-surface); font-weight: var(--type-weight-semi-bold); }

/* Provider surface — named in text, with an empty slot for the official
   mark. No third-party mark is shipped. */
.provider {
  margin: var(--space-xl) var(--frame-gutter) 0;
  padding: var(--space-l);
  border: 1px solid var(--color-hairline);
  border-radius: var(--radius-s);
  background: var(--color-surface);
}
.provider__head { display: flex; align-items: center; gap: var(--space-m); }
/* THE SERVICE'S OWN MARK, ON THE SHEET'S OWN GROUND.
   -------------------------------------------------------------------------
   There used to be a dashed box here saying "logo slot". It was replaced by
   a white tile, which solved nothing and added two problems: BritBox's mark
   is drawn in the brand's pale cyan and all but disappeared on white, and
   Spotify's file is the APP ICON — a black rounded square with the green
   circle inside it — so the tile put a white edge around a black edge around
   a logo. Three borders and a mark.

   No tile. Every one of these publishes a mark that is drawn to sit on a
   dark ground, which is the ground this sheet has: Spotify's plain green
   circle (not the app icon), Deliveroo's teal wordmark, Uniqlo's red square,
   BritBox's cyan lettering. The mark is the only thing drawn, which is both
   cleaner and the way each brand's own guidelines would have it.

   Where a service publishes no mark — Visit Belfast — the initials sit in a
   tonal well, so the row still has something at its left edge. Never a logo
   anyone here has drawn. */
.brandmark-slot {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
}
.brandmark-slot img {
  display: block;
  width: auto; height: auto;
  max-width: 100%; max-height: 100%;
}
.brandmark-slot--m { height: 30px; min-width: 30px; max-width: 108px; }
.brandmark-slot--s { height: 22px; min-width: 22px; max-width: 84px; }
.brandmark-slot--letters {
  width: 34px; height: 34px;
  border-radius: var(--radius-xs);
  background: var(--color-surface-container);
  color: var(--color-on-surface);
  font-size: var(--type-body-s-size);
  font-weight: var(--type-weight-semi-bold);
  letter-spacing: 0.04em;
}

.provider__name { display: block; font-size: var(--type-body-l-size); font-weight: var(--type-weight-semi-bold); }
.provider__kind { display: block; font-size: var(--type-body-s-size); color: var(--color-on-surface-variant); }
.provider__offer { margin: var(--space-m) 0; font-size: var(--type-body-m-size); }
.provider__price { font-weight: var(--type-weight-semi-bold); }
.provider__note { margin-top: var(--space-m); font-size: var(--type-body-xs-size); color: var(--color-surface-variant); }

/* The collection's tile, for a thing rather than a scene. Same plate as the
   sheet's listing head, so a packshot is a packshot wherever it appears. */
.tile__art--object {
  background-color: #f2f1ee;
  background-image: var(--src);
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  background-origin: content-box;
  padding: var(--space-s);
}

/* ==========================================================================
   MORE OF THE WORK  ·  .more
   --------------------------------------------------------------------------
   One module, and only on a cast sheet: the work. It is the answer to the
   question that sheet is opened with — where do I know them from — and it
   replaced a BritBox panel saying "included with your subscription", which
   is a sentence about billing on a card about a performer.

   It sits on the sheet's ground rather than on a surface of its own: it is
   the end of this sheet, not a thing stuck to it.
   ========================================================================== */

.more {
  margin: var(--space-xl) var(--frame-gutter) 0;
  padding-top: var(--space-l);
  border-top: 1px solid var(--color-hairline);
}
.more__head { display: flex; align-items: baseline; justify-content: space-between; }
.more__title {
  font-size: var(--type-body-xs-size);
  font-weight: var(--type-weight-semi-bold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-on-surface);
}
.more__note {
  margin-top: var(--space-xs);
  font-size: var(--type-body-s-size);
  color: var(--color-on-surface-variant);
}

/* The same 84px poster the thread's cast card uses, so the two rails are one
   component at one size. */
.more__rail {
  display: flex;
  gap: var(--space-m);
  overflow-x: auto;
  margin: var(--space-m) calc(var(--frame-gutter) * -1) 0;
  padding: 0 var(--frame-gutter);
  scrollbar-width: none;
}
.more__rail::-webkit-scrollbar { width: 0; height: 0; }
.more__rail .card { flex: none; width: 84px; height: 126px; border: 0; }

.more .btn { margin-top: var(--space-l); }

/* ==========================================================================
   THE LISTING HEAD  ·  .listing
   --------------------------------------------------------------------------
   For the two kinds of card whose picture is a THING rather than a scene: a
   packshot and a record sleeve. Full width they were a tall rectangle pinned
   to the top of the sheet with the title stranded underneath and a band of
   dead space between the two. Beside the title they are a listing — which is
   how a shop and a music app lay out exactly this, and for the same reason.

   The third variant is a face: round, beside the name, the way every cast
   row in the product already does it.
   ========================================================================== */

.listing {
  display: flex;
  align-items: center;
  gap: var(--space-l);
  padding: var(--space-s) var(--frame-gutter) 0;
}
.listing__meta { flex: 1 1 auto; min-width: 0; }
.listing__meta .sheet__title { margin-top: var(--space-xs); font-size: 20px; }
.listing__line {
  margin-top: var(--space-xs);
  font-size: var(--type-body-m-size);
  color: var(--color-on-surface-variant);
}

/* The thing, whole, on a plate that gives it somewhere to sit. A studio
   white cut out on a navy ground has a hard edge all the way round it. */
.listing__plate {
  flex: none;
  width: 108px;
  aspect-ratio: var(--ar, 1 / 1);
  padding: var(--space-s);
  border-radius: var(--radius-s);
  background-color: #f2f1ee;
  background-image: var(--src);
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  background-origin: content-box;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
/* A sleeve is printed to its own edge, so it does not get the padding — it
   IS a square picture and cropping it to one is the correct treatment. */
.listing--object .listing__plate[style*="1 / 1"] { padding: 0; }

.listing__face {
  flex: none;
  width: 92px; height: 92px;
  border-radius: var(--radius-full);
}
.sheet__standfirst--loose { padding: var(--space-l) var(--frame-gutter) 0; }

/* ==========================================================================
   THE QUICK ACTION  ·  .qa
   --------------------------------------------------------------------------
   One tap, directly under the standfirst. "What is that?" is followed within
   a second by "can I have one", and until now the answer to the second was
   eleven hundred words further down the sheet. It carries the service's mark
   so it is obvious where the tap goes before it is taken.
   ========================================================================== */

.qa { padding: var(--space-l) var(--frame-gutter) 0; }
.qa__btn {
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--space-m);
  /* THE MARK NEEDS ROOM. At 8px the well was all but touching the top and
     left of the pill, which on a shape this round reads as a logo falling
     out of it. 12 all round, and the well is smaller than it was. */
  padding: var(--space-m) var(--space-l) var(--space-m) var(--space-m);
  border-radius: var(--radius-full);
  background: var(--accent-action);
  color: var(--color-on-primary);
  text-align: left;
  transition: transform var(--dur-fast) var(--ease-standard);
}
.qa__btn.is-press { transform: scale(0.985); }
.qa__label {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--type-body-m-size);
  font-weight: var(--type-weight-semi-bold);
}
.qa__note {
  display: block;
  font-weight: var(--type-weight-regular);
  opacity: 0.72;
  font-size: var(--type-body-s-size);
}
.qa__chev { flex: none; display: grid; place-items: center; opacity: 0.8; }

/* ON THE ACCENT BUTTON THE MARK NEEDS A GROUND.
   The sheet is dark and every one of these marks is drawn for a dark ground
   — but the primary action is a pale accent slab, which is a LIGHT one, and
   a green circle or a teal wordmark on pale cyan is the poor-contrast
   version of the same mistake the white tile made. So inside this button,
   and only here, the mark gets the dark well back. */
.qa .brandmark-slot:not(.brandmark-slot--letters) {
  padding: 5px 7px;
  border-radius: var(--radius-xs);
  background: var(--color-on-primary);
  height: 24px;
  box-sizing: content-box;
}
.qa .brandmark-slot--letters {
  background: var(--color-on-primary);
  color: var(--color-primary);
}


/* ==========================================================================
   WHAT THIS PARTICULAR THING DESERVES  ·  .extra
   --------------------------------------------------------------------------
   The section that changes with the kind of thing. A dish gets a recipe; a
   garment gets the rest of the outfit; a record gets the rest of the
   soundtrack; a place gets the rest of the city. Same component, different
   payload — the lens never gets a renderer of its own.
   ========================================================================== */

.extra {
  margin: var(--space-xl) var(--frame-gutter) 0;
  padding: var(--space-l);
  border-radius: var(--radius-s);
  background: var(--color-surface);
  box-shadow: var(--edge-light);
}
.extra__title {
  font-size: var(--type-body-l-size);
  font-weight: var(--type-weight-semi-bold);
  letter-spacing: -0.005em;
}
.extra__note {
  margin-top: var(--space-xs);
  font-size: var(--type-body-s-size);
  color: var(--color-on-surface-variant);
}
.extra__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-s);
  margin-top: var(--space-m);
}
.extra__meta li {
  padding: 3px var(--space-s);
  border-radius: var(--radius-full);
  background: var(--color-surface-container);
  font-size: var(--type-body-s-size);
}
.extra__sub {
  margin: var(--space-l) 0 var(--space-s);
  font-size: var(--type-body-xs-size);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-on-surface-variant);
}
/* Ingredients as a column of hairline rows, not bullets: a shopping list is
   read down the left edge and bullets put a dot in front of every line. */
.extra__list li {
  padding: var(--space-s) 0;
  border-top: 1px solid var(--color-hairline);
  font-size: var(--type-body-m-size);
}
.extra__steps { counter-reset: step; }
.extra__steps li {
  display: flex;
  gap: var(--space-m);
  padding: var(--space-s) 0;
  font-size: var(--type-body-m-size);
  line-height: 1.5;
}
.extra__n {
  flex: none;
  width: 22px; height: 22px;
  margin-top: 1px;
  display: grid; place-items: center;
  border-radius: var(--radius-full);
  background: var(--lens-tint);
  color: var(--accent-action);
  font-size: var(--type-body-s-size);
  font-weight: var(--type-weight-semi-bold);
}

.extra__row {
  display: flex;
  align-items: center;
  gap: var(--space-m);
  padding: var(--space-s) 0;
  border-top: 1px solid var(--color-hairline);
}
.extra__row:first-of-type { border-top: 0; margin-top: var(--space-s); }
.extra__thumb {
  flex: none;
  width: 44px; height: 44px;
  border-radius: var(--radius-xs);
  overflow: hidden;
}
.extra__thumb--mark {
  display: grid; place-items: center;
  background: var(--color-surface-container);
  color: var(--color-on-surface-variant);
  font-size: var(--type-body-s-size);
  font-weight: var(--type-weight-semi-bold);
}
.extra__meta2 { flex: 1 1 auto; min-width: 0; }
.extra__name {
  display: block;
  font-size: var(--type-body-m-size);
  font-weight: var(--type-weight-semi-bold);
}
.extra__line {
  display: block;
  font-size: var(--type-body-s-size);
  color: var(--color-on-surface-variant);
}

/* ==========================================================================
   WHERE YOU ARE  ·  .nowat
   --------------------------------------------------------------------------
   The chapter sheet's answer to the only question it is opened to answer.
   It replaced a sentence — "9 chapters. You are in The gates on Lanark
   Way." — which said three things at one weight and buried the one that
   mattered in the middle of it. Three facts, three weights, one panel.
   ========================================================================== */

.nowat {
  margin: var(--space-l) var(--frame-gutter) 0;
  padding: var(--space-m) var(--space-l);
  border-radius: var(--radius-s);
  border-left: 2px solid var(--accent-active);
  background: var(--color-surface);
}
.nowat__label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--type-body-xs-size);
  font-weight: var(--type-weight-semi-bold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--accent-active);
}
.nowat__name {
  display: block;
  margin-top: var(--space-xs);
  font-size: var(--type-body-l-size);
  font-weight: var(--type-weight-semi-bold);
  letter-spacing: -0.005em;
}
.nowat__meta {
  display: block;
  margin-top: 2px;
  font-size: var(--type-body-s-size);
  color: var(--color-on-surface-variant);
}
.list--chapters { margin-top: var(--space-l); }

/* The collection. */
.collection__intro { padding: var(--space-2xl) var(--frame-gutter) var(--space-l); text-align: center; }
.collection__title { font-size: 24px; font-weight: var(--type-weight-semi-bold); letter-spacing: -0.01em; }
.collection__sub { margin-top: var(--space-s); font-size: var(--type-body-m-size); color: var(--color-on-surface-variant); }

/* Grid Content - Vertical  I31431:766523 — THREE columns of the 111 x 167
   card, gap 12 both ways: 111*3 + 12*2 = 357, which is the frame's width.
   Not the 144 x 216 rail card. */
.grid-cards {
  display: grid;
  grid-template-columns: repeat(3, 111px);
  justify-content: space-between;
  gap: var(--space-m);
  padding: var(--space-l) var(--frame-gutter) var(--space-l);
}
.grid-cards .card { width: 111px; height: 167px; }
/* The label on a grid card is type/body/2xs, a step smaller than on a rail. */
.grid-cards .card__label { font-size: var(--type-body-2xs-size); }

.grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-s); padding: 0 var(--frame-gutter); }
.tile {
  display: block;
  padding: var(--space-m);
  border-radius: var(--radius-l);
  /* Same treatment as a lens card, because it is the same thing later. */
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0) 42%),
    var(--companion-surface);
  box-shadow: var(--edge-light), var(--shadow-card);
  text-align: left;
  transition: transform var(--dur-fast) var(--ease-standard);
}
.tile.is-press { transform: scale(0.97); }
.tile__art { display: grid; place-items: center; height: 84px; border-radius: var(--radius-s); overflow: hidden; margin-bottom: var(--space-s); }
.tile__lens { display: flex; align-items: center; gap: 5px; font-size: var(--type-body-xs-size); letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-on-surface-variant); }
.tile__title { display: block; margin-top: 3px; font-size: var(--type-body-m-size); font-weight: var(--type-weight-semi-bold); }

/* ==========================================================================
   ARRIVAL
   --------------------------------------------------------------------------
   Items settle in rather than appearing. Unhurried on purpose: a long, eased
   travel over a short distance reads as composure, where a fast bounce over
   a long one reads as a website. js/app.js sets --i per item for the stagger
   and reveals below-the-fold blocks as they come into view.
   ========================================================================== */

/* NOT hidden by default. An item is only made invisible once JS has taken
   responsibility for revealing it (.is-armed), so if the observer never
   fires — no IntersectionObserver, a panel that was hidden when it was
   scanned, a script error — the content is simply there. Content that
   depends on an animation to be visible at all is a bug waiting to happen. */
.rise.is-armed { opacity: 0; }
.rise.is-in {
  animation: rise var(--dur-rise) var(--ease-rise) both;
  animation-delay: calc(var(--i, 0) * var(--stagger-rise));
}
@keyframes rise {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}

/* THE RIPPLE. A revealed block brings its own contents in behind it, one
   short beat apart, so a rail arrives as a run of cards rather than as a
   single slab. Eight steps is enough for any rail on screen at once; past
   that they share the last delay, which nobody can see. */
.rise.is-in .rail__track > *,
.rise.is-in .grid-cards > *,
.rise.is-in .brandblock__cards > *,
.rise.is-in .episodes > * {
  animation: rise var(--dur-rise) var(--ease-rise) both;
}
.rise.is-in :is(.rail__track, .grid-cards, .brandblock__cards, .episodes) > *:nth-child(1) { animation-delay: calc(var(--i, 0) * var(--stagger-rise)); }
.rise.is-in :is(.rail__track, .grid-cards, .brandblock__cards, .episodes) > *:nth-child(2) { animation-delay: calc(var(--i, 0) * var(--stagger-rise) + 1 * var(--stagger-item)); }
.rise.is-in :is(.rail__track, .grid-cards, .brandblock__cards, .episodes) > *:nth-child(3) { animation-delay: calc(var(--i, 0) * var(--stagger-rise) + 2 * var(--stagger-item)); }
.rise.is-in :is(.rail__track, .grid-cards, .brandblock__cards, .episodes) > *:nth-child(4) { animation-delay: calc(var(--i, 0) * var(--stagger-rise) + 3 * var(--stagger-item)); }
.rise.is-in :is(.rail__track, .grid-cards, .brandblock__cards, .episodes) > *:nth-child(5) { animation-delay: calc(var(--i, 0) * var(--stagger-rise) + 4 * var(--stagger-item)); }
.rise.is-in :is(.rail__track, .grid-cards, .brandblock__cards, .episodes) > *:nth-child(6) { animation-delay: calc(var(--i, 0) * var(--stagger-rise) + 5 * var(--stagger-item)); }
.rise.is-in :is(.rail__track, .grid-cards, .brandblock__cards, .episodes) > *:nth-child(7) { animation-delay: calc(var(--i, 0) * var(--stagger-rise) + 6 * var(--stagger-item)); }
.rise.is-in :is(.rail__track, .grid-cards, .brandblock__cards, .episodes) > *:nth-child(n+8) { animation-delay: calc(var(--i, 0) * var(--stagger-rise) + 7 * var(--stagger-item)); }

@media (prefers-reduced-motion: reduce) {
  .rise, .rise.is-armed, .rise.is-in { opacity: 1; animation: none; transform: none; }
  .rise.is-in :is(.rail__track, .grid-cards, .brandblock__cards, .episodes) > * { animation: none; }
  .media, .media.is-waiting { opacity: 1; transition: none; }
}

/* ==========================================================================
   THE ENTRANCE
   --------------------------------------------------------------------------
   THREE MOVEMENTS, and nothing else is allowed one.

     resolve   a picture comes up with a shade of scale coming off it. 1.04
               to 1 over nearly a second reads as something coming into
               focus; the same picture sliding would read as a slideshow.
     rise      content travels ten pixels up. Short, because the distance is
               not the point — the arrival is.
     settle    chrome fades in place and does not move at all. A frame that
               travels is a frame you end up watching instead of the thing
               inside it, and every version of this that moved the top bar
               looked cheap.

   TWO SCHEDULES, because the two kinds of screen are read differently.
   js/app.js sets data-enter and the --enter indices; every duration and
   delay is here.

     [data-enter="hero"]     home, both detail pages
       0.00  the picture        hero plate / trailer / brand plate
       0.14  the frame          app bar or back bar
       0.20  the lockup         title treatment, meta, synopsis, buttons
       0.40  the panel          companion entry, tab strip, pagination
       0.50  the foot           tab bar
       0.42  the content        rails and panels, released by js/app.js

     [data-enter="thread"]   the companion
       0.00  the live chrome    header
       0.08  the television     playback strip
       0.16  the lenses         chip row
       0.22  the conversation   turns, BOTTOM-MOST FIRST
       0.30  the foot           composer

   The stages overlap at the edges deliberately. Four strictly separated
   stages took a second and a half and felt like a loading screen; starting
   the next while the last is still settling reads as one movement with
   several parts, which is what it is.
   ========================================================================== */

@keyframes enter-resolve {
  from { opacity: 0; transform: scale(1.04); }
  to   { opacity: 1; transform: none; }
}
@keyframes enter-rise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}
@keyframes enter-settle {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes enter-foot {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

/* ---- hero screens ------------------------------------------------------ */

/* NOTHING HERE MAY NAME A .rise BLOCK.
   The entrance owns the first screenful; the arrival system owns everything
   that scrolls into view, and a few elements were in both — the companion
   card and the brand block's plate were animated by the entrance AND armed
   as arrivals, which is what the double-load looked like on the way in. One
   element, one animation, one owner. js/app.js enforces it (see entrance()),
   but the rule starts here: if a selector below ever matches something
   carrying .rise, the .rise is the one that goes. */

.phone.is-enter[data-enter="hero"] .hero__plate,
.phone.is-enter[data-enter="hero"] .trailer {
  animation: enter-resolve 1.15s var(--ease-rise) both;
}
.phone.is-enter[data-enter="hero"] .appbar,
.phone.is-enter[data-enter="hero"] .navbar {
  animation: enter-settle 0.62s var(--ease-standard) 0.24s both;
}
.phone.is-enter[data-enter="hero"] .hero__action > *,
.phone.is-enter[data-enter="hero"] .hero__info > * {
  animation: enter-rise 0.68s var(--ease-rise) both;
  animation-delay: calc(0.36s + var(--enter, 0) * 0.085s);
}
/* .hero__info is a wrapper, not an item — it must not animate as well as its
   own children, or everything inside it fades twice. */
.phone.is-enter[data-enter="hero"] .hero__info { animation: none; }

.phone.is-enter[data-enter="hero"] .dots {
  animation: enter-rise 0.6s var(--ease-rise) 0.62s both;
}
.phone.is-enter[data-enter="hero"] .segmented-wrap {
  animation: enter-settle 0.6s var(--ease-standard) 0.66s both;
}
.phone.is-enter[data-enter="hero"] .tabbar {
  animation: enter-foot 0.62s var(--ease-rise) 0.74s both;
}

/* ---- trailer heroes ----------------------------------------------------
   The Hardacres. Same three movements, half the distance between them: the
   video is only 219px of a 844px screen, so everything under it has to be
   there almost as soon as it is or the page reads as empty. The video fades
   and does not scale — see entrance() in js/app.js. */

.phone.is-enter[data-enter="trailer"] .trailer {
  animation: enter-settle 0.72s var(--ease-standard) both;
}
.phone.is-enter[data-enter="trailer"] .navbar {
  animation: enter-settle 0.5s var(--ease-standard) 0.1s both;
}
.phone.is-enter[data-enter="trailer"] .hero__action > *,
.phone.is-enter[data-enter="trailer"] .hero__info > * {
  animation: enter-rise 0.56s var(--ease-rise) both;
  animation-delay: calc(0.16s + var(--enter, 0) * 0.055s);
}
.phone.is-enter[data-enter="trailer"] .hero__info { animation: none; }
.phone.is-enter[data-enter="trailer"] .segmented-wrap {
  animation: enter-settle 0.5s var(--ease-standard) 0.42s both;
}
.phone.is-enter[data-enter="trailer"] .tabbar {
  animation: enter-foot 0.52s var(--ease-rise) 0.46s both;
}

/* ---- the companion ----------------------------------------------------- */

.phone.is-enter[data-enter="thread"] .topbar {
  animation: enter-settle 0.56s var(--ease-standard) both;
}
.phone.is-enter[data-enter="thread"] .chips {
  animation: enter-settle 0.56s var(--ease-standard) 0.2s both;
}
/* Bottom-most first — see the --enter indices in js/app.js. */
.phone.is-enter[data-enter="thread"] .thread > .turn {
  animation: enter-rise 0.64s var(--ease-rise) both;
  animation-delay: calc(0.34s + var(--enter, 0) * 0.11s);
}
.phone.is-enter[data-enter="thread"] .composer {
  animation: enter-foot 0.62s var(--ease-rise) 0.42s both;
}

@media (prefers-reduced-motion: reduce) {
  .phone.is-enter *,
  .phone.is-enter[data-enter="hero"] .hero__plate,
  .phone.is-enter[data-enter="hero"] .trailer { animation: none !important; }
}

/* THE SHEET LOCKS WHAT IS BEHIND IT. Without this the scroller kept taking
   scroll while the sheet was open, which re-ran the arrival sweep and left
   the page visibly crawling underneath. */
.is-sheet-open .scroll { pointer-events: none; }

/* ==========================================================================
   SCREEN TRANSITIONS
   --------------------------------------------------------------------------
   Cross-document view transitions. The screens are separate documents, but
   the browser captures the outgoing page and animates to the incoming one,
   so moving between them is a transition rather than a reload — no flash, no
   device re-scaling, nothing re-initialising in view.

   The DEVICE is named, so the browser treats the handset as one continuous
   element across the navigation and only its contents cross-fade. That is
   what stops the whole prototype looking like it restarted.

   Where this is unsupported the navigation simply happens, with no
   animation and nothing broken.
   ========================================================================== */

/* DELIBERATELY NOT `navigation: auto`.
   -------------------------------------------------------------------------
   A cross-document transition fades the outgoing page out ON TOP of the
   incoming one — and the incoming one is already assembling itself
   underneath, because its scripts have run. For the length of that fade you
   see outgoing content dissolving over incoming content rising: two fades,
   two directions, same pixels. No amount of retuning the durations fixes
   that; they are two animations of two different things in one place.

   So the navigation is a cut, and the entrance IS the transition. The device
   is sized before the first paint (see the inline script in each page's
   head) and both pages sit on the same ground, so there is nothing to see at
   the cut itself — and then the new screen assembles, once, in order.

   This is also the only version of this that can be verified: an entrance is
   just animations on the page, where a cross-document transition is skipped
   outright in any context where the document is not visible. */
@view-transition { navigation: none; }

.device { view-transition-name: device; }

/* THE OUTGOING SCREEN FADES. THE INCOMING ONE DOES NOT.
   -------------------------------------------------------------------------
   This is the fix for the double-load. The new page used to cross-fade in
   over 0.72s while its own staged entrance was ALSO running from 0 to about
   1.3s — so every element faded up twice, on two different curves, at two
   different times. That is what made the detail pages look like they were
   loading their contents twice and arriving unevenly.

   There only needs to be one animation per element, and the entrance is it:
   the old screen fades out, the new one is simply there underneath with its
   contents still to assemble, and then it assembles. The device itself never
   moves, because it is one named element across the navigation. */
::view-transition-old(device),
::view-transition-new(device) { animation: none; }

/* The group must not animate its own box either, or the device appears to
   resize between screens of different heights. */
::view-transition-group(device) { animation-duration: var(--dur-screen); }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(device),
  ::view-transition-new(device) { animation: none; }
}

/* The scaled wrapper animates only AFTER the first fit. Without this the
   device visibly grows into place on every load, which reads as a restart
   even when the transition itself is clean. */
.device-fit.is-live { transition: width var(--dur-base) var(--ease-standard),
                                  height var(--dur-base) var(--ease-standard); }
.device-fit.is-live .device { transition: transform var(--dur-base) var(--ease-standard); }

/* ==========================================================================
   SCREEN-JUMP  —  REVIEW ONLY
   Not part of the product. Sits OUTSIDE the device, collapsed, toggled by D.
   ========================================================================== */

.jump {
  position: fixed;
  left: 12px; bottom: 12px;
  z-index: 96;
  font-size: 11px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.jump[hidden] { display: none; }
.jump__panel {
  display: flex; flex-direction: column; gap: 2px;
  padding: 8px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 8px;
  background: rgba(0, 12, 22, 0.94);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}
.jump__group { margin-top: 6px; color: var(--color-surface-variant); text-transform: uppercase; letter-spacing: 0.08em; font-size: 9px; }
.jump__group:first-child { margin-top: 0; }
.jump a { padding: 3px 6px; border-radius: 4px; color: var(--color-on-surface-variant); white-space: nowrap; }
.jump a[aria-current="page"] { color: var(--accent-active); }
.jump__hint { margin-top: 6px; color: var(--color-surface-variant); }

/* ==========================================================================
   MOTION PREFERENCES
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .segmented, .chips { scroll-behavior: auto; }
}
