/* One stylesheet, one owner. Monochrome.
 *
 * No hue anywhere in the chrome - the whole interface is black through white,
 * and hierarchy is carried by contrast, weight and space instead of colour.
 * That is the discipline: with no accent hue to lean on, anything that needs
 * to stand out has to earn it by being brighter, heavier or better spaced.
 *
 * The one thing deliberately left in colour is the artwork. Posters, backdrops
 * and clearlogos ARE the colour in a media app; draining them would make the
 * shelves harder to scan and is the opposite of what a poster is for. The
 * badge chips, which are interface rather than content, are desaturated so
 * they sit in the scheme.
 *
 * Ramp, so greys stay deliberate rather than drifting:
 *   #000  ground          #0b0b0b  raised      #141414  panel
 *   #1f1f1f hairline      #333  border      #909090  secondary text
 *   #eaeaea primary text  #fff     emphasis / the "accent"
 */

:root {
  /* surfaces */
  --bg: #000;
  --bg-hi: #000;
  --bg2: #0b0b0b;
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  --panel: rgba(15, 15, 15, .96);
  --header: rgba(0, 0, 0, .55);
  --elev: rgba(255, 255, 255, .055);
  --elev2: rgba(255, 255, 255, .11);
  --line: #1f1f1f;
  --line-hi: #333;
  --topH: 3.6rem;
  /* Where content starts, once the bar has been cleared.
     The bar is a capsule floating .5rem down from the top edge now rather than
     a full-width thing anchored to it, so "clear of it" means clear of a shape
     with air on every other side - and content arriving flush under it reads
     as crowding in a way it did not when the bar was a boundary. One rem of
     that air, stated once: the page padding, the cast jump's landing and the
     sticky year label all used to write calc(var(--topH) + .5rem) separately,
     which is three places to keep in agreement about one distance. */
  --clear: calc(var(--topH) + 1.5rem);
  /* One blur for every piece of floating chrome - the header capsule, the two
     corner buttons, the phone's tab bar and the refresh panel. They are one
     material, and five copies of a number are five chances to drift.
     Turned down from 20px: at that strength what is behind the bar was mush
     rather than a suggestion of a picture, and being able to tell something is
     back there is the whole point of glass.
     It costs no legibility, and that is measured rather than hoped. The .62
     ground was checked at 4.53:1 with NO blur at all, because the car never
     paints one - so every device that does draw a blur has always been getting
     decoration on top of a ground that already passed on its own. */
  --glass: blur(12px) saturate(1.25);
  /* type */
  --fg: #eaeaea;
  --dim: #909090;
  --dimmer: rgba(144, 144, 144, .62);
  /* emphasis - white is the accent. There is no second hue to reach for. */
  --accent: #fff;
  --accent-hi: #fff;
  --accent-deep: #f2f2f2;              /* primary button ground */
  --accent-ink: #000;                  /* text on that ground */
  --good: #fff;                        /* selected / confirmed */
  --good-deep: #d6d6d6;
  --bad: #fff;                         /* failed - carried by wording, not hue */
  /* Was never declared, so every var(--warn, #f5c451) fell through to a
     gold literal - a third hue, in a palette that has exactly two and
     gives both a meaning. "Public" is an assertion, and white is already
     the word for asserted. */
  --warn: #fff;
  /* ---- the measure and the gutter ----
     Declared here, ONCE, and never re-declared. Every "the heading sits at 60
     and the grid it labels sits at 430" bug this file has had came from a
     second opinion about the measure written somewhere further down. There
     were five of them - 96rem in four separate places, 106rem, 92rem, and
     calc(96rem + var(--pad)) - and they did not agree.

     --measure is in px, not rem, on purpose. rem here is calc(15px * --ui),
     and --ui was a TEXT-SIZE preference running 0.92 to 1.26, so a 96rem cap
     silently moved the page between 1325px and 1814px depending on how big
     someone liked their type. How WIDE the page is belongs to the screen; how
     MUCH fits inside it belongs to the type knob. Those are two questions and
     this token answers only the first.

     3000, which is deliberately larger than any screen this runs on, so in
     practice the page is edge-to-edge and the gutter alone sets the margin.
     That is the answer to the actual complaint: on a media browser a wide
     screen should show MORE POSTERS, not the same number in a centred column
     with 469px of nothing beside it. Everything here is already
     auto-fill/minmax, so width turns into items rather than into stretch.

     The token stays because a governor is still wanted at the extreme: past
     3000px a row of text really does become unreadable, and this is where
     that is decided - once, instead of in five places that disagreed. */
  --measure: 3000px;
  /* One kind of unit at every width. This was 3.3% below 900px and 3.2vw
     above it - a percentage of each element own container, then a slice of
     the viewport - so one token produced two different gutters depending on
     where it was used, and grew without limit on a wide screen. */
  --pad: clamp(0.95rem, 3.2vw, 3.4rem);
  /* Prose. In ch, so a measure tracks the type it is measuring rather than
     the window. About 68 characters is the line a person reads without
     losing the start of the next one. */
  --prose: 68ch;
  --prose-tight: 46ch;
  --rail-card: clamp(8.9rem, 9vw, 9.8rem);
  /* ---- rings ----
     One idea at three strengths, so a ring always means the same kind of
     thing and you can still tell which. They had drifted into five different
     treatments - 2px accent, .1em at 85%, .11em at 85%, .12em solid white in
     two places - which is why pointing at a card looked identical to owning
     it.

       faint, inside   the pointer is over this
       solid, OUTSIDE  the keyboard is on this  (outside is the tell)
       solid, inside   you are here

     Ownership is deliberately NOT in this list. It used to take the strongest
     ring on the card, which made every hover claim it; the sub-line already
     reads "In library" in words, which is a better statement than a ring
     anyone has to learn. */
  --ring-w: .12em;
  --ring-hover: rgba(255, 255, 255, .45);
  --ring-focus: #fff;
  --ring-here: rgba(255, 255, 255, .85);
  /* One gap between rows, whatever kind of row it is. */
  --row-gap: 1.7rem;
  /* A still card. Sized here rather than inline because it is the one card
     that never had a phone size - see the max-width: 700px block. */
  --wide-card: 15.5rem;
  --r-s: .5em;
  --r-m: 1em;
  --r-l: 1.25em;
  --ease: cubic-bezier(.2, .8, .2, 1);
  /* Overshoots and settles. For the handful of moments that are an ARRIVAL -
     a tick landing, a heart filling, a dialog appearing - where stopping dead
     on the value reads as mechanical. Never for anything that moves often:
     overshoot on a control you touch repeatedly is a tic. */
  --pop: cubic-bezier(.34, 1.56, .64, 1);

  /* Room above a row for the card that is standing up out of it.
   *
   * Any element with overflow-x: auto also clips VERTICALLY - a computed
   * overflow-y of visible is forced to auto the moment the other axis is not
   * visible - so every horizontally scrolling row in this app is a box with a
   * lid on it. A card that lifts 4px on hover was being shaved by that lid,
   * and the 3D tilt made it obvious: measured worst case, a tilted poster
   * reaches 5.5px above its resting top edge.
   *
   * Measured, not guessed, and not tight: 12.6px against a 5.5px worst case,
   * so the tilt can be deepened later without this coming back.
   *
   * Added as padding and taken straight back off as a negative margin, so
   * every row sits exactly where it sat before. The rules below fold it into
   * their OWN padding rather than a later override doing it for them - each
   * row stays the single place its spacing is written down. */
  --lift-room: .75rem;
  --shadow: .1em .1em .15em rgba(0, 0, 0, .5);
  --shadow-lg: 0 14px 40px -12px rgba(0, 0, 0, .9);
  --blur: blur(5px);
  --blur-lg: blur(10px);

  /* ---- the fixed stack ----
   * Every layer pinned to the viewport, in the order it paints. There are
   * thirteen of them and until now each carried a bare number chosen where it
   * was written, which meant three pairs shared a value and were separated by
   * nothing but source order: .top with .fixbar, .topback with .fab, and
   * .sheet-pop with both full-screen viewers. Only the last of those can
   * actually meet on screen - a corner panel left open while a still opens -
   * and it came out right by luck.
   *
   * A name rather than a number, so the next thing pinned to an edge has to
   * say WHERE it belongs rather than pick a figure that wins. Raising one to
   * win a fight only means covering the navigation instead.
   *
   * --z-corner is shared on purpose: .topback and .fab are the same kind of
   * control at opposite corners of the same screen and cannot overlap. */
  --z-top: 40;        /* the header capsule, top centre */
  --z-fixbar: 42;     /* a commit bar pinned to the bottom edge */
  --z-backbar: 50;    /* the detail page's own back circle */
  --z-corner: 55;     /* .topback and .fab - peers, opposite corners */
  --z-tabs: 60;       /* the tab bar */
  --z-pop: 85;        /* a corner panel; a full-screen view covers it */
  --z-full: 90;       /* full screen over the app: stills, the trailer */
  --z-player: 100;
  --z-qpop: 120;      /* the query popover, over the player's chrome */
  --z-stream: 160;
  --z-toast: 200;     /* last, always: it reports on everything above */
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
/* Any author `display` rule beats the user-agent [hidden] rule, and this app
   sets display on .iconbtn (grid) and .nextup (flex) - so el.hidden = true
   silently did nothing and the up-next card and the subtitle/audio buttons
   showed regardless. One rule fixes the class of bug rather than each case. */
[hidden] { display: none !important; }

/* One knob scales the whole interface: --ui multiplies the rem base, and
   every size in this file is expressed against it. The two selectors are
   split deliberately - rem resolves against html only, so leaving body on a
   literal pixel size would scale every box while the text inside it, and
   anything inheriting through `font: inherit`, stayed exactly as it was. */
/* U+20C1 SAUDI RIYAL SIGN, carried rather than hoped for.

   The sign is new - Unicode 17.0, September 2025 - and the car's browser does
   not have it in any font it ships with, so the app fell back to printing
   "SAR". Correct, readable, and not what the same figure looks like on the
   phone in your pocket, which is how it got noticed.

   768 bytes, and it is 768 bytes because the unicode-range is one codepoint:
   this face is subset to the single glyph and the browser will never download
   it for anything else, never use it for anything else, and never let it touch
   the metrics of a single other character. That is also why it can sit at the
   FRONT of the global stack below without being a font change - for every
   codepoint except this one it does not exist.

   Self-hosted, not linked. It works with no network, it is one fewer
   third-party request from the car, and the file cannot change under us. Cut
   from Noto Naskh Arabic (SIL Open Font License 1.1) via Google's subsetter;
   its outline was checked against the one this machine already draws before
   anything was wired up.

   font-display: block, not swap: swap paints the fallback first, and the
   fallback for a codepoint nothing has is a tofu box. Better a beat of nothing
   than a rectangle. riyalMark() in app.js is what actually decides whether the
   glyph is printed at all, and it only says yes once this has arrived. */
@font-face {
  font-family: 'Riyal';
  src: url(riyal.woff2) format('woff2');
  font-display: block;
  /* 400, and ONLY 400, which is the correction. This said 100 900, which is a
     promise the file cannot keep: a browser told a face already covers 700
     uses it as-is and will NOT synthesize a bold, so asking the mark to be
     heavier did exactly nothing.

     Declaring the one weight it has puts synthesis back on the table. That is
     the right answer here rather than shipping a heavier cut, because there
     isn't one: Google's variable 400-700 subset of this face renders U+20C1
     IDENTICALLY at every weight - measured, 93,116 units of ink at 400, 500,
     600 and 700 alike, against an Inter control that moves 91,615 to 126,484.
     The axis does not reach this glyph. So a real bold does not exist to ship,
     and 272 bytes of variable machinery would buy nothing. */
  font-weight: 400;
  unicode-range: U+20C1;
}
/* The interface scale. It follows the SCREEN, and nothing else.

   It was a four-way setting - Compact, Default, Large, Larger - stored on the
   ACCOUNT, which is the flaw that made it unfixable: picking the size the car
   wanted necessarily picked it for the phone as well. There was never a value
   that suited both, so the setting could only ever be wrong on one of them.

   Two steps, and they are steps rather than a fluid clamp on purpose. Every
   measurement in this file is reasoned in exact rem against this multiplier -
   "3.5rem is the 52px floor" is a statement about a number - and a scale that
   slides with the viewport turns every one of those into an unknowable. Two
   values, each with a knowable px answer, is worth more here than smoothness
   nobody would see.

   BOTH axes have to be big, not just the width, and that is the trap this
   repo has already fallen into once: a landscape phone is 844x390 and sails
   past any width-only test. The car is 1921x1141 and clears both with room to
   spare; a phone fails on width standing up and on height lying down.

   1.26 up there because the car asked for it - measured on the real screen,
   where the largest of the four old sizes was simply right. 1 down here
   because that was the old Default, which is the size the phone had before an
   account-wide preference dragged it up. */
:root { --ui: 1; }
@media (min-width: 1200px) and (min-height: 700px) { :root { --ui: 1.26; } }
html { font-size: calc(15px * var(--ui)); }
html, body {
  margin: 0; padding: 0; min-height: 100%;
  color: var(--fg);
  background: #000;
  font-family: 'Riyal', Inter, system-ui, sans-serif;
  line-height: 1.55;
  overscroll-behavior-y: none;
  text-rendering: optimizeLegibility;
  /* Pan in both directions, and nothing else. This is the declaration that
     actually stops pinch-zoom on iOS, which ignores user-scalable=no - and
     it is inherited down the tree, so no descendant can hand the gesture
     back. Rails still scroll sideways (pan-x) and pages still scroll
     (pan-y); only the zoom is gone. */
  touch-action: pan-x pan-y;
  /* Stops iOS inflating text when it decides a column is too narrow, which
     it does silently and which would fight the interface-size setting. */
  -webkit-text-size-adjust: 100%;
     text-size-adjust: 100%;
}
/* Kills the 300ms double-tap wait on anything tappable. */
button, a, [role="button"], .card, .epc { touch-action: manipulation; }
/* body only - naming html here too would resolve 1rem against the root's own
   parent and quietly undo the calc above. */
body { font-size: 1rem; }
/* Depth without hue. A pure-black vignette plus the faintest grain: large
   flat blacks band badly on an 8-bit panel, and a monochrome scheme has no
   colour variation to hide it. Fixed layers, no layout cost. */
body::before {
  content: ''; position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background: radial-gradient(130% 95% at 50% 0%, rgba(255,255,255,.028), transparent 58%);
}
/* --grain is on :root so anything that has to paint an OPAQUE ground over this
   layer can put the texture back rather than leaving a smooth hole in it. The
   sticky month head that needed it went with the month grid; the token stays
   because the next opaque surface over this layer will need it too. */
body::after {
  content: ''; position: fixed; inset: 0; z-index: 0; pointer-events: none;
  opacity: .022;
  background-image: var(--grain);
}
#app { position: relative; z-index: 1; }

/* Badge chips are interface, not artwork, so they lose their colour with
   everything else. The posters behind them keep theirs. */
/* The rule that used to sit here desaturated badge chips: `.row .chip img`.
   Neither class is applied anywhere in app.js - every row in the client is
   .ed-row, .cfm-row, .nu-row, .nl-row or .sr-row, and nothing is a bare
   .chip - so it has matched nothing for as long as those names have been
   what they are. The manifesto above still describes the intent, and
   .vb img is where it is actually carried out. */

img { display: block; }
button {
  font: inherit; color: inherit; border: 0; background: none;
  cursor: pointer; padding: 0;
}
:focus { outline: none; }
:focus-visible {
  outline: var(--ring-w) solid var(--ring-focus); outline-offset: var(--ring-w);
  /* On the scale, not a loose 6px - the ring has to follow the corner of the
     thing it is ringing, and every rounded thing here uses --r-s/-m/-l. */
  border-radius: var(--r-s);
}
::selection { background: rgba(255, 255, 255, .26); }

/* Thin, quiet scrollbars everywhere */
* { scrollbar-width: thin; scrollbar-color: var(--line-hi) transparent; }
*::-webkit-scrollbar { height: 6px; width: 8px; }
*::-webkit-scrollbar-thumb { background: var(--line-hi); border-radius: 99px; }
*::-webkit-scrollbar-track { background: transparent; }

/* ---------------------------------------------------------------- motion -- */
@keyframes rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
@keyframes fadein { from { opacity: 0; } to { opacity: 1; } }
@keyframes spin { to { transform: rotate(360deg); } }
/* A band of light travelling across, moved with a transform rather than by
   animating background-position. The old way repainted the whole element every
   frame; this one moves a layer that is already painted, which is the
   difference between a repaint and a composite - and on the car, which
   rasterises in software, that is the difference worth having. */
/* ---- the two motions a placeholder has ----

   DRIFT is the only motion a placeholder has. There was an ORBIT here too,
   rotating a ring of blobs around the card, and it is gone: a circular path
   inside a rectangular card draws a visible oval, which is a ring painted on
   the card rather than an edge that glows. The edge is an inset shadow now and
   does not move at all.

   DRIFT is the line, and it does have ends, so it keeps its envelope: opacity
   zero at both, which doubles as the reduced-motion rest pose. Reduced motion
   zeroes duration and iteration count, so the last keyframe is what the
   element wears for ever - invisible for the line, and for the glow a single
   still frame of it, which is a soft coloured edge that does not move. That is
   a better resting state than nothing, and it is free. */

@keyframes drift {
  0%   { opacity: 0; transform: translate3d(-78%, -24%, 0); }
  5%   { opacity: 1; }
  95%  { opacity: 1; }
  100% { opacity: 0; transform: translate3d(78%, 24%, 0); }
}



/* THE WAY IN IS A CLASS, NOT AN ANIMATION, and the reason is measured rather
   than stylistic.

   It was @keyframes toastin, and it could not carry a fill-mode: the end state
   has to hand the element back to the DECLARED transform, the one that knows
   about --i, or a toast would keep asserting the i=0 pose and never step back
   when the next one arrived. That constraint was real and the reasoning was
   right.

   But a from-0 keyframe with no fill-mode has a failure mode that a transition
   does not: if the frame clock never ticks, the animation is pinned at 0% -
   which here is opacity 0 - and the toast is INVISIBLE rather than merely
   unanimated. Measured: three toasts, playState "running", currentTime 0, all
   at opacity 0, in a context producing no frames. That context is not
   hypothetical on a car that rasterises in software.

   A class inverts the failure. .enter is the pre-state with its transition
   switched off; it is applied, forced to layout, and removed in the same task
   - the same shape transitions.dev's banner-stacking uses, and for the same
   reason, since a rAF hop is skipped when the clock is throttled. If the clock
   never ticks at all, the class is still gone and the element simply computes
   to its resting style, which is visible and depth-correct. The worst case
   stops being an invisible toast and becomes a toast that did not slide.

   (0,2,0), so it outranks .toast's own transform and opacity. */
.toast.enter {
  transition: none;
  opacity: 0;
  transform: translateX(-50%) translateY(12px) scale(.96);
}
/* And the way out. A toast used to be removed from the document mid-sentence,
   which reads as a glitch rather than as a window closing - worst on the undo
   toast, which holds a real button for six seconds and then is simply not
   there. Shorter than the way in: arriving is an announcement, leaving is
   not. */
@keyframes toastout {
  to { opacity: 0; transform: translateX(-50%) translateY(8px) scale(.97); }
}
.toast.out { animation: toastout .18s var(--ease) both; pointer-events: none; }

/* The one orchestrated moment: each screen's children arrive in sequence.
 *
 * Retimed from .55s with delays out to .32s, which meant the sixth child was
 * not settled until 870ms after every navigation - and `both` fill held it at
 * opacity 0 for the first 320ms of that, so a third of the wait was spent
 * looking at nothing.
 *
 * Changing a screen is the single most frequent thing anyone does in this
 * app, and it was carrying its longest animation. The rule that decides it:
 * frequency of use sets duration, and a UI animation should generally stay
 * under 300ms. Rare things can afford to be slow; the thing you do a hundred
 * times cannot.
 *
 * The orchestration survives - children still arrive in order, the sequence
 * still reads - it just stops being something you sit through. Safe to
 * retime: nothing listens for animationend and no timer is coupled to it. */
/* ---- the measure, applied once ----
   Every screen is exactly two elements: a fixed .top, and one page body
   carrying .body-clear or .body-flush. So the measure goes on the page body,
   and nothing inside it is ever capped again.

   That is the whole point. While each block carried its own max-width, any
   block that was forgotten - .qh-query, .ix-h, .hs-h, .stand, .shead - kept
   the viewport left edge while its siblings moved to the centred column, and
   the two drifted further apart the wider the screen got. There is no list to
   keep in sync now: one element owns the measure, everything else fills its
   parent. */
.screen > .body-clear,
.screen > .body-flush {
  max-width: var(--measure);
  margin-inline: auto;
}

.screen > * { animation: rise .3s var(--ease) both; }
/* Except the bar, which no longer arrives - it stays. It used to lift 14px and
   fade on EVERY navigation, which is the "the whole thing redraws under you"
   feeling in its most literal form: the one element that is meant to be the
   fixed point of the app was the most obviously animated thing on it. It is
   also the element carrying two backdrop-filter pseudo-elements, so not
   animating its opacity is a straight win on a machine that rasterises in
   software. */
/* Neither does the nav, on a phone, where it is a child of .screen rather than
   of the header. Two reasons and the second is a bug: it is the same persistent
   element and re-animating it on every navigation is the thing this rule
   exists to stop - and `rise` ends on `transform: none` with a `both` fill,
   which beats the translateX(-50%) that centres the bottom capsule. The
   capsule sat with its left edge on the middle of the screen until this line
   existed. */
/* And neither do the two corner buttons, for a harder reason than taste:
   `rise` ends on opacity 1 and transform none with a `both` fill, and a filled
   animation beats every ordinary declaration including inline styles. So while
   these were in the ladder, nothing could move or fade them - not :active,
   not the hide-while-the-panel-is-open rule below. They are fixed to the
   viewport and never move; there was nothing for an entry animation to do
   here anyway. */
.screen > .top,
.screen > .tabs,
.screen > .fab,
.screen > .topback { animation: none; }
.screen > :nth-child(1) { animation-delay: .02s; }
.screen > :nth-child(2) { animation-delay: .06s; }
.screen > :nth-child(3) { animation-delay: .10s; }
.screen > :nth-child(4) { animation-delay: .14s; }
.screen > :nth-child(5) { animation-delay: .18s; }
.screen > :nth-child(n+6) { animation-delay: .22s; }
/* .railSlot went with the Library tab's four rails; .discSlot is the one
   remaining user and it holds a single rail, so the three delays below it had
   nothing left to stagger. The ladder is kept as one line rather than folded
   into the .screen > * rule above, because a rail arriving into a slot that is
   already on screen is a different entrance from a screen laying itself out,
   and only the slot version carries `both` fill. */
.discSlot > .rail { animation: rise .3s var(--ease) both; }
.discSlot > .rail:nth-child(2) { animation-delay: .05s; }
.discSlot > .rail:nth-child(3) { animation-delay: .10s; }
.discSlot > .rail:nth-child(4) { animation-delay: .15s; }

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

.hero.skel { display: block; height: clamp(26rem, 56.25vw, 100vh); }
/* ---- and how it leaves ----
   .heroSlot had no rule of its own until now; it needs one, because the
   departing skeleton is positioned against it.

   The REAL hero stays in normal flow and the skeleton is the one lifted out.
   That is the opposite of the transitions.dev recipe, which absolutely
   positions both layers - and it is deliberate: the hero owns its own height
   (clamp(26rem, 56.25vw, 100vh) here, clamp(30rem, 64vh, 38rem) in the phone
   block), and taking it out of flow would collapse the slot and drag every
   row on the page up by a screen. The disposable layer is the one that can
   afford to be absolute.

   height: auto so `inset: 0` alone decides the size. Without it the skeleton
   keeps its own clamp and the two agree only by coincidence - they do agree
   today, at every width, but that is two expressions that have to be kept in
   step by hand, and this needs none.

   z-index because both are positioned - .hero is position: relative - and the
   hero comes LAST in the DOM, so paint order would otherwise put the answer
   on top of the placeholder that is meant to be covering it.

   (0,4,0), which is what it takes to beat `.heroSlot .hero` in the phone
   block at (0,2,0); that rule sets the height this one has to override. */
.heroSlot { position: relative; }
.heroSlot > .hero.skel.handover {
  position: absolute; inset: 0; height: auto; z-index: 2;
  /* It is a picture of something that has already arrived. It must not take a
     press meant for the thing underneath it. */
  pointer-events: none;
  transition: opacity .4s ease-in-out;
}
.heroSlot > .hero.skel.handover.gone { opacity: 0; }
/* A rail standing in for one still loading. The heading is real and reads at
   full strength - it is true, and it is the part that tells you what is
   coming. The cards under it are holes, so they sit back. */
.skelrail .track { pointer-events: none; }
.skelrail h2 { opacity: .72; }
/* A heading that is not knowable yet. Sized by a real non-breaking space
   rather than by a height, so it is exactly the line the words would have
   occupied at whatever font-size the breakpoint is using. */
.skelh { display: inline-block; width: 9rem; border-radius: var(--r-s); }
.skelrow { pointer-events: none; }
/* Placeholder blocks - a rail of these stands in for a row still loading. */
.skel {
  position: relative; overflow: hidden;
  /* #222226, which is the tile the organic-shimmer recipe is designed against
     - its blobs run at .11 to .14 alpha and simply do not read on the .045
     white this used to be, which is about three times darker. Stated as a
     solid rather than a white overlay so a placeholder is the same surface
     wherever it sits, which is what the recipe assumes. */
  background: #222226;
  border-radius: var(--r-s);
}
/* AND THE HOSTS THAT OUTRANK IT. .skel is one class, so any component rule
   that sets its own background beats it - .listcard and .pickcard both carry
   var(--elev), and .epsec .epc-art carried var(--bg2) at (0,2,0). The blobs
   run at .11 to .14 alpha and do not read on either, which is how a hole can
   be shimmering correctly and still look flat and empty. Restated wherever a
   picture-shaped placeholder has a ground of its own; grep this comment
   before adding another. */
.listcard.skel, .pickcard.skel { background: #222226; }
/* ---- the shimmer, on the diagnostics page ----

   Real .skel and .artbox elements at the shapes the app draws, laid out so
   they can be looked at side by side and compared. This exists because the
   shimmer is the one thing on that page that is not reportable: whether it is
   pretty is a judgement, and the two screens it has to be pretty on - a car
   dashboard and a phone - are exactly the two this machine cannot see.

   The cells are given real sizes rather than aspect-ratio, because a
   placeholder that collapses to nothing here would look like the shimmer had
   failed when what failed was the demo.
*/
.shimdemo {
  display: flex; flex-wrap: wrap; align-items: flex-start;
  gap: 1.1rem; margin: .9rem 0 .4rem;
}
/* flex: none, because these cells exist to show a SHAPE. Left to shrink, a
   16:9 still turns into a tall narrow box on a phone and the label under it
   becomes a lie - which is exactly how it read on the first look. */
.shimdemo .shimcell { flex: none; }
.shimcell { display: flex; flex-direction: column; gap: .45rem; }
.shimcell > span {
  font-size: 0.7333rem; color: var(--dimmer); font-weight: 600; letter-spacing: .04em;
}
/* Fixed rem, not percentages, and the same at every width. These cells exist
   to show a SHAPE, so the one thing they must never do is take their size from
   whatever room is left: a percentage inside a flex: none cell resolves against
   a cell that is sizing to its own content, which collapsed the 16:9 still into
   a tall narrow box and made its label a lie. They wrap instead. */
.shimdemo .sd-still  { width: 13rem; height: 7.31rem; }
.shimdemo .sd-poster { width: 7rem; height: 10.5rem; }
.shimdemo .sd-bar    { width: 13rem; height: 1.1rem; }
/* .artbox is normally a frame around an <img> and gets its radius from the
   card it sits in; standing alone it needs both stated. The same tile as its
   neighbours, so the only difference on show is the shimmer rather than the
   surface underneath it - it looked broken beside them at the old .045. */
.shimdemo .artbox { border-radius: var(--r-s); background: #222226; }
.diag-note {
  margin: .5rem 0 0; max-width: 42rem;
  font-size: 0.8rem; line-height: 1.55; color: var(--dim);
}
@media (max-width: 700px) { .shimdemo { gap: .8rem; } }

/* ---- an edge that glows, and a line that crosses it ----

   Three goes at this missed the same thing, so it is worth writing down what
   the target actually is. Hisham named it in the end: the Siri glow. That is
   not a highlight travelling over a surface - it is a WIDE, SOFT, COLOURED
   LIGHT LIVING INSIDE THE BORDER, hugging the edge the whole way round and
   flowing along it. A thin bright line with a faint halo is a different
   object, and no amount of tuning turns one into the other.

   So there are two things here now, and the glow is the important one.

   THE GLOW is four big coloured blobs sitting near the edges of an oversized
   layer, and the layer rotates. Because the layer is 200% of the card and
   anchored at -50%, its 25% and 75% marks land exactly on the card's own
   edges - so blobs placed there ride around a path that follows the card's
   perimeter, and overflow: hidden keeps only the part that falls inside. The
   result is light that lives in the border and travels round it.

   Rotation is also why this one cannot break: 360 degrees is 0 degrees. There
   is no seam to hide.

   NO BLUR FILTER. A radial gradient with soft stops IS a blur, and it costs a
   rasterisation once rather than a filter pass per frame - which matters on a
   car that rasterises in software, and matters more because these appear six
   to a row. The softness here is entirely in the colour stops.

   THE LINE is the second thing and now clearly the lesser one: wider than
   before and much softer, crossing the glow rather than competing with it.
   It was the whole effect for three attempts and it was never going to be
   enough on its own.

   THE RING is a hairline, and it is under both - an inset shadow paints below
   the pseudo-elements - so the glow tints it from within as it passes. That
   is the "glows from the inside" part, and it needs nothing animated.
*/
/* ---- organic shimmer, from transitions.dev (Pro) ----

   Bought 2026-09-07 after five home-grown attempts, each of which I could
   describe and none of which looked right. What they were all missing is in
   the first two lines of the recipe: the band is WAVED BY AN SVG TURBULENCE
   FILTER. That is where "organic" comes from, and it is not something a
   gradient can be tuned into.

   The shape of it, so it is legible without reading every value:

   THE BAND is six coloured blobs behind a travelling mask - a 135-degree comet
   profile at 280% of the layer, slid corner to corner by mask-position. The
   mask is what makes the light a band; the blobs only supply its colour. That
   is inside out from every version I wrote, where the gradient WAS the band.

   THE WARP sits on the wrapper, not the band, and the split is load-bearing: a
   filter applies before a mask on a single element, so warping and masking the
   same node warps the mask's source rather than its result.

   THE EDGE BEAM is the same mask over a second stack - a 1px gradient ring, an
   inner glow at 2px of blur, and a bloom at 20px - so the border lights up
   exactly where the band is crossing it. Phase-locked because both animations
   run the same duration on the same clock, not because anything syncs them.

   WHAT WAS CHANGED, and only this:
   - the surface stays ours: .skel keeps its own grey and radius rather than
     their #222226 tile;
   - --shimmer-scale is set per placeholder, since their geometry is tuned for
     a 142px tile and ours run from a 97px rail card to a full-bleed hero;
   - the reduced-motion rule also clears the warp, so the band stops rather
     than freezing mid-displacement, which looks like damage.

   THE CAR IS THE OPEN QUESTION, and it should be said plainly: this is built
   on animated mask-position, an SVG displacement filter and a 20px blur -
   the three techniques this stylesheet has refused by name for a device that
   rasterises in software. It ships anyway, because five rounds of guessing
   produced nothing worth keeping and because the diagnostics page carries a
   live copy, so the question can be answered by looking rather than by
   prediction. If it is slow there the levers, in order, are
   --shimmer-glow-blur and then dropping .t-shimmer-edge. */
/* .shim is the marker: anything carrying it and the five child elements gets
   the real thing. .skel has it by rule, and a picture frame gets it from
   cardEl, so a placeholder does not have to be a skeleton to shimmer. */
.skel, .shim {
  --shimmer-dur: 3000ms;
  --shimmer-ease: ease-out;
  --shimmer-band-span: 26%;
  --shimmer-glow-blur: 20px;
  --shimmer-glow-strength: .7;
  --shimmer-ring-strength: 1;
  /* Their geometry is tuned for a 142px tile; a rail card's art is about 97px
     tall, which is where this default comes from. Overridden where a
     placeholder is a very different size. */
  --shimmer-scale: .7;
  isolation: isolate;
}
.hero.skel { --shimmer-scale: 2.4; }
/* A bar is about 16px tall and the edge beam insets by 20px x scale, so at any
   normal scale the ring, glow and bloom collapse through each other and the
   bar just flashes. A tenth puts that inset at 2px, which fits. */
.skel.sd-bar, .vd-sk { --shimmer-scale: .1; }
/* A picture frame is a card's art box - same size as a rail card's still. */
.artbox, .st-plate, .pface { --shimmer-scale: .7; }
/* A picture frame gets the BAND only. The edge beam is a 14px blur and a 1.4px
   blur on top of the band's own warp, and frames outnumber skeletons by more
   than twenty to one - 385 against 6 on a cold Home - so it is the one place
   where the difference between four layers and two is the difference between
   the effect running and the page stuttering. The warped colour band is what
   reads on a frame anyway; the beam is for a surface you are looking AT rather
   than one about to be covered by a poster. */
.artbox .t-shimmer-edge,
.st-plate .t-shimmer-edge,
.pface .t-shimmer-edge { display: none; }
.shimdemo .sd-still { --shimmer-scale: 1.1; }
.shimdemo .sd-poster { --shimmer-scale: .9; }

/* ---- nothing expensive runs until it is on screen ----

   A cold Home carries 385 of these at once, and every one was warping,
   blurring and repainting two animated masks whether or not it was anywhere
   near the viewport. That is where the frame rate went.

   So the layout and the colour are unconditional - a placeholder still looks
   like a placeholder the instant it exists - and the filter and the two
   animations only attach under .shim-on, which an observer puts on what is
   actually in view. Everything off screen costs a static gradient. */
.skel .t-shimmer, .shim .t-shimmer {
  position: absolute; inset: calc(-20px * var(--shimmer-scale));
  pointer-events: none;
}
/* A SKELETON is always on, and a picture frame is not. The difference is
   arithmetic rather than principle: a screen holds at most about a dozen
   skeletons - six in a rail, the hero, four in the diagnostics preview - and
   they are built as HTML strings with no element to hand an observer. Frames
   are built by cardEl one at a time and there are hundreds. A reserved row
   holding twenty-eight is hushed separately, above. */
.shim-on .t-shimmer, .skel .t-shimmer {
  filter: url(#t-shimmer-warp) blur(calc(5px * var(--shimmer-scale)));
}
/* ---- the warp is for skeletons, not for picture frames ----

   feTurbulence into feDisplacementMap is the most expensive thing in this
   recipe by a wide margin, and it is worse than its raw cost suggests: the
   animated mask lives INSIDE the filtered subtree, so the displacement field
   is not computed once and reused - it is recomputed on every frame of the
   sweep. WebKit is the least forgiving about that, which is what "short of
   smooth on my iPhone" is.

   So a frame keeps the blur and loses the warp. What is lost is the ripple
   along the band's edge; what is kept is the colour, the sweep and the soft
   edge - and a frame wears it for the second or two before its poster lands,
   which is not long enough to study a rippling boundary.

   Skeletons keep the whole thing. There are about a dozen at most, they are
   what you are actually looking at while a row loads, and a dozen displacement
   fields is a bill any of these devices can pay. */
/* .shim-on, not bare. Written unqualified first, and it put a filtered layer
   on all 385 frames at once - off-screen ones included - which is the very
   thing the gate exists to prevent. A blur is cheaper than a displacement
   field; four hundred of them are not cheap at all. */
.artbox.shim-on .t-shimmer,
.st-plate.shim-on .t-shimmer,
.pface.shim-on .t-shimmer { filter: blur(calc(5px * var(--shimmer-scale))); }
/* The diagnostics preview's frame is exempt from the gate, and deliberately.
   That page exists to be LOOKED at - it is the only way to see this effect on
   the car or the phone - so the one cell there must not depend on a scroll
   sweep having run. It is a single element on a page nobody keeps open. */
.shimdemo .artbox .t-shimmer { filter: blur(calc(5px * var(--shimmer-scale))); }
.shimdemo .artbox .t-shimmer-band {
  animation: t-shimmer-sweep var(--shimmer-dur) var(--shimmer-ease) infinite;
}
.skel .t-shimmer-band, .shim .t-shimmer-band {
  position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(ellipse 49.45% 38.46% at 20% 15%, rgba(40, 140, 255, .14), transparent),
    radial-gradient(ellipse 43.96% 32.97% at 65% 25%, rgba(255, 50, 100, .13), transparent),
    radial-gradient(ellipse 38.46% 43.96% at 30% 55%, rgba(50, 200, 80, .12), transparent),
    radial-gradient(ellipse 49.45% 38.46% at 75% 65%, rgba(180, 40, 240, .13), transparent),
    radial-gradient(ellipse 38.46% 32.97% at 45% 85%, rgba(255, 120, 40, .12), transparent),
    radial-gradient(ellipse 32.97% 32.97% at 10% 85%, rgba(30, 185, 170, .11), transparent),
    linear-gradient(rgba(90, 90, 100, .05), rgba(90, 90, 100, .05));
  -webkit-mask-image: linear-gradient(135deg,
    transparent 0%,
    transparent calc(50% - var(--shimmer-band-span) * .8),
    rgba(255, 255, 255, .25) calc(50% - var(--shimmer-band-span) * .45),
    rgba(255, 255, 255, .7) calc(50% - var(--shimmer-band-span) * .18),
    #fff 50%,
    rgba(255, 255, 255, .7) calc(50% + var(--shimmer-band-span) * .18),
    rgba(255, 255, 255, .25) calc(50% + var(--shimmer-band-span) * .45),
    transparent calc(50% + var(--shimmer-band-span) * .8),
    transparent 100%);
  mask-image: linear-gradient(135deg,
    transparent 0%,
    transparent calc(50% - var(--shimmer-band-span) * .8),
    rgba(255, 255, 255, .25) calc(50% - var(--shimmer-band-span) * .45),
    rgba(255, 255, 255, .7) calc(50% - var(--shimmer-band-span) * .18),
    #fff 50%,
    rgba(255, 255, 255, .7) calc(50% + var(--shimmer-band-span) * .18),
    rgba(255, 255, 255, .25) calc(50% + var(--shimmer-band-span) * .45),
    transparent calc(50% + var(--shimmer-band-span) * .8),
    transparent 100%);
  -webkit-mask-size: 280% 280%; mask-size: 280% 280%;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: 100% 100%; mask-position: 100% 100%;
}
.shim-on .t-shimmer-band, .skel .t-shimmer-band {
  animation: t-shimmer-sweep var(--shimmer-dur) var(--shimmer-ease) infinite;
}
.skel .t-shimmer-edge, .shim .t-shimmer-edge {
  position: absolute; inset: calc(-20px * var(--shimmer-scale));
  z-index: 1; opacity: var(--shimmer-glow-strength); pointer-events: none;
  -webkit-mask-image: linear-gradient(135deg,
    transparent 0%,
    transparent calc(50% - var(--shimmer-band-span) * 1.4),
    rgba(255, 255, 255, .06) calc(50% - var(--shimmer-band-span) * 1),
    rgba(255, 255, 255, .18) calc(50% - var(--shimmer-band-span) * .6),
    rgba(255, 255, 255, .45) calc(50% - var(--shimmer-band-span) * .25),
    #fff 50%,
    rgba(255, 255, 255, .5) calc(50% + var(--shimmer-band-span) * .18),
    transparent calc(50% + var(--shimmer-band-span) * .35),
    transparent 100%);
  mask-image: linear-gradient(135deg,
    transparent 0%,
    transparent calc(50% - var(--shimmer-band-span) * 1.4),
    rgba(255, 255, 255, .06) calc(50% - var(--shimmer-band-span) * 1),
    rgba(255, 255, 255, .18) calc(50% - var(--shimmer-band-span) * .6),
    rgba(255, 255, 255, .45) calc(50% - var(--shimmer-band-span) * .25),
    #fff 50%,
    rgba(255, 255, 255, .5) calc(50% + var(--shimmer-band-span) * .18),
    transparent calc(50% + var(--shimmer-band-span) * .35),
    transparent 100%);
  -webkit-mask-size: 280% 280%; mask-size: 280% 280%;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: 100% 100%; mask-position: 100% 100%;
}
.shim-on .t-shimmer-edge, .skel .t-shimmer-edge {
  animation: t-shimmer-sweep var(--shimmer-dur) var(--shimmer-ease) infinite;
}
@keyframes t-shimmer-sweep {
  0%   { -webkit-mask-position: 100% 100%; mask-position: 100% 100%; }
  100% { -webkit-mask-position: 0% 0%;     mask-position: 0% 0%; }
}
.skel .t-shimmer-edge-ring, .shim .t-shimmer-edge-ring,
.shim .t-shimmer-edge-glow,
.skel .t-shimmer-edge-bloom, .shim .t-shimmer-edge-bloom {
  position: absolute; inset: calc(20px * var(--shimmer-scale));
  border-radius: var(--r-s); pointer-events: none;
}
.skel .t-shimmer-edge-ring, .shim .t-shimmer-edge-ring {
  padding: calc(1px * var(--shimmer-scale));
  opacity: var(--shimmer-ring-strength);
  background:
    radial-gradient(ellipse 29.58% 16.90% at 33% -7.4%, rgba(255, 50, 100, .62), transparent),
    radial-gradient(ellipse 25.35% 14.79% at 12% -5%, rgba(40, 140, 255, .48), transparent),
    radial-gradient(ellipse 16.90% 29.58% at 2.1% 68.3%, rgba(50, 200, 80, .55), transparent),
    radial-gradient(ellipse 8.45% 14.79% at 2.1% 68.3%, rgba(30, 185, 170, .44), transparent),
    radial-gradient(ellipse 76.06% 13.38% at 74.4% 100%, rgba(100, 70, 255, .58), transparent),
    radial-gradient(ellipse 35.92% 11.27% at 55% 100%, rgba(40, 140, 255, .51), transparent),
    radial-gradient(ellipse 30.99% 13.38% at 93.9% 0%, rgba(255, 120, 40, .65), transparent),
    radial-gradient(ellipse 11.27% 17.61% at 100% 27.1%, rgba(240, 50, 180, .5), transparent),
    radial-gradient(ellipse 21.83% 20.42% at 100% 27.1%, rgba(180, 40, 240, .56), transparent),
    linear-gradient(rgba(90, 90, 100, .22), rgba(90, 90, 100, .22));
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask-composite: exclude;
}
.skel .t-shimmer-edge-glow, .shim .t-shimmer-edge-glow {
  background:
    radial-gradient(ellipse 27.46% 14.79% at 33% -7.4%, rgba(255, 50, 100, .34), transparent),
    radial-gradient(ellipse 23.24% 12.68% at 12% -5%, rgba(40, 140, 255, .28), transparent),
    radial-gradient(ellipse 14.79% 27.46% at 2.1% 68.3%, rgba(50, 200, 80, .3), transparent),
    radial-gradient(ellipse 6.34% 12.68% at 2.1% 68.3%, rgba(30, 185, 170, .25), transparent),
    radial-gradient(ellipse 73.24% 11.97% at 74.4% 100%, rgba(100, 70, 255, .32), transparent),
    radial-gradient(ellipse 33.80% 9.15% at 55% 100%, rgba(40, 140, 255, .28), transparent),
    radial-gradient(ellipse 28.87% 11.97% at 93.9% 0%, rgba(255, 120, 40, .35), transparent),
    radial-gradient(ellipse 9.15% 16.20% at 100% 27.1%, rgba(240, 50, 180, .28), transparent),
    radial-gradient(ellipse 19.72% 18.31% at 100% 27.1%, rgba(180, 40, 240, .3), transparent),
    radial-gradient(ellipse 25.35% 25.35% at 0% 0%, rgba(90, 90, 100, .14), transparent 70%),
    radial-gradient(ellipse 25.35% 25.35% at 100% 0%, rgba(90, 90, 100, .14), transparent 70%),
    radial-gradient(ellipse 25.35% 25.35% at 0% 100%, rgba(90, 90, 100, .14), transparent 70%),
    radial-gradient(ellipse 25.35% 25.35% at 100% 100%, rgba(90, 90, 100, .14), transparent 70%);
  box-shadow: inset 0 0 calc(14px * var(--shimmer-scale)) calc(1px * var(--shimmer-scale)) rgba(90, 90, 100, .12);
  filter: blur(calc(2px * var(--shimmer-scale)));
  -webkit-mask-image:
    linear-gradient(#fff, transparent calc(44px * var(--shimmer-scale)), transparent calc(100% - 44px * var(--shimmer-scale)), #fff),
    linear-gradient(to right, #fff, transparent calc(44px * var(--shimmer-scale)), transparent calc(100% - 44px * var(--shimmer-scale)), #fff);
  -webkit-mask-composite: source-over;
  mask-image:
    linear-gradient(#fff, transparent calc(44px * var(--shimmer-scale)), transparent calc(100% - 44px * var(--shimmer-scale)), #fff),
    linear-gradient(to right, #fff, transparent calc(44px * var(--shimmer-scale)), transparent calc(100% - 44px * var(--shimmer-scale)), #fff);
  mask-composite: add;
}
.shim .t-shimmer-edge-bloom {
  background:
    radial-gradient(ellipse 38.73% 21.83% at 33% -7.4%, rgba(255, 50, 100, .4), transparent),
    radial-gradient(ellipse 33.10% 19.01% at 12% -5%, rgba(40, 140, 255, .34), transparent),
    radial-gradient(ellipse 21.83% 38.73% at 2.1% 68.3%, rgba(50, 200, 80, .38), transparent),
    radial-gradient(ellipse 98.59% 17.61% at 74.4% 100%, rgba(100, 70, 255, .4), transparent),
    radial-gradient(ellipse 46.48% 14.08% at 55% 100%, rgba(40, 140, 255, .35), transparent),
    radial-gradient(ellipse 40.85% 17.61% at 93.9% 0%, rgba(255, 120, 40, .44), transparent),
    radial-gradient(ellipse 28.17% 26.76% at 100% 27.1%, rgba(180, 40, 240, .38), transparent);
  box-shadow: inset 0 0 calc(var(--shimmer-glow-blur) * 3 * var(--shimmer-scale))
              calc(var(--shimmer-glow-blur) / 2 * var(--shimmer-scale)) rgba(90, 90, 100, .1);
  filter: blur(calc(var(--shimmer-glow-blur) * var(--shimmer-scale)));
  -webkit-mask-image:
    linear-gradient(#fff, transparent calc(26px * var(--shimmer-scale)), transparent calc(100% - 26px * var(--shimmer-scale)), #fff),
    linear-gradient(to right, #fff, transparent calc(26px * var(--shimmer-scale)), transparent calc(100% - 26px * var(--shimmer-scale)), #fff);
  -webkit-mask-composite: source-over;
  mask-image:
    linear-gradient(#fff, transparent calc(26px * var(--shimmer-scale)), transparent calc(100% - 26px * var(--shimmer-scale)), #fff),
    linear-gradient(to right, #fff, transparent calc(26px * var(--shimmer-scale)), transparent calc(100% - 26px * var(--shimmer-scale)), #fff);
  mask-composite: add;
}
/* The wrapper as well as the two animated layers: without clearing the filter
   the warp keeps its displacement and the band sits frozen mid-wave, which
   reads as damage rather than as stillness. */
@media (prefers-reduced-motion: reduce) {
  .skel .t-shimmer-band, .shim .t-shimmer-band,
  .skel .t-shimmer-edge, .shim .t-shimmer-edge { animation: none; }
  .skel .t-shimmer, .shim .t-shimmer { filter: none; }
}
/* A placeholder with no shimmer markup inside it - a text bar, a heading, a
   verdict rule - keeps the plain sweep it has always had. */
.skel:not(:has(.t-shimmer))::before {
  content: ''; position: absolute; top: -30%; left: -55%; width: 210%; height: 160%;
  background: linear-gradient(112deg,
              rgba(255, 255, 255, 0) 30%,
              rgba(255, 255, 255, .03) 41%,
              rgba(255, 255, 255, .14) 50%,
              rgba(255, 255, 255, .04) 59%,
              rgba(255, 255, 255, 0) 70%);
  animation: drift 3.4s linear infinite;
}
/* Both of these were written when .skel carried two shimmer pseudo-elements and
   a bar was too short to hold both. It carries none now - the organic shimmer
   is child ELEMENTS, and ::before is the plain sweep a bar falls back to - so
   turning it off leaves a bar with nothing at all, which is what it had. */

/* ---------------------------------------------------------------- and the
   same treatment for real pictures, for the gap between a frame appearing and
   the image landing in it. Posters come from TMDB and stills from a server on
   the end of a tunnel, so that gap is real - it was a dark hole before, which
   on a wall of them reads as a broken grid rather than as a loading one.
   .lit is put on the frame by imgSettled() the moment the image resolves,
   whether it loaded or failed. */
.artbox, .st-plate, .pface { position: relative; overflow: hidden; }
/* ONE layer here, not two, and not by choice: .artbox::after is already the
   card's outline and its ownership ring, so there is no second slot to put a
   core in. It gets the same diagonal, the same asymmetry and the same rest -
   just the wash and the core folded into a single gradient, which is a little
   flatter than a skeleton's and is the closest this slot allows. */
/* :not(:has(.t-shimmer)) is the whole point of this selector now. cardEl gives
   every frame the organic shimmer's markup, and without this guard the frame
   ran BOTH - the licensed shimmer and the plain sweep it was meant to replace,
   on top of each other, on 385 frames at once. The .skel rule below has always
   carried the guard; this one was widened to the shimmer and never given it. */
.artbox:not(:has(.t-shimmer))::before,
.st-plate:not(:has(.t-shimmer))::before,
.pface:not(:has(.t-shimmer))::before {
  content: ''; position: absolute; z-index: 1; pointer-events: none;
  top: -30%; left: -55%; width: 210%; height: 160%;
  /* One layer here - .artbox::after is already the outline and the ownership
     ring - so the wash and the core fold into a single gradient. Colourless on
     purpose: this one waits over artwork that is about to arrive, and the
     picture is the colour. */
  background: linear-gradient(114deg,
              rgba(255, 255, 255, 0) 30%,
              rgba(255, 255, 255, .03) 41%,
              rgba(255, 255, 255, .20) 50%,
              rgba(255, 255, 255, .065) 57%,
              rgba(255, 255, 255, 0) 70%);
  animation: drift 3.4s linear infinite;
}
.artbox.lit::before, .st-plate.lit::before,
.pface.lit::before { content: none; }
/* And the shimmer's own elements, for the same two reasons ::before had: a
   frame with its picture in it is not waiting, and a frame that will never
   hold one must not promise otherwise. */
.artbox.lit .t-shimmer, .artbox.lit .t-shimmer-edge,
.st-plate.lit .t-shimmer, .st-plate.lit .t-shimmer-edge,
.pface.lit .t-shimmer, .pface.lit .t-shimmer-edge { display: none; }
/* A frame with no picture in it is not waiting for one. A history plate with
   nothing to draw shows an initial instead - it has no <img> that will ever
   fire, so without this it would shimmer for as long as the screen is open
   and promise something that is never coming.

   THE SECOND :not() IS WEIGHT, NOT MEANING. Do not trim it back.
   This rule shipped without it and never fired once. :has() takes its
   argument's own weight, so the sweep rule above - .artbox:not(:has(.t-shimmer))
   - is (0,2,1) on a CLASS, while this was (0,1,2) on a TYPE. It lost every
   time, silently, and the frames it was written to quiet shimmered for ever:
   measured, four of ten cards on an airing season, plus five behind the front
   card of a still pile.
   It is a regression rather than an oversight. Before :not(:has(.t-shimmer))
   was added above - to stop 385 frames running the old sweep and the organic
   shimmer at once - the sweep rule was .artbox::before at (0,1,1) and this
   guard beat it. Fixing the double shimmer killed the never-shimmer guard, and
   the two are 29 lines apart.
   (0,2,2) now, which beats (0,2,1) outright rather than tying on source order
   the way .lit does. Same over-specification, for the same reason, as the
   index's own hole rule further down. */
.artbox:not(:has(img)):not(:has(.t-shimmer))::before,
.st-plate:not(:has(img)):not(:has(.t-shimmer))::before,
.pface:not(:has(img)):not(:has(.t-shimmer))::before { content: none; }
.artbox:not(:has(img)) .t-shimmer, .artbox:not(:has(img)) .t-shimmer-edge,
.st-plate:not(:has(img)) .t-shimmer, .st-plate:not(:has(img)) .t-shimmer-edge,
.pface:not(:has(img)) .t-shimmer, .pface:not(:has(img)) .t-shimmer-edge { display: none; }
/* The picture fades up rather than snapping in, so the handover from shimmer
   to image is one movement instead of two. */
.artbox img, .st-plate img, .pface img {
  opacity: 0; transition: opacity .35s var(--ease);
}
.artbox img.ld, .st-plate img.ld, .pface img.ld { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .artbox img, .st-plate img, .pface img { opacity: 1; transition: none; }
}

/* ---- the dot matrix ----
   Sixteen dots on a four-by-four grid pulsing as a diagonal wave, inside a
   container that snaps a quarter turn every cycle - so the wave enters from a
   different corner four times before the whole thing repeats. Four patterns
   out of one keyframe and one steps().

   steps(4) rather than a smooth rotation, deliberately: a grid that spins is a
   spinner again, and not being one is the entire point. It holds each
   orientation still and changes between them.

   Dots move opacity and transform; the turn is a transform. Nothing here wants
   a filter, which is the only reason it paints in the car at all. */
.mdl { display: grid; place-items: center; }
.mdl > span {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: .26rem;
  animation: mdlTurn 6.4s steps(4, end) infinite;
}
.mdl i {
  width: .3rem; height: .3rem; border-radius: 50%; background: currentColor;
  opacity: .18; animation: mdlDot 1.6s var(--ease) infinite;
}
/* Delay by row plus column, so the wave crosses corner to corner. The
   container's quarter turn is what makes that one wave into four. */
.mdl i:nth-child(1) { animation-delay: 0s; }
.mdl i:nth-child(2), .mdl i:nth-child(5) { animation-delay: .08s; }
.mdl i:nth-child(3), .mdl i:nth-child(6), .mdl i:nth-child(9) { animation-delay: .16s; }
.mdl i:nth-child(4), .mdl i:nth-child(7),
.mdl i:nth-child(10), .mdl i:nth-child(13) { animation-delay: .24s; }
.mdl i:nth-child(8), .mdl i:nth-child(11), .mdl i:nth-child(14) { animation-delay: .32s; }
.mdl i:nth-child(12), .mdl i:nth-child(15) { animation-delay: .4s; }
.mdl i:nth-child(16) { animation-delay: .48s; }
@keyframes mdlDot {
  0%, 55%, 100% { opacity: .18; transform: scale(.82); }
  22%           { opacity: 1;   transform: scale(1.18); }
}
@keyframes mdlTurn { to { transform: rotate(360deg); } }
/* Still, but not absent: the grid stays, at the brightness it spends most of
   its cycle at, so the space does not read as empty. */
@media (prefers-reduced-motion: reduce) {
  .mdl > span, .mdl i { animation: none; }
  .mdl i { opacity: .4; }
}

/* ----------------------------------------------------------------- boot -- */
.boot { display: grid; place-items: center; height: 100vh; }
/* WHEN THE BOOT SCREEN HAS SOMETHING TO SAY. The traps that add this live in
   index.html, inline and dependency-free, because a black page with four rows
   of dots and no console is what a failed module looks like on a phone.
   The dots go when the message arrives - they promise something is coming, and
   once this is on screen nothing is. */
.boot.failed .mdl { display: none; }
.bootfail {
  max-width: 26rem; padding: 0 1.5rem; text-align: center;
  font-size: .95rem; line-height: 1.5; color: var(--fg, #fff);
}
.bootfail p { margin: 0 0 .5rem; }
/* The message itself is the only thing here worth reading twice, so it is the
   only thing not dimmed. The location under it is for a photograph, not for
   the reader. */
.bootfail p:first-child { font-weight: 600; }
.bootfail p:nth-child(2) { opacity: .75; word-break: break-word; }
.bootfail p:nth-child(3) { opacity: .45; font-size: .8rem; word-break: break-all; }
.spin {
  width: 2.2rem; height: 2.2rem; border-radius: 50%;
  border: .12em solid var(--line-hi); border-top-color: #fff;
  animation: spin .8s linear infinite;
}

/* ---- spinner into a check ----
   One box, two occupants. The ring turns while the work is out, then drops
   away as the tick draws itself in behind it with stroke-dashoffset - one
   path, so the line follows the stroke the way a hand would.

   A button that says "Saving..." and then says "Apply" again has told you
   nothing about whether it worked, and the toast carrying the real answer is
   at the far end of the screen. */
/* ---- the watch editor ----
   A sheet listing every season of a show, each expandable into its episodes,
   where taps build a DRAFT and nothing is written until Apply.

   Built on .strmwrap/.strmdlg rather than on anything new, which is most of
   why it costs so little: that shell already carries the flat rgba backdrop
   with no backdrop-filter, sheetup/sheetdown on transform and opacity only,
   the safe-area padding so the last row clears a home indicator, and the two
   height clamps for a phone and for a car in landscape.

   No hue enters. Cyan means you narrowed a query and this asks none; amber
   means results are hidden and this hides nothing - it lists every season a
   show has. Selection is carried by FORM: a filled white disc with a black
   tick for watched, a hollow ring for not. The disc is .epc-mark's own shape
   at another size, so a tick means one thing in the grid and in here. */
.we-body {
  overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch; padding: 0 0 .3rem;
}
.we-row {
  display: flex; align-items: center; gap: .1rem;
  min-height: max(3.5rem, 52px);
  /* On the row, NOT on .stage-on/.stage-off. Declared on the staged classes
     the transition arrives in the same style change as the value it is meant
     to animate, and the shadow latches at its from-value - transparent, zero
     offset - and never travels. Measured: with the transition suppressed the
     same rule paints rgb(255,107,107) 3.36px inset, and with it declared on
     the state it stays rgba(0,0,0,0) 0px for ever. A transition has to be in
     force BEFORE the thing it transitions changes. */
  transition: box-shadow .18s var(--ease), background .18s var(--ease);
}
/* Between seasons, not between the episodes inside one, so the grouping is
   done by the rule and not by a box around anything. */
.we-sn + .we-eps { border-bottom: 0; }
.we-sn { border-top: 1px solid var(--line); }
.we-body > .we-grp:first-child > .we-sn { border-top: 0; }

/* ---- the box, and the two colours in this sheet ----
   Two hues enter here and nowhere else, and they earn it by saying something
   nothing else on the row can: WHICH WAY a change is going. Green is a play
   about to be recorded, red is one about to be deleted, and the difference
   between those two is the only thing in this sheet that cannot be undone.
   They are on the staged state only - the resting states stay white and grey
   like the rest of the app, so an untouched sheet is as monochrome as every
   other screen and colour appears exactly when there is something to lose.

   Deliberately not --good/--bad, which this app sets to #fff on purpose:
   those mean "confirmed" and "failed" everywhere else, and this is neither.
   Scoped to .we so nothing outside changes. */
.we {
  --we-add: #4ADE80;
  --we-del: #FF6B6B;
}
.we-box {
  flex: none; width: max(3.25rem, 48px); height: max(3.25rem, 48px);
  display: grid; place-items: center; background: none; border: 0; padding: 0;
  color: var(--fg);
}
.we-box i {
  position: relative; display: grid; place-items: center;
  width: 1.55rem; height: 1.55rem; border-radius: 99px;
  box-sizing: border-box;
  transition: background .18s var(--ease), box-shadow .18s var(--ease);
  box-shadow: inset 0 0 0 .1rem var(--line-hi);
}
.we-glyph { display: grid; place-items: center; }
.we-box svg.we-ring + .we-glyph { position: absolute; inset: 0; }
.we-glyph svg {
  width: .82rem; height: .82rem; opacity: 0;
  transform: scale(.6) rotate(-25deg);
  transition: opacity .2s var(--ease), transform .24s var(--pop);
}
.we-box.on i { background: #fff; box-shadow: inset 0 0 0 .1rem #fff; color: #000; }
.we-box.on .we-glyph svg { opacity: 1; transform: none; }

/* The progress ring, which replaces a single bar that stood in for every
   partial state - 2 of 13 and 12 of 13 drew the same glyph. An arc is the
   number itself, and stroke-dashoffset transitions, so it travels when a
   tick moves it. The season box carries it; an episode is on or off and has
   nothing to draw. */
.we-box .we-ring {
  position: absolute; inset: -.12rem; width: auto; height: auto;
  transform: rotate(-90deg); overflow: visible;
}
.we-box .we-trk, .we-box .we-arc {
  fill: none; stroke-width: 3.4; stroke-linecap: round;
}
/* A faint full circle behind the arc, so a part-watched season reads as a
   ring that is partly filled rather than a crescent floating on its own.
   Only under the arc - a complete or empty season has a disc outline from
   .we-box i and does not want a second circle on top of it. */
.we-box .we-trk { stroke: transparent; }
.we-sbox.part .we-trk { stroke: var(--line-hi); }
.we-box .we-arc {
  stroke: var(--dim);
  transition: stroke-dashoffset .42s var(--ease), stroke .18s;
}
.we-sbox.part i { box-shadow: inset 0 0 0 .1rem transparent; }
.we-sbox.part .we-arc { stroke: #fff; }
.we-box.on .we-arc { stroke: transparent; }

/* Staged. The ring and the disc take the direction's colour, so a season you
   are about to empty is red before you reach the confirmation that says so. */
.we-box.to-add i { background: var(--we-add); box-shadow: inset 0 0 0 .1rem var(--we-add); color: #06331a; }
.we-box.to-add .we-arc { stroke: var(--we-add); }
.we-box.to-del i { background: none; box-shadow: inset 0 0 0 .1rem var(--we-del); }
.we-box.to-del .we-glyph svg { opacity: 0; transform: scale(.6); }
.we-box.to-del .we-arc { stroke: var(--we-del); }

/* The press, and it is the only motion on a tap: a squash that settles with
   --pop's overshoot, on transform alone. */
@keyframes we-pop {
  0% { transform: scale(1); }
  38% { transform: scale(.86); }
  100% { transform: scale(1); }
}
.we-box.pop i { animation: we-pop .34s var(--pop); }
.we-box:active i { background: var(--elev2); }

.we-open {
  flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: .55rem;
  background: none; border: 0; padding: .35rem .6rem .35rem .1rem;
  text-align: left; color: var(--fg); min-height: max(3.5rem, 52px);
}
.we-open b { font-size: .95rem; font-weight: 800; letter-spacing: -.01em; }
.we-sub {
  font-size: .75rem; color: var(--dim); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* The season chip's own bar, restated rather than reused: the rules for it
   are scoped `.epsec .sn-bar` and this sheet is not inside .epsec, so the
   element rendered as an empty inline span. Same height, same track, same
   --p, so a season reads identically on the rail and in here. */
.we-open .sn-bar {
  flex: 1 1 3rem; max-width: 7rem; align-self: center;
  height: .28rem; border-radius: 99px; overflow: hidden;
  background: rgba(255, 255, 255, .16);
}
.we-open .sn-bar i {
  display: block; height: 100%; background: #fff;
  transform: scaleX(var(--p, 0)); transform-origin: left;
  transition: transform .2s var(--ease);
}
.we-chev {
  flex: none; width: .55rem; height: .55rem; margin-left: auto;
  border-right: .12rem solid var(--dimmer); border-bottom: .12rem solid var(--dimmer);
  transform: rotate(45deg); transition: transform .2s var(--ease);
}
.we-grp.open .we-chev { transform: rotate(-135deg); }

/* Episodes. One rhythm, and the numbers line up: a fixed numeral column and a
   fixed date column make true vertical rules down the sheet, so a half-watched
   season reads as a waterline - the discs stop and the rings begin - without
   anyone reading a number. */
.we-eps { display: none; }
.we-grp.open .we-eps { display: block; }
.we-ep .we-tog {
  flex: 1 1 auto; min-width: 0; display: flex; align-items: baseline; gap: .55rem;
  background: none; border: 0; padding: .3rem .6rem .3rem 0; text-align: left;
  color: var(--fg); min-height: max(3.5rem, 52px);
}
.we-n {
  flex: none; width: 2.1rem; font-size: .8rem; font-weight: 800;
  color: var(--dim); font-variant-numeric: tabular-nums;
}
.we-t {
  flex: 1 1 auto; min-width: 0; font-size: .875rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.we-tail {
  flex: none; font-size: .72rem; color: var(--dimmer);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.we-ep.soon .we-t, .we-ep.soon .we-n { color: var(--dimmer); }

/* Staged rows say so in the row, not only in the total at the foot. A left
   rule in the direction's colour, so the shape of an edit reads straight down
   the column - a block of green is a season going on, a block of red is one
   coming off. */
.we-row.stage-on { box-shadow: inset .2rem 0 0 var(--we-add); }
.we-row.stage-off { box-shadow: inset .2rem 0 0 var(--we-del); }
.we-ep.stage-off .we-t { text-decoration: line-through; color: var(--dim); }

/* Episodes arrive in order when a season opens. Opacity and transform only,
   40ms apart and capped at ten by the caller, and the whole thing is off
   under reduced motion by the global block that zeroes durations AND delays. */
@keyframes we-in {
  from { opacity: 0; transform: translateY(.35rem); }
  to { opacity: 1; transform: none; }
}
.we-eps.in > * { animation: we-in .26s var(--ease) both; animation-delay: calc(var(--i, 10) * 40ms); }

/* Clearing a season from inside it, naming what it does and how many. The
   season's own box does this too now; this row stays because it says the
   number out loud, which a box cannot. */
.we-clear {
  width: 100%; min-height: max(3.2rem, 48px);
  display: flex; align-items: center; gap: .5rem;
  background: none; border: 0; padding: .2rem .6rem .2rem 1.1rem;
  color: var(--dim); font-size: .8rem; font-weight: 700; text-align: left;
}
.we-clear:active { opacity: .7; }
.we-clear.on { color: var(--fg); }
.we-clear i {
  flex: none; width: 1.1rem; height: 1.1rem; border-radius: 99px;
  border: .1rem solid var(--line-hi); box-sizing: border-box;
}
.we-clear.on { color: var(--we-del); }
.we-clear.on i { background: var(--we-del); border-color: var(--we-del); }

/* The foot. Says what the draft costs before Apply is pressed, and Apply is
   dead until there is something to do. */
.we-foot {
  flex: none; display: flex; align-items: center; gap: .7rem;
  padding: .7rem var(--pad) calc(.7rem + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--line); background: var(--bg2);
}
.we-cost {
  flex: 1 1 auto; min-width: 0; font-size: .8rem; color: var(--dim);
  font-variant-numeric: tabular-nums;
}
.we-cost b { color: var(--fg); font-weight: 800; }
.we-cost .add { color: var(--we-add); }
.we-cost .del { color: var(--we-del); }
.we-foot .btn { flex: none; min-height: max(3.2rem, 52px); }
.we-foot .btn[disabled] { opacity: .4; }

@media (max-width: 700px) {
  .we-open .sn-bar { display: none; }
  .we-t { font-size: .84rem; }
  /* Four things do not fit across 375px: the title wrapped to two lines and
     pushed the show name into two more. The show name goes - this sheet is
     opened from that show's own page, so it is the one item on the line the
     reader cannot be in any doubt about. */
  .we .sd-scope { display: none; }
  .we .sd-hd { gap: .5rem; }
  .we .sd-n { margin-left: auto; }
}

/* ---- the success check ----
   Transitions.dev's "Success check", with its blur dropped: filter does not
   paint on the car, so the published version's fade-in-from-blur would be an
   effect that only ever happened on a desk. Opacity, rotate and a short bob
   are what is left, and those are the two things that rasteriser composites
   cheaply.

   The travel is 10px rather than the snippet's 40. It lands inside a button
   here, not alone on a page, so 40 starts the mark outside the pill.

   Not folded into .s2c above, which is a spinner MORPHING into a tick and is
   a different event: that one says "the thing you started has finished", this
   one says "it worked". The first is worth watching, the second is worth
   catching out of the corner of an eye. */
.okchk {
  display: inline-grid; place-items: center;
  width: 1.15em; height: 1.15em; margin-right: .35rem;
  transform-origin: center; opacity: 0;
}
.okchk svg { display: block; overflow: visible; width: 100%; height: 100%; }
.okchk svg path { stroke-dasharray: 21; stroke-dashoffset: 21; }
.okchk[data-state="in"] {
  animation: okchk-fade .5s cubic-bezier(.22,1,.36,1) forwards,
             okchk-turn .5s cubic-bezier(.22,1,.36,1) forwards,
             okchk-bob  .5s cubic-bezier(.34,1.35,.64,1) forwards;
}
.okchk[data-state="in"] svg path {
  animation: okchk-draw .5s cubic-bezier(.22,1,.36,1) 80ms forwards;
}
@keyframes okchk-fade { from { opacity: 0 } to { opacity: 1 } }
@keyframes okchk-turn { from { transform: rotate(80deg) } to { transform: rotate(0) } }
@keyframes okchk-bob  { from { translate: 0 10px } to { translate: 0 0 } }
@keyframes okchk-draw { to { stroke-dashoffset: 0 } }

/* The mark still arrives, it just does not travel to get here. The global
   reduced-motion block zeroes durations, which would leave the stroke
   half-drawn at dashoffset 21 - an unfinished tick is worse than no
   animation, so the path is completed outright. */
@media (prefers-reduced-motion: reduce) {
  .okchk { animation: none; opacity: 1; }
  .okchk svg path { animation: none; stroke-dashoffset: 0; }
}

.s2c {
  position: relative; display: inline-grid; place-items: center;
  width: 1.1em; height: 1.1em; vertical-align: -.16em;
}
.s2c > * { grid-area: 1 / 1; }
.s2c-ring {
  width: 1em; height: 1em; border-radius: 50%;
  border: .11em solid rgba(255, 255, 255, .28); border-top-color: currentColor;
  animation: spin .8s linear infinite;
  transition: transform .2s var(--ease), opacity .2s var(--ease);
}
.s2c-tick {
  width: 1.05em; height: 1.05em;
  stroke-dasharray: 22; stroke-dashoffset: 22;
  transform: scale(.6); opacity: 0;
}
.s2c.done .s2c-ring { transform: scale(.4); opacity: 0; animation: none; }
.s2c.done .s2c-tick {
  opacity: 1; transform: scale(1); stroke-dashoffset: 0;
  transition: stroke-dashoffset .34s var(--ease) .1s,
              transform .34s var(--pop) .06s,
              opacity .12s linear .06s;
}
@media (prefers-reduced-motion: reduce) {
  .s2c-ring { animation-duration: 1.4s; }
  /* The global rule zeroes durations and leaves delays alone, so these would
     survive as a stall - a tick appearing a beat after everything else has
     stopped moving. */
  .s2c.done .s2c-tick { transition-delay: 0s; }
}

/* ---- the shake ----
   Five crossings, each shorter than the last, on a curve that arrives hard and
   leaves soft. Small on purpose: this says "that field is wrong", not "the
   page is falling over". */
@keyframes shakeX {
  0%   { transform: translateX(0); }
  15%  { transform: translateX(-.44rem); }
  32%  { transform: translateX(.38rem); }
  50%  { transform: translateX(-.26rem); }
  68%  { transform: translateX(.16rem); }
  85%  { transform: translateX(-.07rem); }
  100% { transform: translateX(0); }
}
.shake { animation: shakeX .5s cubic-bezier(.36, .07, .19, .97) both; }
@media (prefers-reduced-motion: reduce) { .shake { animation: none; } }

/* ---- content arriving where a skeleton was ----
   No fill mode. This class is taken off again on a timer, and an animation
   still asserting its last keyframe at that moment would hand the element back
   to its declared state with a visible flick. The end state is the resting
   state anyway, so there is nothing to hold. */
.revealin { animation: revealIn .34s var(--ease); }
@keyframes revealIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  /* The stagger is an inline animation-delay, which the global reduced-motion
     rule does not touch - it zeroes durations only - so it would survive as a
     stall: content sitting invisible for a quarter second and then simply
     being there. */
  .revealin { animation: none; animation-delay: 0s !important; }
}

/* ---- 3D tilt, on a poster under a pointer ----
   Guarded by (hover: hover) and (pointer: fine), which is not a nicety: the
   car reports coarse and has no hover at all, so on the screen this app is
   mainly for, none of this exists. It is for the desk.

   The card leans toward the cursor and a glare crosses it. .artbox's two
   pseudo-slots are already the shimmer and the outline, so the glare is a real
   element - there was no third slot to take.

   --mx and --my are set from JS as the pointer moves; the resting values here
   are the centre, so a card that never gets a pointer is simply flat. */
@media (hover: hover) and (pointer: fine) {
  .card.tilt { perspective: 900px; }
  .card.tilt .artbox {
    --mx: .5; --my: .5;
    transition: transform .35s var(--ease);
    will-change: transform;
  }
  /* The lift is KEPT and the tilt is added to it, rather than replacing it.

     This has to be .card.tilt:hover and not .card.tilt, because .card:hover
     .artbox already sets translateY(-4px) with exactly the same specificity
     and sits two thousand lines further down - so a tilt declared on the
     resting selector loses to it silently and the card simply never leans.
     Three classes beats two, and composing the two transforms in one
     declaration is the only way both can be true at once anyway.

     .06s, not .35s: at rest this is the return home, but under a pointer it
     runs on every move, and a third of a second there is a card lagging the
     cursor. */
  .card.tilt:hover .artbox {
    transform: translateY(-4px)
               rotateX(calc((var(--my) - .5) * -7deg))
               rotateY(calc((var(--mx) - .5) * 9deg));
    transition: transform .06s linear;
  }
  /* And the press still reads as a press - .card:active .artbox would
     otherwise lose to the hover rule above for the whole time a finger is
     down. */
  .card.tilt:active .artbox { transform: translateY(-1px) scale(.98); }
  .tiltglare {
    position: absolute; inset: 0; pointer-events: none; z-index: 1;
    border-radius: inherit; opacity: 0;
    background: radial-gradient(38% 46% at calc(var(--mx) * 100%) calc(var(--my) * 100%),
                rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%);
    transition: opacity .25s var(--ease);
  }
  .card.tilt:hover .tiltglare { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .card.tilt .artbox { transform: none; transition: none; }
  .tiltglare { display: none; }
}

/* ---- icon swap ----
   Two glyphs in one grid cell, so the outgoing one leaves while the incoming
   one arrives and the box never reflows. The host needs place-items: center
   and a single cell; .tricon already had both, which is why it is the one this
   is used on.

   Scale and opacity only. The usual version of this blurs the outgoing glyph,
   and blur is the one thing the car's browser will not paint - it would be an
   effect that exists only on machines this app is not for. */
.iswap { display: grid; place-items: center; }
.iswap > .isw {
  grid-area: 1 / 1; display: grid; place-items: center;
  transform: scale(.55); opacity: 0;
  transition: transform .22s var(--ease), opacity .18s var(--ease);
}
.iswap > .isw.in { transform: scale(1); opacity: 1; }
/* The one that is leaving grows slightly as it goes, so the two do not read as
   the same object shrinking and returning. */
.iswap > .isw.out { transform: scale(1.3); opacity: 0; pointer-events: none; }
/* The first paint has nothing to cross-fade from and must simply be there. */
.iswap > .isw:only-child { transform: none; opacity: 1; transition: none; }
@media (prefers-reduced-motion: reduce) {
  .iswap > .isw { transition: none; transform: none; opacity: 1; }
  .iswap > .isw.out { display: none; }
}


/* ---- clearing an input, word by word ----
   A ghost of the text laid exactly over the field, whose words lift and fade
   in sequence. The field itself is already empty by the time this paints - it
   is decoration over a change that has happened, never a delay in front of one.

   white-space: pre and the copied font are what keep the ghost sitting on the
   same pixels as the text it replaces; without them it is a second, differently
   spaced copy sliding over the first. */
.dissolve {
  position: absolute; pointer-events: none; z-index: 3;
  white-space: pre; overflow: hidden;
  display: flex; align-items: center;
  box-sizing: border-box;
}
.dissolve i {
  font-style: inherit;
  animation: wordGo .34s var(--ease) forwards;
}
@keyframes wordGo {
  0%   { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-.5rem); }
}

/* ---------------------------------------------------------------- login -- */
.login { display: grid; place-items: center; min-height: 100vh; padding: var(--pad); }
.login form { width: min(21rem, 100%); display: flex; flex-direction: column; gap: .75rem; }
.login form > * { animation: rise .55s var(--ease) both; }
.login form > :nth-child(1) { animation-delay: .05s; }
.login form > :nth-child(2) { animation-delay: .12s; }
.login form > :nth-child(3) { animation-delay: .19s; }
.login form > :nth-child(4) { animation-delay: .26s; }
.login form > :nth-child(5) { animation-delay: .33s; }
.login form > :nth-child(6) { animation-delay: .40s; }
.login .mark {
  font-size: 0.7333rem; font-weight: 800; letter-spacing: .34em;
  color: var(--accent); text-transform: uppercase; margin-bottom: .2rem;
}
.login h1 { font-size: 1.9rem; font-weight: 800; letter-spacing: -.02em; margin: 0; }
.login p { margin: 0 0 .5rem; color: var(--dim); font-size: 0.8667rem; }
.login input {
  background: var(--bg2); border: .06em solid var(--line-hi); border-radius: var(--r-s);
  padding: .85rem 1rem; color: var(--fg); font: inherit;
  transition: border-color .2s var(--ease), background .2s var(--ease);
}
.login input:focus { border-color: var(--accent); background: var(--bg2); outline: none; }
.login button[type=submit] {
  background: var(--accent); color: var(--accent-ink); font-weight: 800;
  border-radius: 99px; padding: .85rem; letter-spacing: .01em;
  transition: transform .15s var(--ease), background .2s;
}
.login button[type=submit]:hover { background: var(--accent-hi); }
.login button[type=submit]:active { transform: scale(.97); }
.login .err { color: var(--bad); font-size: 0.8667rem; min-height: 1.2em; }

/* --------------------------------------------------------------- chrome -- */
/* A capsule floating ON the view, not a bar across the top of it.
 *
 * It used to span the screen and carry a gradient scrim down from the top
 * edge, because the tabs had no ground of their own and had to stay readable
 * over an arbitrary hero frame. That works, and it costs the top of every
 * picture in the app: a strip of every backdrop permanently darkened to hold
 * up five words. The capsule carries its own ground instead, so the scrim goes
 * and the picture behind it is the picture.
 *
 * Solid, not blurred. The car never paints backdrop-filter at all - the same
 * measurement the player's wait plate is built on - so a blur is added as an
 * enhancement underneath a ground that is already sufficient on its own,
 * rather than as the thing carrying the contrast.
 *
 * .5, and the number is the floor rather than a taste. It started at .72,
 * borrowed from the player's wait plate, and that plate sits over a paused
 * frame where nothing else competes; here it read as a black slab laid on the
 * artwork. The question is how light it can go while the CAR is still legible,
 * because the car has the ground and nothing else. Worst case is a near-white
 * frame behind it with no blur: at .5 the composite is 115 and white 800-weight
 * text on it is 4.7:1, which clears the 4.5:1 floor for normal-size text - and
 * this text is 0.8rem, so it is normal-size text and does not get to use the
 * 3:1 large-text floor. .45 would be 4.0:1 and fails. So .5 is as light as this
 * is allowed to be, not as light as it looked good at.
 *
 * width: max-content with a max-width is what makes it hug its contents and
 * still give way on a narrow screen: past the cap its children shrink. There
 * is not much left to shrink now that search is one icon rather than a field,
 * which is most of why this fits everywhere it has to. */
.top {
  position: fixed; top: .5rem; left: 50%; transform: translateX(-50%);
  z-index: var(--z-top);
  width: max-content; max-width: calc(100% - 2 * var(--pad));
  display: flex; align-items: center; gap: .9rem;
  padding: .3rem .55rem;
  min-height: var(--topH);
  border-radius: 999px;
  /* .5 was recorded here as a measured floor and the measurement was wrong -
     or rather it was right about the wrong text. Recomputed over a PURE WHITE
     frame with no blur, which is what the car gets:
        ground .5 over white          = rgb(128)
        --fg white text on it         = 3.31:1   (not the 4.7 the old note said)
        an UNSELECTED tab label on it = 1.15:1
     The second number is the one that matters and nobody had ever checked it,
     because the labels are not --fg: they were rgba(144,144,144,.62), a
     translucent grey that composites to rgb(138) against a rgb(128) ground.
     That is the same colour twice, and it is exactly what a bright sky behind
     the bar looked like.
     The ground and the label brightness trade against each other and only
     their RATIO has to hold. .62 with the labels at 78% white measured 4.53:1;
     .55 with them at 95% measures 4.48:1 - the same floor, with an eighth less
     black laid over the picture. Black is the expensive half of that trade
     because it is the half you see on every frame, and the labels cost
     nothing to brighten. */
  background: rgba(0, 0, 0, .55);
  border: .06em solid var(--line-hi);
  backdrop-filter: var(--glass);
  -webkit-backdrop-filter: var(--glass);
}
/* The two masked blur layers are gone with the scrim. They existed to fade a
   full-width bar into the picture below it, and a capsule has an edge instead
   of a fade - it needs no gradient to end on. They also never painted on the
   one device that matters most here. */

/* Screens whose first thing is a hero run under the header; everything else
   clears it. The class is set in chrome(), not guessed at with :has(), which
   is not safe to rely on in the browser this targets. */
.screen > .body-flush { padding-top: 0; }
.screen > .body-clear { padding-top: var(--clear); }
.top .brand {
  font-size: 0.8rem; font-weight: 800; letter-spacing: .32em;
  text-transform: uppercase; color: var(--fg);
  transition: color .2s;
}
.top .brand em { font-style: normal; color: var(--accent); }
.top .brand:hover { color: var(--accent-hi); }
/* Back, first in the bar. It was given a hairline to divide it from the tabs,
   on the argument that it is the only control there acting on the page rather
   than moving between pages. Taken out: it is already a filled disc among
   unfilled words, which says the same thing without drawing a line across a
   bar whose whole shape is one uninterrupted capsule. */
.top .backin { margin-right: .1rem; }
/* Back, as a corner circle - only where there is no bar to sit in, which is a
   phone. Same glass as both capsules, at the same .5 whose contrast floor was
   measured against a near-white frame with no blur, because this sits on
   artwork too. */
.screen > .topback {
  position: fixed; z-index: var(--z-corner);
  left: var(--pad); top: calc(.55rem + env(safe-area-inset-top, 0px));
  width: 3rem; height: 3rem; border-radius: 50%;
  display: grid; place-items: center; color: #fff;
  background: rgba(0, 0, 0, .55);
  border: .06em solid var(--line-hi);
  backdrop-filter: var(--glass);
  -webkit-backdrop-filter: var(--glass);
  transition: background .2s, transform .15s var(--ease);
}
.screen > .topback svg { width: 1.15rem; height: 1.15rem; }
.screen > .topback:active { transform: scale(.92); }
/* The nav names where you are and draws the rest as icons.
 *
 * Four words across the top is four words to read; three of them are places
 * you are not. The one you are in keeps its name and takes a filled pill, the
 * other three are their icon alone. It also buys back most of a header that
 * has to hold search, settings, refresh and sign-out as well.
 *
 * The animation is an `animation`, not a `transition`, and that is forced
 * rather than chosen: show() does app.replaceChildren(node) on every route
 * change, so the header is a brand new element each time. A transition has
 * nothing to transition FROM - the new tab is simply born active - and would
 * never once run. An animation on the mounted element replays on every
 * navigation, which is exactly the moment worth marking.
 *
 * Nothing here animates a width. The pill is a pseudo-element scaled on its X
 * axis and the label moves on opacity and translate, so the whole thing is
 * transform and opacity - the only two properties the car is guaranteed to
 * paint, and the only two that do not put layout back through a CPU
 * rasteriser sixty times a second. The tab is laid out at its final size
 * immediately; only the paint arrives late. */
.top .navtab {
  position: relative; isolation: isolate;
  display: inline-flex; align-items: center; gap: .45rem;
  font-size: 0.8rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  /* Light, not grey. --dimmer is rgba(144,144,144,.62) - a translucent grey
     that composited to within ten values of the capsule's own ground over a
     bright frame, so the words were the same colour as the bar they sat on.
     95% white on the .55 ground measures 4.48:1 over a pure white frame with
     no blur. It went up when the ground came down - see the note there. */
  color: rgba(255, 255, 255, .95);
  padding: .42rem .6rem; border-radius: 99px;
  transition: color .2s var(--ease);
}
.top .navtab .nt-i { display: grid; place-items: center; flex: none; }
.top .navtab .nt-i svg { width: 1.25rem; height: 1.25rem; display: block; }
/* Present for the accessible name and for measurement, absent to the eye.
   display:none would take it out of the accessibility tree too, and then the
   only label left is the aria-label - which is on the button already, but the
   pill has nothing to grow around. */
.top .navtab .nt-l { display: none; }
.top .navtab:hover { color: var(--fg); }

.top .navtab.on { color: #000; }
/* The tab no longer draws its own ground, and no longer animates itself in.
   It used to paint a ::before that scaled up from 42% every time the bar was
   built - which was every navigation - so the highlight was born at its
   destination and inflated. That reads as five separate controls each
   announcing itself. There is one lozenge now and it travels, which is the
   whole difference between this and what was here before.
   The three keyframes it used - navPill, navWord, navIcon - are deleted rather
   than left unreferenced. */
.top .navtab:active { transform: scale(.94); }
.top .navtab:first-child { margin-left: 0; }
@media (prefers-reduced-motion: reduce) {
  .top .navtab.on::before, .top .navtab.on .nt-l,
  .top .navtab.on .nt-i, .top .navtab { animation: none; transition: none; }
  .top .navtab:active { transform: none; }
}

/* pill links (Library tab shortcuts) */
.chipbtn {
  background: var(--elev); border: .06em solid var(--line-hi); border-radius: 99px;
  padding: .45rem 1rem; font-size: 0.8333rem; font-weight: 700; color: var(--fg);
  transition: background .2s, border-color .2s, transform .15s var(--ease);
}
.chipbtn:hover { background: var(--elev2); border-color: var(--accent); }
.chipbtn:active { transform: scale(.96); }
.chipbtn.on { background: #fff; color: #000; border-color: #fff; }
/* ---- the cast chips are a switch, so they get the switch's pill ----
   The same white lozenge that slides between the tabs on Discover and the
   review sort. The machinery, and why it is transform-and-width only and
   paints nothing the car cannot, is documented at .tabpill below; these are
   the declarations that hand it this particular row.

   position: relative is the mandatory one and there is no working around it.
   slidePill translates the pill by the selected chip's offsetLeft/offsetTop,
   and offsetLeft is measured from the offsetParent - with nothing positioned
   here the offsetParent is whatever positioned ancestor happens to be up the
   page, and the pill lands somewhere else entirely. .kindsw, .viewsw and
   .revsort carry the same declaration for the same reason.

   The colour goes into the transition list, and it is not in the base
   .chipbtn rule: with a pill gliding under it over .3s, a label that snaps to
   #000 the instant it is pressed is black text on a near-black chip until the
   pill arrives, and the label it left is light grey on white for just as long.
   .ktab has carried `color` in its transition since the pill was built, for
   exactly this. Not gated on .haspill, because the un-pilled row has a milder
   version of the same thing - a 200ms background fade under an instantly
   black label - and so does the person page's filmography row, which shares
   this class.

   Everything else is gated behind .haspill, which only slidePill adds. If the
   script never runs, .chipbtn.on keeps painting its own white background and
   this row is exactly what it is today: the effect is additive and its
   absence is the old design, not a broken one.

   THE OPAQUE GROUND is what stops the pill showing through, and it is the one
   thing .ktab did not need. The pill travels at z-index 0 UNDER the chips, and
   a .chipbtn is var(--elev) - 94.5% transparent - over an OPAQUE .06em
   var(--line-hi) border with a var(--fg) label. Over white that reads as a
   dark #333 ring around invisible text: #eaeaea on #ffffff is 1.2:1. A .ktab
   survives the same trip because its border is transparent and var(--dim) on
   white is 3.2:1. And this row is up to five chips - Everyone plus one per
   filled credit group - so Everyone to Producers drags the pill under THREE of
   them, on one line, for the whole .3s: three chips reading as selected at
   once, which the row-change cut in slidePill never sees.
   #0e0e0e and #1c1c1c are not new colours. They are var(--elev) and
   var(--elev2) composited over the page's own #000 by hand - 0.055 x 255 is
   14.0 and 0.11 x 255 is 28.05, and the browser resolves the chip's own
   translucent ground to rgb(14,14,14) either way. Written opaquely because a
   background-image layer would not interpolate and the fade to and from the
   selected state has to. They are wanted as COLOURS, not a shorthand reset,
   which is why nothing else is in these two declarations.
   :not(.on) rather than trusting source order, so the two grounds can never
   reach the selected chip whatever gets written after them - the hover would
   otherwise tie with the .on rule at four classes each and paint over the pill.
   The hover has to be restated at all because .castchips.haspill .chipbtn
   would outrank the plain .chipbtn:hover and silently delete the affordance.

   The border goes transparent rather than to zero, so its .06em stays
   reserved and nothing reflows when a chip is selected - the pill is sized
   from offsetWidth/offsetHeight, which are border-box, so it covers the
   chip's outer edge exactly, border included. */
.castchips { position: relative; display: flex; flex-wrap: wrap; gap: .5rem; margin: 0 0 .9rem; }
.castchips .chipbtn { transition: background .2s, border-color .2s, color .2s, transform .15s var(--ease); }
/* Above the pill, and no longer carrying a ground of its own: an absolutely
   positioned child paints in the positioned layer, over static in-flow
   content, no matter that it is the first child - without these the selected
   chip's label disappears behind a blank white lozenge. */
.castchips.haspill .chipbtn { position: relative; z-index: 1; }
.castchips.haspill .chipbtn:not(.on) { background: #0e0e0e; }
.castchips.haspill .chipbtn:not(.on):hover { background: #1c1c1c; }
.castchips.haspill .chipbtn.on { background: none; border-color: transparent; }

.morebtn {
  background: var(--elev); border: .06em solid var(--line-hi); border-radius: 99px;
  padding: .6rem 1.6rem; color: var(--fg); letter-spacing: .06em;
}
.morebtn:hover { background: var(--elev2); border-color: var(--accent); color: var(--fg); }
.top .icon {
  width: 2.3rem; height: 2.3rem; border-radius: 50%; display: grid;
  /* Same reason as the magnifier: these sit over a hero on every screen whose
     first thing is one, and a grey picked against black does not survive a
     bright frame. */
  place-items: center; color: rgba(255,255,255,.66);
  transition: background .2s, color .2s, transform .15s var(--ease);
}
.top .icon svg { width: 1.05rem; height: 1.05rem; }
.top .icon:hover { background: var(--elev2); color: var(--fg); }
.top .icon:active { transform: scale(.92); }
/* Sign out, armed. The first tap arms it and says so; the second signs out.
   A monochrome scheme cannot go red to warn, so it goes white - the button
   inverts, which is the loudest thing available here. */
.top .icon.arm {
  background: #fff; color: #000;
  animation: armpulse 1.1s ease-in-out infinite;
}
@keyframes armpulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255,255,255,.5); }
  60%      { box-shadow: 0 0 0 .35rem rgba(255,255,255,0); }
}
@media (prefers-reduced-motion: reduce) { .top .icon.arm { animation: none; } }

/* detail action buttons */
.btn.iconlabel { gap: .45rem; }
.btn.iconlabel svg { width: 1.05em; height: 1.05em; }
/* The icon sits in its own span so the two states can be swapped without
   disturbing the label beside it. */
.btn.iconlabel .tg-i { display: grid; place-items: center; flex: none; }
.btn.iconlabel .tg-i svg { display: block; }
/* On is a filled pill, not a tint. It used to be a colour change on the
   label of a dark pill, which on a phone, over a bright backdrop, was very
   nearly no signal at all - you could not tell whether "Watched" was telling
   you something or offering to do it. Filled reads at a glance and from
   across a car. The palette has no second hue to reach for, so the signal is
   fill against ghost - and the fill sits one step below Play's pure white,
   so the thing that starts playback still reads as the loudest thing here. */
.btn.iconlabel.on {
  background: var(--good-deep); color: var(--accent-ink);
  border-color: transparent; font-weight: 800;
}
/* Filling a solid shape reads as "on". Filling a shape that is DRAWN in
   outline just floods it: the eye became a black blob with no pupil. Anything
   marked i-stroke keeps its stroke and lets the pill carry the state. */
.btn.iconlabel.on svg { fill: currentColor; }
.btn.iconlabel.on svg.i-stroke { fill: none; }
.btn.iconlabel.on:hover { background: var(--accent); }
/* Watched, but only on Flick - so the button reads as watched and still acts
   as "record this", because PublicMetaDB is the only record it can write to.
   The state has to sit visibly between the two it is between.

   An OUTLINE of the on state, not a colour of its own. It draws the same eye
   and takes the same token as the filled pill, so it reads as the same family
   of answer; what it does not have is the fill, which is exactly the thing the
   filled state is claiming and this one cannot. The accent system here is
   closed - white is "you are here", cyan is a narrowed query, amber is hidden
   results - so a third meaning had to come out of form rather than a fourth
   hue, and a ring against a fill is the difference the shape can carry.

   inset box-shadow rather than a border, because a border would change the
   button's box and shift every button beside it by the width of the ring on
   the one title in twenty that is in this state. */
.btn.iconlabel.viaflick {
  background: transparent;
  box-shadow: inset 0 0 0 .11rem var(--good-deep);
  color: var(--fg);
}
.btn.iconlabel.viaflick svg { fill: currentColor; }
.btn.iconlabel.viaflick svg.i-stroke { fill: none; }
.btn.iconlabel.viaflick:hover { background: rgba(255,255,255,.13); }

/* ---- cast, similar and franchise rails inside the sheet: full bleed ----
   These are .track outside a .rail, sitting inside .detail .sheet's own
   var(--pad). That left all three inset at both ends: dead space in front of
   the first card, and the last card cut off a gutter short of the screen
   edge - while every rail on home runs to the edge and lets its own padding
   inset the CONTENT instead. Same trick as .rail .track, and as .trrail
   further down which has been doing it inside this very sheet all along:
   pull the box out by a gutter, hand that gutter back as padding.

   The arithmetic, because it is exact and that is not obvious. The containing
   block is the sheet's content box - width W - 2*pad, left edge at x = pad
   (nothing narrows it; none of .castSlot, .simSlot or .colSlot appears in
   either of this sheet's max-width lists). Minus pad each side makes the
   margin box W wide with its left edge at 0: EQUAL to .detail, never wider,
   so nothing here can put a horizontal scrollbar on the page. Plus pad of
   padding puts the content box back at x = pad. Measured against an 980px
   .detail at all three gutters the clamp can produce - 14.25px, 31.36px and
   51px - the rail comes out left 0 width 980 every time, the first card lands
   at exactly the gutter, and the .sec heading above it lands on the same
   number to the sub-pixel. Body overflow: zero at all three.
   It holds only because --pad is clamp(0.95rem, 3.2vw, 3.4rem) - VIEWPORT
   relative, so it is the same pixel count on the sheet and on the rail. It
   was a percentage below 900px once, container-relative, and against that
   this would have been off by whatever the two boxes' widths differed by.

   margin-inline and not a `margin` shorthand: .track already sets a negative
   margin-top for the lift room and a shorthand would silently drop it. Not
   hypothetical - .trrail's comment records that exact mistake being made
   there first.

   scroll-padding-left because the gutter now sits in front of the first card.
   .card is scroll-snap-align: start, so without it the first card snaps to
   the scrollport edge and hides under the padding, which is the bug .rail
   .track's own comment is about. The cast rail's .person cards carry no
   snap-align, so for that one the line is inert today - written anyway,
   because "inert until somebody adds snap-align" is how the next person
   inherits it instead of rediscovering it. */
.detail .castrail, .detail .simrail, .detail .colrail {
  padding-left: var(--pad); padding-right: var(--pad);
  margin-inline: calc(var(--pad) * -1);
  scroll-padding-left: var(--pad);
}
.person { flex: 0 0 auto; width: 6rem; cursor: pointer; text-align: center; }
/* The frame, so there is something to shimmer while the face is on its way -
   a pseudo-element cannot be hung on an <img>. It is also what holds the
   initial when there is no photograph at all. */
/* Portrait cards, not medallions. A circle crops a headshot's chin or its
   hair to fit a shape no photograph was composed for, and a rail of circles
   next to a rail of rectangular posters reads as two different apps. TMDB
   profiles are shot 2:3, the same as a poster - so they get a poster's frame. */
.pface {
  position: relative; overflow: hidden; display: block;
  width: 6rem; aspect-ratio: 2 / 3; border-radius: var(--r-s);
  background: var(--bg2); margin: 0 auto;
}
/* Every image in a face frame fills the frame, stated once on the frame's own
   selector - the frames hold three kinds of image now (headshots in the rail,
   the portrait in the header, posters on the timeline) and each had, or
   lacked, sizing of its own. One rendered at intrinsic size inside an
   overflow:hidden box and showed the top-left corner of a poster: a patch of
   sky where Fast X should be.
   Pinned to the frame's edges rather than sized 100%/100%, and that choice is
   the actual fix: the frame's height comes from aspect-ratio, and iOS Safari
   does not resolve a child's percentage height against an aspect-ratio-derived
   height - the header portrait rendered as a strip across the top of an empty
   frame on the phone. inset needs no percentages and no parent height at all;
   .pface is already position:relative for the shimmer. */
.pface img {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover; display: block;
}
.person img {
  /* Sizing comes from the .pface img rule; only the crop bias is this rail's
     own - headshots frame the face high, so a centred crop keeps forehead
     and loses chin. */
  object-position: 50% 18%;
  /* These used to be desaturated so they read as interface rather than as
     artwork. Two things were wrong with that. The car does not paint `filter`
     at all, so the one device that matters most was already showing them in
     colour while every other screen showed grey - the intent was never
     achieved where it counted. And a cast is the most human thing on the
     page; draining it is what made these look like a database rather than
     like people. */
  transition: outline-color .2s, filter .25s var(--ease);
  outline: .12em solid transparent;
}
.pface { outline: .12em solid transparent; transition: outline-color .2s; }
.person:hover .pface { outline-color: var(--ring-hover); }
/* And the same ring for a keyboard, on the face rather than round the whole
   card: the card is a 6rem column with two lines of text under it, and an
   outline round all of that reads as a box drawn on the page rather than as a
   thing that is selected. Only a rail whose faces open a page is focusable,
   so this can never appear on one that does nothing. */
.person:focus { outline: none; }
.person:focus-visible { outline: none; }
.person:focus-visible .pface {
  outline-color: var(--fg); outline-width: .16rem; outline-offset: .18rem;
}
.person .pn { margin-top: .4rem; font-size: 0.8rem; font-weight: 650; line-height: 1.25;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.person .pr { font-size: 0.7333rem; color: var(--dimmer); overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; }

/* browse pages */
/* .browsefilters had no base rule anywhere - no display, no gap - so it was a
   plain block and the chips inside it were held apart by collapsed whitespace,
   about 4px, while the phone block set flex-wrap and row-gap on a box that was
   not a flex container. (.qh-query overrides this to display: contents on the
   discover head, which still works: that rule comes later.) */
.browsefilters {
  display: flex; flex-wrap: wrap; align-items: center; gap: .45rem;
}
.browsehead {
  display: flex; align-items: flex-end; gap: 1rem; flex-wrap: wrap;
  padding: 1rem var(--pad) .6rem;
}
.browsehead h1 { font-size: clamp(1.5rem, 3.4vw, 2.2rem); font-weight: 800; letter-spacing: -.02em; margin: 0; }
.browsecount {
  font-size: 0.8rem; font-weight: 600; color: var(--dimmer);
  letter-spacing: .04em; margin-left: .5rem;
}
/* ---- films or shows ----
   Two tabs became one, so the thing that used to be the tab bar's job is
   done here instead. Beside the heading and not in the source rail below it:
   the rail answers "where does this list come from" and this answers "of
   what", which is the prior question and a different kind of question. Same
   shape as the genre band's switch at the foot of home, because it is the
   same control doing the same job. */
.kindsw { display: inline-flex; gap: .3rem; align-self: center; }
.ktab {
  min-height: 1.9rem; padding: 0 .8rem; border-radius: 999px;
  background: var(--elev); border: var(--rule-w) solid transparent;
  color: var(--dim); font: inherit; font-size: .7333rem; font-weight: 800;
  letter-spacing: .08em; text-transform: uppercase;
  transition: background .18s, color .18s, border-color .18s;
}
.ktab:hover, .ktab:focus-visible { background: var(--elev2); color: var(--fg); }
.ktab.on { background: #fff; color: #000; border-color: #fff; }
.ktab:active { transform: scale(.96); }

/* ---- the sliding pill ----
   One white pill that MOVES between the tabs, instead of each tab painting and
   unpainting its own background. Three switches here are the same control -
   the Films/Shows kind switch, the Posters/Details/Stills lens and the review
   sort - and the travel is what says the two options are one choice rather
   than two buttons.

   transform and width, nothing else. No blur, no hover, nothing the car cannot
   paint, which is what rules out most published versions of this effect.

   It only takes over once slidePill has measured a selected tab and set
   .haspill; until then, and if the script never runs, .ktab.on keeps painting
   its own white background exactly as before. The effect is additive and its
   absence is the old design, not a broken one. */
.kindsw, .viewsw, .revsort { position: relative; }
.tabpill {
  position: absolute; left: 0; top: 0; z-index: 0;
  border-radius: 999px; background: #fff;
  opacity: 0; pointer-events: none;
  transition: transform .3s var(--ease), width .3s var(--ease), opacity .2s var(--ease);
}
.haspill .tabpill { opacity: 1; }
/* Above the pill, and no longer carrying a ground of its own - the pill is the
   ground now, and two whites stacked would double-paint the corners. */
.haspill .ktab { position: relative; z-index: 1; }
.haspill .ktab.on { background: none; border-color: transparent; }
/* The hover ground would sit over the pill and grey it out. */
.haspill .ktab.on:hover, .haspill .ktab.on:focus-visible { background: none; color: #000; }

@media (prefers-reduced-motion: reduce) {
  .tabpill { transition: none; }
}
/* ---- the source rail ----
   Where the list comes from, beside the heading rather than under it: the h1
   already makes that row ~75px tall, so the rail costs a pixel instead of a
   whole row, and the grid keeps two full poster rows above the fold on the
   car.

   overscroll-behavior-x is what stops a sideways flick at the end of the rail
   firing the browser's back gesture, the same reason the season rail carries
   it. No fade at the edge: a mask-image is not trusted to paint on the car,
   and a chip clipped by the edge is a truer "there is more" than a gradient. */
.srcrail {
  display: flex; gap: .55rem; margin-left: auto;
  overflow-x: auto; scrollbar-width: none; overscroll-behavior-x: contain;
  max-width: 100%;
}
.srcrail::-webkit-scrollbar { display: none; }
.srcrail .chipbtn { white-space: nowrap; flex: none; }

/* ---- the shelf's doors ----
   The five screens that are next to the shelf without being on it, as one
   horizontally scrolling line that costs the same height at every width.

   .srcrail and not .browsefilters, and not an .ix-band. Not .browsefilters
   because that element holds browseScreen's Sort and Genre and Discover's
   tri-state genre chips - eight navigation buttons standing in it taught that
   navigating and filtering are one control, which is the exact mistake
   .viewsw's own comment says it exists to avoid. Not an .ix-band because two
   bands of doors above a poster wall cost about 242px at 375: five cells at
   the .ix-grid phone track of 7.6rem is two columns, so three rows of 3.6rem
   plus two headings. A rail of five chips is ONE line at every width, which
   is less than the three lines eight wrapping chips take on a phone today.

   Three declarations. .srcrail already carries the scroll, the hidden
   scrollbar, overscroll-behavior-x (which is what stops a sideways flick
   firing the browser's back gesture) and its deliberate refusal of an end
   fade - a chip clipped by the edge is a truer "there is more" than a
   gradient, and a mask-image is not trusted to paint on the car. And
   .srcrail .chipbtn is already in the car block's 3.47rem list, so every door
   is a 52px target there for free.

   margin-left is re-asserted because .srcrail's own is `auto`, for the case
   where it sits beside a heading; this one is a row of its own. The padding
   is the page gutter, which .srcrail never needed while it lived inside a
   .browsehead that had one. */
.srcrail.shelfdoors { margin-left: 0; padding: 0 var(--pad) .55rem; }
/* A door that states its size, in .kfilt b's recipe - the count sits back
   against the word at the same .55 and the same tabular figures, because a
   door reading "History 3,825 plays" is a door you can decide about without
   pressing it, which matters most on a screen read at arm's length. */
.srcrail.shelfdoors .chipbtn b {
  margin-left: .4rem; font-weight: 800; opacity: .55;
  font-variant-numeric: tabular-nums;
}


/* ---- the lens row ----
   How you are looking, under what you are looking for. Its own row rather
   than more chips in the filter row, because those narrow the QUERY and these
   two narrow what came back - one goes to TMDB and one never leaves the
   browser, and a row that mixes them teaches that they are the same thing. */
.viewsw { display: inline-flex; align-items: center; gap: .3rem; }
/* A mark, not a word. Square, so three of them read as one control rather
   than as three differently-sized pills. */
.vtab { padding: 0; width: 2.3rem; display: grid; place-items: center; }
.vtab svg { width: 1.05rem; height: 1.05rem; display: block; }

/* ---- Details ----
   One item per row, and the first view here to spend the overview and the
   genres, both of which have arrived with every result since this screen was
   written and had nowhere to go on a poster tile. Newspaper columns fall out
   of auto-fill: one at 375px, two on the car, three on a wide desktop. */
.rows {
  display: grid; gap: 0 2.2rem; padding: .2rem var(--pad) 2rem;
  grid-template-columns: repeat(auto-fill, minmax(23rem, 1fr));
}
.drow {
  display: grid; grid-template-columns: 7.4rem minmax(0, 1fr) auto;
  gap: .85rem; align-items: start; text-align: left;
  padding: .7rem .6rem; margin: 0 -.6rem;
  border-top: 1px solid var(--line);
  background: none; color: var(--fg); font: inherit;
  transition: background .16s;
}
.drow:hover, .drow:focus-visible { background: var(--elev); }
.drow:active { transform: scale(.995); }
.dr-shot {
  display: block; border-radius: var(--r-s); overflow: hidden;
  background: var(--bg2); aspect-ratio: 16/9;
}
.dr-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.dr-body { min-width: 0; display: flex; flex-direction: column; gap: .18rem; }
.dr-t {
  font-size: .95rem; font-weight: 800; letter-spacing: -.01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The standing line every listing in print has: when, and of what kind. */
.dr-m {
  font-size: .6667rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Two lines is the whole point: enough to tell what it is, not so much that
   the row stops being a row. */
.dr-p {
  font-size: .8rem; line-height: 1.35; color: var(--dim); margin-top: .1rem;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* A review column prints a score as a figure, never as a star. */
.dr-v { display: flex; flex-direction: column; align-items: center; gap: .05rem; padding-left: .2rem; }
.dr-v b { font-size: 1.05rem; font-weight: 800; font-variant-numeric: tabular-nums; }
.dr-v i {
  font-style: normal; font-size: .55rem; font-weight: 800;
  letter-spacing: .14em; color: var(--dimmer);
}


/* ---- Stills ----
   The same grid, turned on its side. .card.wide already carries 16/9, so this
   is a column width and nothing else. */
.grid.wideg { grid-template-columns: repeat(auto-fill, minmax(15.5rem, 1fr)); }

/* ---- what is happening with this one ----
   Above the title, small, and quiet enough that the picture is still the
   picture. It earns its place by being rare: most titles have nothing to say
   and show nothing at all, so a badge means something is genuinely going on
   rather than being chrome every hero wears.

   Its own element rather than a pseudo on .body - .hero .body already carries
   layout, and this file has spent an afternoon learning what happens when a
   new overlay takes a slot something else was using. */
.hbadge {
  /* Arrives after the hero ladder has already run for the meta line it sits
     in - see @keyframes arrive. */
  animation: arrive .22s var(--ease) both;
  display: inline-block; vertical-align: -.15em;
  margin: 0 .5rem 0 0; padding: .18rem .6rem; border-radius: 999px;
  background: rgba(0, 0, 0, .5);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .3);
  color: #fff; font-size: .7333rem; font-weight: 800;
  letter-spacing: .02em; white-space: nowrap;
}
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .hbadge { font-size: .8667rem; padding: .38rem .85rem; }
}
@media (max-width: 700px) { .hbadge { font-size: .6667rem; } }

/* ---- the title, on the picture ----
   In a band of its own along the bottom, and that band is a REAL element.
   Both pseudo-element slots on .artbox are already spoken for - ::before is
   the loading shimmer and ::after is the card's outline and hover ring - and
   I took each of them in turn for this scrim. The first left every still
   shimmering for as long as the screen was open; the second moved the card's
   outline to 45% down the frame and drew a line across the picture. A third
   guess was not the answer; a span is.

   Small and low, because the point of a still is the frame: a wordmark blown
   across the middle of it is a poster with extra steps. Capped on both axes
   so a long mark is stopped by the width and a stacked one by the height,
   which leaves the landscape intact either way. */
.cwm {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  height: 55%; display: flex; align-items: flex-end;
  padding: 0 1rem .9rem;
  background: linear-gradient(to top, rgba(0, 0, 0, .5), transparent);
  pointer-events: none;
}
.cwordmark {
  max-height: 36%; max-width: 40%; width: auto; height: auto;
  object-fit: contain; object-position: left bottom;
}

/* ---- the mark ----
   White on black, like every other selected or asserted thing here. Top-left
   because the bottom-right of a poster is where the progress bar lives. */
.cmark {
  position: absolute; top: var(--mark-inset, .4rem); left: var(--mark-inset, .4rem); z-index: 2;
  padding: .12rem .38rem; border-radius: var(--r-s);
  background: #fff; color: #000;
  font-size: .5667rem; font-weight: 800; letter-spacing: .08em;
  text-transform: uppercase; pointer-events: none;
  /* Never broken across lines. Inside the details row it sits in the title's
     own text flow, so at 375px it split into "IN" above "LIBRARY" - two
     half-pills reading as damage rather than as a label. */
  white-space: nowrap;
}
.dr-lib { position: static; margin-left: .45rem; vertical-align: .06em; }
/* Finished is a filled pill; part-way through is the same pill hollowed out.
   Two states from one accent, and the difference reads at a glance without a
   second colour or a second shape.

   Nothing is dimmed. Fading a watched title treats having seen it as a reason
   to look away, and a discovery screen full of half-faded artwork is a
   discovery screen that has decided for you. */
.cmark.part {
  /* A ground, not a hue. White type on a hollow pill reads perfectly against
     the near-black page and not at all against the bright half of a film
     still - and this pill sits on arbitrary photography by definition. The
     TMDB images are cross-origin so a canvas cannot measure the luminance
     under it; a scrim removes the question instead of sampling it. */
  background: rgba(0, 0, 0, .62); color: #fff;
  box-shadow: inset 0 0 0 var(--rule-w) rgba(255, 255, 255, .6);
}
/* Inside a details row there is no picture beneath it, so it needs no ground
   and the outline alone does the work. */
.dr-lib.part { background: none; }

/* The empty state offers the filters themselves, not advice about them. */
.emptydrop { display: flex; flex-direction: column; align-items: center; gap: .7rem; }
.ed-row { display: flex; flex-wrap: wrap; gap: .45rem; justify-content: center; }


/* ================= the Discover head ================= *
   Two rows with two different jobs, and the difference is the whole point of
   the rebuild.

   Row one is IDENTITY and PRESENTATION: what kind of thing, how many, how it
   is drawn, and what is being held back. Nothing here changes the question.
   Row two is THE QUESTION: where the list comes from, how it is ordered, and
   what narrows it - one idiom, left to right, in a fixed order, so the row
   reads as a sentence about the list rather than as five unrelated widgets. */
.qhead {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: .5rem .7rem; padding: .8rem var(--pad) .35rem;
}
/* The word is on screen already, lit, in the nav tab - at every size, not
   just on a phone. Kept in the document for anyone listening to it. */
.qhead .bh-word {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
.qhead .browsecount {
  font-size: .8rem; font-weight: 700; color: var(--dim); margin: 0;
  font-variant-numeric: tabular-nums; letter-spacing: .02em;
}
/* Amber when the number is not the whole truth, so "some of this is being
   kept from you" is a peripheral fact rather than a string you must read.
   Unscoped from .qhead: the Calendar head is a plain .browsehead and it has
   the same thing to say once its filters hide a row. `.cut` is only ever set
   deliberately, so widening the selector cannot surprise a head that does
   not ask for it. */
.browsecount.cut { color: var(--hide-tint); }
.qh-gap { flex: 1 1 auto; }

/* ---- row two: the query ----
   One horizontal scroller with a fixed slot order rather than a wrapping row.
   The head's height is then CONSTANT: today a third filter token pushes the
   grid down a whole line, and under this it cannot. What scrolls off the
   right is always "more filters", never the two structural tokens. */
.qh-query {
  display: flex; align-items: center; gap: .45rem;
  padding: 0 var(--pad) .5rem;
  overflow-x: auto; scrollbar-width: none; overscroll-behavior-x: contain;

  /* A fade at whichever end has more behind it.
   *
   * This row is deliberately a scroller - see the note above: a constant head
   * height is worth more than a second line of chrome, which on a phone cost
   * four lines and pushed the first poster nearly halfway down the screen. But
   * nothing said so, and a token sliced in half at the screen edge reads as a
   * broken layout rather than as a row that continues.
   *
   * Four background layers and no script. The two `local` layers are painted
   * in the page's own black and travel WITH the content, so each covers its
   * end-stop only while that end is the one you are at; the two `scroll`
   * layers are the fades and stay pinned to the scrollport. Covers are listed
   * first so they paint over the fades - which makes the left fade hidden at
   * rest and the right one vanish on arrival, correct at every position in
   * between without measuring a scroll offset.
   *
   * Backgrounds rather than a mask: this machine reports support for
   * mask-image and for backdrop-filter and paints neither, and a mask honoured
   * as "no mask" would take the row with it. */
}
/* Only when there is something off the edge to fade. The four layers above are
   painted in --bg on a page painted in --bg, so the intent was that a row with
   nothing to scroll paid nothing for them - black on black. It does not hold:
   reported from a real screen and reproduced here, a flat explicitly-painted
   black reads as a dark rectangle against the page's own black at the end of
   the row. A decoration that can only be invisible or wrong is not painted at
   all now. fadeIfScrolls() sets the class from scrollWidth against clientWidth
   and keeps it right through a resize. */
.qh-query.scrolls {
  background:
    linear-gradient(to right, var(--bg) 55%, rgba(0, 0, 0, 0)) 0 0 / 2.2rem 100% no-repeat local,
    linear-gradient(to left,  var(--bg) 55%, rgba(0, 0, 0, 0)) 100% 0 / 2.2rem 100% no-repeat local,
    linear-gradient(to right, var(--bg), rgba(0, 0, 0, 0)) 0 0 / 2.2rem 100% no-repeat scroll,
    linear-gradient(to left,  var(--bg), rgba(0, 0, 0, 0)) 100% 0 / 2.2rem 100% no-repeat scroll;
}
.qh-query::-webkit-scrollbar { display: none; }
.qhead + .qh-query .srcrail, .qh-query .browsefilters { display: contents; }
.qh-query .srcrail { display: contents; }

/* ---- the token ----
   A label and the value it holds. Three of them are a <select> in a shell:
   native, one tap, reachable in a car, and it already knows how to show its
   own value - which is the property the chip rail was missing. */
.qtok {
  display: inline-flex; align-items: center; gap: .4rem; flex: none;
  min-height: 2.35rem; padding: 0 .7rem; border-radius: 999px;
  background: var(--elev);
  box-shadow: inset 0 0 0 var(--rule-w) transparent;
  color: var(--fg); font-size: .8rem; font-weight: 700;
  /* transform and opacity only. background and border-color are deliberately
     NOT transitioned: they are what stutters on a rasteriser with no GPU, and
     an instant colour change is the correct feedback anyway. */
  transition: transform .12s var(--ease);
  cursor: pointer;
}
.qtok:active { transform: scale(.97); }
.qtok-l {
  font-size: .6333rem; font-weight: 800; letter-spacing: .12em;
  text-transform: uppercase; color: var(--dimmer);
}
.qtok-v {
  display: inline-block; min-width: 0; max-width: 11rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  vertical-align: bottom;
}
/* Drawn from two borders - no asset, no icon font, and it rotates on open
   with a transform, which is one of the two things the car does cheaply. */
.qtok-c {
  width: .32rem; height: .32rem; flex: none; margin-left: .1rem;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: translateY(-.1em) rotate(45deg);
  opacity: .55; transition: transform .16s var(--ease);
}
.qtok[aria-expanded="true"] .qtok-c { transform: translateY(.06em) rotate(-135deg); }

/* SET: this token is narrowing the question. */
.qtok.set { background: var(--ask-fill); box-shadow: inset 0 0 0 var(--rule-w) var(--ask-ring); }
.qtok.set .qtok-l { color: var(--ask); }
.qtok.set .qtok-v, .qtok.set select { color: var(--ask-tint); }

/* A set filter removes itself where it sits. The old "Filters (3)" badge is
   gone with it: the tokens ARE the count, and the row stated that fact twice. */
.qtok-x { padding-right: .5rem; }
.qtok-x svg { width: .78rem; height: .78rem; opacity: .7; }
.qtok-x:hover svg { opacity: 1; }
.qtok-add { color: var(--dim); }
.qtok-add svg { width: .8rem; height: .8rem; }
.qtok-add:hover, .qtok-add:focus-visible { color: var(--fg); background: var(--elev2); }
.qnote {
  flex: none; font-size: .7rem; color: var(--dimmer); padding-left: .2rem;
  white-space: nowrap;
}


/* ---- the choices, anchored to the control that owns them ----
   Not a <select>. This file already made that argument at the settings group:
   a native select on the car opens a modal list that is its own small fight to
   dismiss. And a genre is not a choice of one - it is three states per row,
   which a native select cannot hold at all. */
.qpop {
  position: fixed; z-index: var(--z-qpop);
  min-width: 11rem; max-width: min(23rem, calc(100vw - 1rem));
  max-height: min(60vh, 26rem); overflow-y: auto;
  /* Reaching the end of this list must not start scrolling the page behind
     it, which on a touchscreen reads as the panel refusing to move. */
  overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
  border-radius: var(--r-m); padding: .5rem;
  background: var(--panel);
  box-shadow: inset 0 0 0 1px var(--line-hi), var(--shadow-lg);
  /* Opens from nothing, but the resting state IS the final state - no
     backwards fill, so with animations disabled it is simply there.

     transform-origin is set in JS from the anchor's position: the panel grows
     out of the control that owns it, downward when it hangs below and upward
     when it has been flipped above. It used to grow downward always, which
     when flipped looked like it had arrived from somewhere else on the screen
     - and on a phone, where the head is at the top, it is flipped nearly every
     time. */
  animation: qpopIn .16s var(--ease);
}
@keyframes qpopIn {
  from { opacity: 0; transform: translateY(-.25rem) scale(.94); }
  to   { opacity: 1; transform: none; }
}
/* Flipped above: it must not start by moving UP, away from its anchor. */
.qpop.up { animation-name: qpopUp; }
@keyframes qpopUp {
  from { opacity: 0; transform: translateY(.25rem) scale(.94); }
  to   { opacity: 1; transform: none; }
}
.qpop-h {
  padding: .2rem .45rem .45rem; font-size: .6333rem; font-weight: 800;
  letter-spacing: .12em; text-transform: uppercase; color: var(--dimmer);
}
.qpop-h span {
  display: block; margin-top: .2rem; letter-spacing: .02em;
  text-transform: none; font-weight: 600; font-size: .7rem; color: var(--dim);
}
.qpop-list { display: flex; flex-direction: column; gap: .15rem; }
/* Eighteen genres are a grid, not a column: a column of them is a scroll on
   every screen, and a two-column grid fits without one on all but a phone. */
.qpop-list.two {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .25rem;
}
.qopt {
  display: block; width: 100%; text-align: left;
  min-height: 2.4rem; padding: 0 .6rem; border-radius: var(--r-s);
  background: none; border: 0; color: var(--fg);
  font: inherit; font-size: .8rem; font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: transform .1s var(--ease);
}
.qopt:hover { background: var(--elev); }
.qopt:active { transform: scale(.98); }
/* A rule above an item that acts on the LIST rather than on the work - taking
   a card off Recently opened, beneath three things that write to
   PublicMetaDB. Margin above the line and padding below it, so the gap reads
   as a division rather than as one item having drifted. */
.qopt.sep {
  margin-top: .35rem; padding-top: .35rem;
  border-top: 1px solid var(--line-hi); border-radius: 0 0 var(--r-s) var(--r-s);
}

/* Three states, and the difference is in the WORDS as much as the colour:
   filled means in, hollow with "Not" in front of it means out. Colour alone
   would be invisible to anyone who cannot see it, and a strike-through at
   this size is a smudge. */
.qopt.inc { background: var(--ask-fill); color: var(--ask-tint);
            box-shadow: inset 0 0 0 var(--rule-w) var(--ask-ring); }
.qopt.exc { background: none; color: var(--ask);
            box-shadow: inset 0 0 0 var(--rule-w) var(--ask-ring); }
.qopt.exc i, .chipbtn.exc i {
  font-style: normal; opacity: .7; font-size: .7rem;
  letter-spacing: .06em; text-transform: uppercase;
}

/* The same three states inside the filter sheet, where the two genre lists
   used to be. */
.chipbtn.tri.inc {
  background: var(--ask-fill); color: var(--ask-tint);
  border-color: transparent; box-shadow: inset 0 0 0 var(--rule-w) var(--ask-ring);
}
.chipbtn.tri.exc {
  background: none; color: var(--ask);
  border-color: transparent; box-shadow: inset 0 0 0 var(--rule-w) var(--ask-ring);
}
@media (prefers-reduced-motion: reduce) {
  .qpop, .qpop.up { animation: none; }
  .fxfbody, .stepswrap { transition: none; }
  .qtok-c { transition: none; }
}
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  /* 52px under a thumb, and the panel gets wider so eighteen genres stay two
     across at car type sizes. */
  /* px floor under the rem one, because --ui scales rem and a finger does
     not scale with it. It mattered more when --ui was a setting: the root is
     calc(15px * --ui), so 3.5rem was 52.5px at the old Default but 48.3px at
     the old Compact (--ui 0.92) - under the 52 this app spends
     on anything reached without looking. These two carry it first because
     the Calendar's filters are built on them; the same arithmetic applies to
     every other 3.4-3.5rem floor in this file at that one setting. */
  .qopt { min-height: max(3.5rem, 52px); font-size: 1rem; }
  .qpop { max-width: min(34rem, calc(100vw - 2rem)); padding: .6rem; }
}
@media (max-width: 700px) {
  /* One column on a phone: two columns of genre names at this width are two
     columns of ellipsis. */
  .qpop-list.two { grid-template-columns: 1fr; }
  .qtok-v { max-width: 7.5rem; }
}

/* ---- the foot of a list ----
   Three dots while the next page is in flight, and a line when there is no
   next page. The end of a list should be a statement rather than a place
   where things stop happening - that is the one thing a "Show more" button
   did well and an endless list has to say for itself.

   The dots animate with transform and opacity only, which is all the car
   rasterises cheaply, and the whole thing is correct with no animation at
   all: three dots at rest still read as waiting. */
.apage {
  display: flex; justify-content: center; align-items: center;
  min-height: 3.4rem; padding: 1rem var(--pad) 2rem;
}
.ap-spin { display: flex; gap: .34rem; }
.ap-spin i {
  width: .42rem; height: .42rem; border-radius: 50%;
  background: var(--dim); display: block;
  animation: apDot 1s var(--ease) infinite;
}
.ap-spin i:nth-child(2) { animation-delay: .14s; }
.ap-spin i:nth-child(3) { animation-delay: .28s; }
@keyframes apDot {
  0%, 100% { opacity: .3; transform: none; }
  40%      { opacity: 1;  transform: translateY(-.22rem); }
}
.ap-end {
  font-size: .7333rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--dimmer); text-align: center;
}
@media (prefers-reduced-motion: reduce) { .ap-spin i { animation: none; } }

/* ---- the lenses ----
   Amber, because they are the one pair of controls whose effect you might not
   notice: they take results away after the catalogue has answered. */
.lensrow .chipbtn.on {
  background: var(--hide-fill); color: var(--hide-tint);
  box-shadow: inset 0 0 0 var(--rule-w) var(--hide-ring);
  border-color: transparent;
}
.lensrow .chipbtn.on:hover { background: var(--hide-fill); }

/* ---- motion ----
   Every rule below decorates a state that is already correct without it. No
   backwards fill anywhere: an element that is only right once an animation
   has run is an element that is wrong in any environment that does not run
   animations, and this one has several. */
@keyframes qtokIn {
  from { opacity: 0; transform: translateY(.3rem); }
  to   { opacity: 1; transform: none; }
}
.qtok-x { animation: qtokIn .2s var(--ease); }
@keyframes qcount {
  0% { transform: none; }
  40% { transform: scale(1.14); }
  100% { transform: none; }
}
/* Fired by adding the class, so the number visibly answers the switch you
   just pressed rather than quietly rewriting itself two elements away. */
.browsecount.bump { animation: qcount .22s var(--ease); }
@media (prefers-reduced-motion: reduce) {
  .qtok-x, .browsecount.bump { animation: none; }
}

/* ---- the controls ----
   Under the head rather than crammed beside it, because the tokens live here
   and there is no telling how many there are.

   Two containers, one row. They were two rows, and on a 375px phone each
   wrapped to two lines of its own - four lines of chrome, with the first
   poster starting 42% of the way down the screen. `display: contents` drops
   both wrappers out of the layout so every control flows as a single run and
   takes a new line only when it truly has to, while each keeps its own
   element to replaceChildren() on: drawFilters and drawLenses run at
   different moments and sharing one node would let either wipe the other. */
/* Only inside the query row, where the two containers must flow as one run.
   .lensrow now lives in row ONE and is a real box again - as `contents` its
   children flowed loose into .qhead and wrapped it to five lines on a phone. */
.qh-query .browsefilters, .qh-query .srcrail { display: contents; }
.qhead .lensrow { display: flex; align-items: center; gap: .45rem; min-width: 0; }
/* A hairline where the query controls end and the reading controls begin.
   Everything left of it changes what is asked of TMDB; everything right of it
   changes what you do with the answer, which is worth one pixel to say. */
.viewsw::before {
  content: ''; align-self: stretch; width: 1px; margin: .15rem .35rem .15rem 0;
  background: var(--line-hi);
}
/* A set filter is white-on-black like every other selected thing here, with
   the cross inside it: one target that says what it is and removes itself,
   rather than a label and a separate button to hunt for. */
/* A token chip, wherever it is drawn.
   These were scoped to .browsefilters, and the empty state builds the same
   chip outside it - so its cross had no width at all and fell back to an SVG's
   default 300x150, rendering as a white disc the size of a poster with the
   label wrapped above it. The layout belongs to the chip, not to one of the
   places a chip happens to appear. */
.tok { display: inline-flex; align-items: center; gap: .35rem; }
.tok svg { width: .75rem; height: .75rem; flex: none; opacity: .65; }
.tok:hover svg { opacity: 1; }

/* ---- the sheet ---- */
.sd-ft {
  flex: none; display: flex; align-items: center; gap: .7rem;
  padding: .8rem 1rem calc(.8rem + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line); background: var(--bg2);
}
/* The primary action carries the count, so it is the wide one and it is the
   only white thing in the sheet. */
.sd-ft .sdgo {
  margin-left: auto; background: #fff; color: #000; border-color: #fff;
  font-weight: 800;
}
.sd-ft .sdgo:disabled { opacity: .45; pointer-events: none; }
.fsec { padding: .55rem 1rem .2rem; }
.fsec-h {
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .16em;
  text-transform: uppercase; color: var(--dimmer); margin-bottom: .45rem;
}
.fsec-n { margin-left: .5rem; letter-spacing: .04em; text-transform: none; font-weight: 600; }
.fsec-b { display: flex; flex-wrap: wrap; gap: .4rem; }
.fsec-b select {
  background: var(--elev); border: .06em solid var(--line-hi); border-radius: 99px;
  color: var(--fg); padding: .45rem .9rem; font: inherit; font-size: 0.8333rem;
  font-weight: 700; cursor: pointer;
}
/* The browse filters are .qtok buttons and take the token styling that every
   other filter in the app takes. What stood here was a private copy of it,
   drawn for two <select>s that no longer exist.

   One thing the tokens need that the selects did not: a long genre name has
   no dropdown arrow to be truncated against, so the value truncates itself. */
/* The browse screen's two tokens, named rather than described.
   `.browsefilters .qtok` also matches the DISCOVER query row, because
   .qh-query .browsefilters is display: contents - which removes the box but
   leaves the element in the selector's path. A cap written for two tokens was
   landing on a row of five, and on a phone it ellipsised every one of them and
   pushed the last two off the screen. */
.fsort, .fgenre { max-width: 14rem; }
.fsort .qtok-v, .fgenre .qtok-v {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.browsehead + .results .grid, .results > .grid,
.browsehead + .results .rows, .results > .rows { padding-top: .4rem; }

/* Fourteen pixels, and they are worth chasing.
   On a 747px-tall car screen the grid started at 201 and a poster row is 273,
   so the second row ended at 761 - fourteen pixels below the fold, which
   turns two rows of browsing into one plus a sliver. Measured, then taken out
   of the chrome above it rather than out of the cards.
   Gated on HEIGHT, not on the car specifically: any short screen has the same
   problem and a tall one should keep its air. */
@media (max-height: 820px) {
  .browsehead { padding-top: .7rem; padding-bottom: .25rem; }
  .browsefilters { padding-bottom: 0; }
  /* .results > * as well as the adjacency pairs, and the rule fourteen lines
     above already names it for the same reason: a head with a query row
     between it and its results is reached by no adjacency selector, so
     Discover and the shelf both kept .4rem where every other browse body got
     .2rem. Three pixels, on the 747px screen the fourteen above were measured
     on. */
  .browsehead + .browsefilters + .results .grid,
  .browsehead + .results .grid, .results > .grid,
  .browsehead + .browsefilters + .results .rows,
  .browsehead + .results .rows, .results > .rows { padding-top: .2rem; }
}

/* The 96rem cap that used to be here, and the 1800px poster bump under it,
   are both gone. The cap is on the page body now (see .screen > .body-clear),
   which is what these five selectors were reaching for one at a time - and
   .qh-query, sitting between two of them, was never on the list. The bump was
   scoped to `.results .grid`, so it missed the watchlist and list grids that
   are the same component, and beat `.grid.wideg` on source order, costing the
   Stills view its wide track. Column width is a clamp on .grid itself now:
   fluid instead of stepping at one width, and true wherever .grid is used. */

/* On a phone the head stacks; the rail takes its own full-width line. */
@media (max-width: 700px) {
  .srcrail { margin-left: 0; width: 100%; order: 3; }
}

/* person page header */
.personhead {
  display: flex; gap: 1.6rem; align-items: flex-start;
  padding: 1.6rem var(--pad) .6rem;
}
.pface.big {
  width: 10rem; flex: none; margin: 0; border-radius: var(--r-m);
  box-shadow: 0 0 0 .09em var(--line-hi), var(--shadow-lg);
}

.pnoimg {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-size: 2rem; font-weight: 800; color: var(--dimmer);
  background: var(--bg2); letter-spacing: -.02em;
}
.pface.big .pnoimg { font-size: 3.2rem; }
.personhead .pinfo { min-width: 0; max-width: 52rem; }
.personhead h1 {
  font-size: clamp(1.5rem, 3.4vw, 2.4rem); font-weight: 800;
  letter-spacing: -.02em; margin: 0 0 .25rem;
}
.personhead .pbio {
  margin: 0; color: var(--dim); font-size: 0.9rem; line-height: 1.6;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical;
}
@media (max-width: 640px) {
  /* The portrait keeps the name company and then gets out of the way.
     It was a flex row of two: a 6.5rem photo and everything else. The photo is
     145px tall and everything else was 400, so two thirds of the left hand
     column was empty while the right hand one was 234px wide - which is why
     four figures wrapped two, one, one, and why the facts line broke mid
     date. The photo now spans the name and the facts only; the figures, the
     career strip and the biography cross under it at full width.
     .pinfo goes display: contents so its five blocks become items of this
     grid. It is a wrapper, not a box - nothing is drawn on it. */
  .personhead {
    display: grid; gap: .5rem 1rem; align-items: start;
    grid-template-columns: 6.5rem minmax(0, 1fr);
    /* The second row takes the slack, not both of them. The photo spans these
       two rows and is taller than the name and the facts together, and grid
       shares a spanning item's excess out evenly - which opened 28px between
       the name and the line under it. A flexible track claims that excess on
       its own, so it now falls below the facts, where the photo is still
       covering it. */
    grid-template-rows: min-content 1fr;
  }
  /* The frame shrinks, never the image - the image is pinned to the frame's
     edges, so sizing it separately would float a smaller photo inside the
     card. This rule predates the frame and did exactly that. */
  .pface.big { width: 6.5rem; grid-column: 1; grid-row: 1 / span 2; }
  .personhead .pinfo { display: contents; }
  .personhead .pname,
  .personhead .pfacts { grid-column: 2; }
  .personhead .pstats,
  .personhead .pcareer,
  .personhead .pbio { grid-column: 1 / -1; }
  /* Two by two rather than however many fit. Left to wrap they came out two,
     one, one - three rows of different lengths reading as three groups when
     they are one. */
  .personhead .pstats .figures {
    display: grid; grid-template-columns: 1fr 1fr; gap: .8rem 1.1rem;
  }
}

/* ----------------------------------------------------------------- hero --
   Slides cross-fade in place: the frame never moves, so the eye stays put and
   the dots stay put with it. Only opacity and the inner text animate, both
   compositor-friendly. Swiping is handled as a gesture rather than by
   scrolling the container - scrolling was what dragged the whole element, and
   the indicator with it. */
.hero {
  position: relative; overflow: hidden; touch-action: pan-y;
  /* Sized to the artwork, not to an arbitrary fraction of the screen.
     Backdrops are 16:9, and 56.25vw IS 16:9 of the full width - so on any
     screen wide enough to allow it the frame matches the image and almost
     nothing is cropped. 100vh stops it running past the fold on tall windows;
     26rem stops it collapsing on a phone, where 16:9 of the width is tiny.
     The vh cap is the whole viewport: the header floats over the hero rather
     than above it, so there is no chrome to leave room for. On a window
     narrower than 16:9 the frame IS the image and nothing is cropped; on a
     wider one - an ultrawide desktop - a 16:9 image simply cannot be shown
     whole at full bleed, and this shows as much of it as physically fits. */
  height: clamp(26rem, 56.25vw, 100vh);
}
.hero .slide {
  position: absolute; inset: 0;
  opacity: 0; visibility: hidden;
  transition: opacity .7s var(--ease), visibility 0s linear .7s;
}
.hero .slide.on {
  opacity: 1; visibility: visible;
  transition: opacity .7s var(--ease), visibility 0s;
}
.hero img.bg {
  width: 100%; height: 100%; object-fit: cover;
  object-position: 50% 42%;
  transform: scale(1.06);
  transition: transform 7s linear, opacity .7s var(--ease);
}
.hero .slide.on img.bg { transform: scale(1); }

/* The inner elements arrive in sequence each time a slide takes over. The
   animation is re-triggered by toggling .on, so it plays on every change
   rather than only on first paint. */
.hero .slide.on .logo,
.hero .slide.on h1,
.hero .slide.on .meta,
.hero .slide.on .plot,
.hero .slide.on .castSlotHero,
/* backwards, not both.
   `both` keeps asserting the final keyframe for ever after the animation has
   finished, and an animation outranks a declared value - so the trailer state
   below could collapse these boxes (max-height is not in the keyframes) and
   could not fade them (opacity is). backwards still fills BEFORE the run,
   which is the half that matters here: it is what holds each element hidden
   through its stagger delay. heroIn ends on opacity 1 / transform none, which
   is the resting state anyway, so nothing about the entrance changes. */
.hero .slide.on .btns { animation: heroIn .65s var(--ease) backwards; }
.hero .slide.on .logo,
.hero .slide.on h1     { animation-delay: .10s; }
.hero .slide.on .meta  { animation-delay: .18s; }
.hero .slide.on .plot  { animation-delay: .26s; }
.hero .slide.on .castSlotHero { animation-delay: .32s; }
.hero .slide.on .btns  { animation-delay: .40s; }
@keyframes heroIn {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}

/* ---- and the way out ----
   The outgoing slide's text leaves upward while the incoming slide's text
   arrives from below, so the two read as a swap rather than as a dissolve
   between two translucent copies. Same order as the entrance, quicker, and
   over well before the .7s picture cross-fade finishes underneath it.

   forwards, not both: the class is taken off again a moment later, and an
   animation still asserting its last keyframe when that happens would leave
   the slide's text invisible the next time it is shown. */
.hero .slide.out .logo,
.hero .slide.out h1,
.hero .slide.out .meta,
.hero .slide.out .plot,
.hero .slide.out .castSlotHero,
.hero .slide.out .btns { animation: heroOut .3s var(--ease) forwards; }
.hero .slide.out .meta  { animation-delay: .03s; }
.hero .slide.out .plot  { animation-delay: .06s; }
.hero .slide.out .castSlotHero { animation-delay: .08s; }
.hero .slide.out .btns  { animation-delay: .1s; }
@keyframes heroOut {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(-12px); }
}
@media (prefers-reduced-motion: reduce) {
  /* The delays would outlive the global duration reset as a stall - text
     sitting still for a tenth of a second before disappearing. */
  .hero .slide.out .logo,
  .hero .slide.out h1,
  .hero .slide.out .meta,
  .hero .slide.out .plot,
  .hero .slide.out .castSlotHero,
  .hero .slide.out .btns { animation: none; animation-delay: 0s; }
}

/* ---- a line that changed ----
   Used by setTx, which is the only thing that knows the difference between a
   line being repainted and a line being different. Small: these are status
   lines a metre from your eye, and a full swap for "3 of 11" becoming
   "4 of 11" would be a page that never sits still. */
.txin { animation: txIn .26s var(--ease); }
@keyframes txIn {
  from { opacity: 0; transform: translateY(.3em); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) { .txin { animation: none; } }

/* Late arrivals.
 *
 * The ladder above runs the moment a slide takes over - measured at 353ms
 * from a cold navigation. Three things reach the hero AFTER that: the status
 * badge and the cast strip at ~417ms, and the wordmark at ~423ms, each
 * waiting on its own request. Their slots have already animated, so they
 * appeared with nothing at all - snapping into the most prominent place on
 * the screen 60ms after it had settled.
 *
 * A short fade, not a repeat of the ladder. They are filling space that is
 * already held rather than announcing themselves, and the page has just
 * finished moving: this should read as the last detail resolving, not as a
 * second entrance. Opacity only, so it composites. */
@keyframes arrive { from { opacity: 0; } to { opacity: 1; } }

.hero .scrim {
  position: absolute; inset: 0;
  /* Two gradients, eased and short instead of broad and linear.

     What was wrong before was not that there were two. It was that both ramped
     straight: 94% black down the left falling only to 60% by 44% across, over
     a vertical one that reached halfway up the frame. Two long ramps multiply,
     so the middle of the picture - which is where a face usually is - carried
     about 88% black to make one corner readable. Composite at 30% across and
     20% up: was .88, now .53.

     Both states live in pseudo-elements rather than on .scrim itself, because
     a background is not an animatable property - two gradients cannot tween
     into each other, and the trailer state has to ARRIVE rather than snap. So
     ::before holds the still state, ::after the trailer state, and the class
     cross-fades them. The base is now only a positioning box.

     The detail page's hero shares this rule and never gets .trailing, so it
     keeps ::before at full and is unaffected. */
}
.hero .scrim::before,
.hero .scrim::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  transition: opacity .32s var(--ease);
}
.hero .scrim::before {
  opacity: 1;
  background:
    linear-gradient(90deg,
      rgba(0,0,0,.90) 0%,
      rgba(0,0,0,.74) 10%,
      rgba(0,0,0,.50) 22%,
      rgba(0,0,0,.28) 34%,
      rgba(0,0,0,.12) 46%,
      rgba(0,0,0,.03) 56%,
      rgba(0,0,0,0) 64%),
    linear-gradient(0deg,
      var(--bg) 0%,
      rgba(0,0,0,.62) 6%,
      rgba(0,0,0,.38) 14%,
      rgba(0,0,0,.20) 24%,
      rgba(0,0,0,.08) 34%,
      rgba(0,0,0,0) 46%);
}
/* The trailer state. Only a logo and a button row are left to light, and both
   sit on the floor of the frame - so the darkness has half the reach and the
   whole upper picture is clear. */
.hero .scrim::after {
  opacity: 0;
  background:
    linear-gradient(90deg,
      rgba(0,0,0,.70) 0%,
      rgba(0,0,0,.40) 12%,
      rgba(0,0,0,.15) 24%,
      rgba(0,0,0,.04) 34%,
      rgba(0,0,0,0) 44%),
    linear-gradient(0deg,
      var(--bg) 0%,
      rgba(0,0,0,.55) 5%,
      rgba(0,0,0,.22) 13%,
      rgba(0,0,0,.05) 22%,
      rgba(0,0,0,0) 32%);
}
.hero.trailing .scrim::before { opacity: 0; }
.hero.trailing .scrim::after { opacity: 1; }

/* ---- the hero, while a trailer plays ----
   Everything here is a transition on the resting state rather than an
   animation on the trailing one, so the way back is the way out played
   backwards and costs no extra rules. Nothing carries a transition-delay:
   the reduced-motion rule zeroes DURATIONS only, and a delay survives it as a
   dead stall.

   What stands down is what is read: the badge and rating, the description,
   the cast strip. You are watching the thing now; a paragraph about it is the
   one piece of the screen that has stopped being useful. What stays is what
   identifies and what acts - the wordmark, smaller, and the button row.

   They collapse rather than just fading, or the logo and the buttons would
   hold a hand-sized hole between them. The negative margin cancels the flex
   gap each one leaves behind. */
.hero .body > .meta,
.hero .body > .plot,
.hero .body > .castSlotHero {
  overflow: hidden; max-height: 14rem;
  transition: opacity .22s var(--ease), max-height .32s var(--ease),
              margin-top .32s var(--ease), transform .22s var(--ease);
}
.hero.trailing .body > .meta,
.hero.trailing .body > .plot,
.hero.trailing .body > .castSlotHero {
  opacity: 0; max-height: 0; margin-top: -.6rem;
  transform: translateY(.35rem); pointer-events: none;
}

/* The wordmark stays, at about half. It is the one thing that still has to be
   true while the picture moves - you should never have to wonder what you are
   looking at - but it has no business being the size it is when it was the
   headline of a still. */
.hero .logo, .hero h1 {
  /* margin-bottom joins the list it belongs in. Without it the lift below
     would land in a single frame at the instant `.trailing` is added, while
     everything beside it eases - the wordmark would jump 16px and then the
     description would fold away around it. */
  transition: max-height .3s var(--ease), max-width .3s var(--ease),
              font-size .3s var(--ease), opacity .22s var(--ease),
              margin-bottom .3s var(--ease);
}
.hero.trailing .logo { max-height: 4.2rem; max-width: min(14rem, 42%); }
.hero.trailing h1 { font-size: clamp(1.3rem, 2.4vw, 1.9rem); }
/* The wordmark steps up so the sound control has a band to sit in; the stack
   it sits in does not move. `.hero .body` is justify-content: flex-end and
   bottom-anchored, so a bottom margin on the wordmark lifts the wordmark
   alone inside a box that stays put. The three rows below it are already at
   max-height 0 with margin-top: -.6rem cancelling their own gap, so this
   margin has exactly one job.
   Keeping the body's box still is the point: the header clamp further down
   stays literally true rather than becoming conditional on content height,
   `.body` never becomes a containing block for a fixed descendant, and
   nothing leaks to the two detail heroes that share these selectors. A
   transform on `.body` would have been cheaper by one reflow and wrong on all
   three counts.
   (0,3,0), which does not collide with `.heroSlot .hero.trailing .logo` in
   the phone block at (0,4,0) - that one sets max-height and nothing else. */
.hero.trailing .logo, .hero.trailing h1 { margin-bottom: var(--snd-lift); }

/* The button used to come down with them, and cannot any more.
   `.trailing` is added in exactly one place - heroEl, on its own <section
   class="hero"> - and heroEl's .btns was deleted in 117c602 as unreachable,
   because discSlide never returned a `play` to put in it. The only two
   `class="btns"` left are episodePage's and titleScreen's, and neither of
   those heroes ever gets .trailing, so `.hero.trailing .btns .btn` matched
   nothing on any screen. It is gone here and in the car block.
   The transition stays, minus the two properties whose only animator went
   with the rule: transform, background and box-shadow are the press and
   hover states of a button that is still very much alive on both detail
   pages, which ARE inside a .hero - the group sits at
   .hero > .slide > .body > .actions > .btns. */
.hero .btns .btn {
  transition: transform .15s var(--ease), background .2s, box-shadow .2s;
}

/* The touch floor, on every coarse pointer rather than only under 700px.
   The rule that sets it lives in the phone block, which a phone held SIDEWAYS
   does not match: 852x393 is how you would actually hold one to watch
   something, and there the row fell back to .btn's own padding and landed at
   44px measured - under the floor, though not as far under as the note that
   started this claimed. Width was never the right test for a finger.
   Height only. The phone block pairs its 3.2rem with a padding trim, because
   at 393px five buttons have to make two rows rather than three; at 852 there
   is horizontal room and the trim would be solving a problem that is not
   there. (0,2,0), so the phone block's own rule still wins where both apply.
   The third rule this paragraph used to name - .hero.trailing .btns .btn at
   (0,4,0) - is gone, because the .btns it needed was deleted with heroEl's
   unreachable Play button. Nothing outranks this now except the phone
   block. */
@media (pointer: coarse) {
  .btns .btn { min-height: 3.2rem; }
  /* The season's bulk mark, here rather than in the phone block for the same
     reason and found the same way - by grepping for the PATTERN after fixing
     .btns .btn, instead of taking one selector's word for the class. It is the
     only other floor in this file that was reachable on a narrow phone alone;
     .sfield has its own rule in the car block, and .screen > .tabs .navtab
     styles a nav that only exists once the phone layout has moved it.
     Measured before and after: 44.7px at 852x393, and 48.2px with the car
     block's .chipbtn declarations applied - that rule sets font-size and
     padding and no height, so a bare .chipbtn had no floor anywhere except
     under 700px. Both now clear 52. */
}

/* The stepper's numbers live on .hero rather than on .dots, because .body has
   to be able to read them.

   Both are anchored to the bottom of the hero and neither knew about the
   other: .dots sits at 1.6rem and is --pip tall, so it occupies up to 2.02rem,
   and .body sat at 2.4rem - or at 1.8rem under the phone rule, which is
   INSIDE that band. It never showed while the last thing in the body was the
   Details button, which is narrow and on the left; the moment the button went
   and the description became the last line, its right-hand end ran straight
   under the pips.

   So the body's floor is now derived from where the stepper actually ends,
   plus a gap, instead of being a number that happened to clear it. Named
   because the trailer rule further down reproduces the row's whole width from
   these, and a slash between two duplicated sets of numbers is exactly how a
   stepper ends up not adding up to itself. */
.hero {
  --pip: .42rem; --pip-on: 1.7rem; --pip-gap: .4rem;
  --dots-y: 1.6rem;
  --dots-clear: calc(var(--dots-y) + var(--pip) + .8rem);
  /* THE TRAILER'S SOUND CONTROL, and its four numbers are here for the reason
     the paragraph above already gives about the stepper's: `.body` has to be
     able to read them. --snd-lift is substituted by a rule that matches the
     WORDMARK, in a different subtree from the button, and a custom property
     declared on the button is invisible from there. The failure would be
     silent - an unresolved var() makes the whole declaration invalid at
     computed-value time, so the margin resolves to 0 and the wordmark sits on
     top of the control with nothing in the console.

     A capsule, not a circle, because a dark circle floating over content is
     what `.screen > .fab` already is on every screen in this app.
     49.14 x 35.91px at --ui 1.26; 39 x 28.5 on a phone.
     --snd-h has a ceiling and it is not arbitrary: the band between the
     hero's floor and `.hero .body`'s own floor is --dots-clear, and the box's
     top must stay under it or it prints on the wordmark. Centred on the
     stepper's line, that caps --snd-h at 2.02rem. 1.9rem leaves 1.13px. */
  --snd-h: 1.9rem;
  --snd-w: 2.6rem;
  /* The stepper's own centre line, inherited from the two tokens the stepper
     is anchored from rather than typed as a number that happens to match. */
  --snd-cy: calc(var(--dots-y) + var(--pip) / 2);
  /* What the wordmark steps up by so the 52px target below it has somewhere
     to be. Derived from the TARGET, not from the paint: 52px is absolute and
     --dots-clear is a rem, so on a phone the target outgrows the band the box
     fits in - a flat .6rem would have overlapped the wordmark by 1.85px
     there. .5rem is the clearance that comes out the far side, at every --ui,
     by construction. 16.36px on the car, 18.35px on a phone. */
  --snd-lift: max(0px, var(--snd-cy) + max(26px, var(--snd-h) / 2) + .5rem
                       - var(--dots-clear));
}
.hero .body {
  position: absolute; left: var(--pad); right: var(--pad);
  bottom: var(--dots-clear);
  max-width: 38rem; display: flex; flex-direction: column; gap: .6rem;
}
/* The home hero can never cross the header, at ANY width.
   This clamp existed and was correct, and was inside @media (max-width:700px)
   - so a phone was protected and the car, at 1307px, was not. The car query
   enlarges the buttons, the chips, the cast row and the badge, and --ui
   scales all of it again, so that is precisely the screen where the stack
   grows tallest. Photographed there with the badge tucked under the nav.

   Scoped to .heroSlot deliberately: the detail hero's .body is
   position:relative, where an explicit `top` SHIFTS the stack instead of
   anchoring it - which once slid it 64px down and clipped the buttons into
   white half-pills. That is why this cannot simply live on .hero .body.

   Content sits at the BOTTOM of the clamp, so a short hero is not
   top-heavy, and anything that still overruns loses its top rather than
   climbing over the header. */
.heroSlot .hero .body {
  top: calc(var(--topH) + .7rem);
  justify-content: flex-end;
  overflow: hidden;
}
.hero .logo {
  max-width: min(22rem, 62%); max-height: 7.5rem; object-fit: contain;
  object-position: left bottom;
  /* The one filter in this hero, and it does not paint on the car - so the
     wordmark is the single element in the stack with no shadow there, while
     every neighbour gets its lift from text-shadow.
     It stays anyway, because there is no substitute for a transparent PNG:
     text-shadow does not apply to an image, and box-shadow would draw around
     the element's rectangle rather than around the letterforms. So this
     degrades to NO shadow rather than to a wrong one, and the scrim behind
     the hero body is what actually carries legibility on that screen.
     Written down so the next reader does not spend an afternoon deciding
     whether it was an oversight. */
  filter: drop-shadow(0 3px 14px rgba(0, 0, 0, .55));
}
.hero h1 {
  font-size: clamp(1.9rem, 4.4vw, 3.4rem); font-weight: 800;
  letter-spacing: -.02em; line-height: 1.05; margin: 0;
  text-shadow: 0 2px 20px rgba(0, 0, 0, .75);
}
.hero .meta {
  display: flex; flex-wrap: wrap; gap: .45rem; align-items: center;
  font-size: 0.8333rem; font-weight: 600; color: var(--dim);
  text-shadow: 0 1px 8px rgba(0, 0, 0, .9), 0 0 2px rgba(0, 0, 0, .7);
}
.hero .plot {
  margin: 0; font-size: 0.9rem; color: var(--dim); overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  max-width: 30rem;
  /* Carries its own contrast now the scrim is not doing it for them. Two
     shadows: a wide soft one to lift it off a busy frame, a tight one to keep
     the letterforms crisp against a bright pixel. text-shadow, not filter -
     filter does not paint on the car at all. */
  text-shadow: 0 1px 8px rgba(0, 0, 0, .9), 0 0 2px rgba(0, 0, 0, .7);
}
/* The whole picture opens the title, so it says so with the cursor. There is
   no hover on the car and none is relied on - the affordance there is the
   convention every other hero on every other service has already taught, plus
   the press below. */
.hero .slide { cursor: pointer; }
.hero .slide .dots, .hero .slide .trmute { cursor: default; }
/* The wordmark is the named target: it takes focus, it answers Enter, and it
   acknowledges a press. transform only - the car paints nothing else cheaply. */
.hero .logo, .hero h1 { cursor: pointer; }
.hero .logo:active, .hero h1:active { transform: scale(.985); }
.hero .logo:focus-visible, .hero h1:focus-visible { outline-offset: .3rem; }

/* Empty now on anything not in the library, and an empty flex row still
   carries its own top margin. */
/* Not `.hero .btns` any more. On a phone the row is a child of the sheet, and
   this is the only rule in the file that makes it a row at all - left
   location-scoped it simply stopped matching there, and five buttons reflowed
   as inline text with no gaps, no wrapping and no touch floor. There are three
   .btns in the app and all three are hero button rows (heroEl, episodePage,
   titleScreen), so dropping the scope changes nothing about the two that never
   move. `.hero .btns .btn` above keeps its scope on purpose: it transitions
   padding and font-size for the trailing state, which no sheet can enter. */
.btns { display: flex; gap: .55rem; flex-wrap: wrap; margin-top: .35rem; }
.btns:empty { display: none; }

/* The wrapper the button row, the resume bar and the version strip share, so
   that a phone can move one node instead of three. It reproduces exactly the
   column .hero .body was already giving them - flex, column, .6rem - which is
   why it carries no media query of its own: the group looks identical in both
   of its homes and only its parent differs.

   One node rather than three, because they are one thing. The version strip
   captions Play and the resume bar captions Resume, and a quarter of a page
   between a control and its caption is worse than either place either of them
   could go.

   All three children can be empty at once - a public title before ownership
   resolves has no buttons, nothing to resume and no copy to name - and three
   display:none children leave the wrapper, and its gaps, at nothing. Which is
   what has to happen at the top of a sheet. */
.actions { display: flex; flex-direction: column; gap: .6rem; }

/* The sheet is block flow with no gap of its own, and the first thing the
   group ever precedes is .epsec, which brings .4rem - five and a half pixels
   between a version strip and a season rail, which reads as one block rather
   than two. Matched to .detail .sheet's own top padding so the group is inset
   from the hero above it and the page below it by the same amount. */
.detail .sheet > .actions { margin-bottom: 1.2rem; }

/* On the children, and that is the whole rule. placeActions runs immediately
   after show(), when all three slots are still the empty divs from the
   template - a fade on .actions itself would start on a zero-height box and be
   over 220ms later, while the buttons are still waiting on ownedIndex() and a
   614 KB library.json. It would work on a memoised hit and do nothing on a
   cold load, which is worse than not being there.

   Each child is display:none while :empty, and an animation does not run on a
   display:none element - so each one starts its fade on the frame it becomes
   non-empty, which is the frame it arrives. Same shape as .hero .caststrip and
   as the <img> logoSwap builds: animate the thing that was late, never its
   slot.

   On the row rather than on each .btn deliberately: `.detail .sheet > .actions
   .btn` would be (0,4,0) and would outrank .btn.iconlabel.flash, killing the
   press acknowledgement on Save and Favorite. `both` on a row also cannot
   collide with .btn.pending's opacity, which a button-level rule would.

   In the hero none of this matches and the group keeps the slide's ladder -
   .hero .slide.on .btns and .verSlot are descendant selectors and the wrapper
   does not break them. arrive rather than heroIn, and no delay: the global
   reduced-motion rule zeroes durations only, so the .40s the row's ladder
   carries would survive it as a dead stall. */
.detail .sheet > .actions > * { animation: arrive .22s var(--ease) both; }

/* The three failure lines this page can put beside the row - two "Could not
   check whether you have this." and one "Could not read your lists." - were
   taking their measure from .detail .hero .body > *. Inside the wrapper they
   lose it, and they are sentences, not controls. The padding is the other
   half: .empty carries a --pad indent that .detail .sheet .empty removes, so
   without this the same sentence would sit flush with the buttons on a phone
   and a full gutter past them on a desktop. */
.actions > .empty { max-width: 38rem; padding-left: 0; padding-right: 0; }
.hero .dots {
  position: absolute; right: var(--pad); bottom: var(--dots-y); z-index: 3;
  display: flex; pointer-events: auto;
}
/* margin rather than gap. The row needs to close up to nothing when a trailer
   takes it over, and that has to animate; `gap` only became animatable in
   Chromium 84 and the car is an embedded browser of unknown vintage, while
   margin has transitioned everywhere for as long as there have been
   transitions. Same layout either way. */
.hero .pip + .pip { margin-left: var(--pip-gap); }
.hero .pip {
  position: relative; overflow: hidden;
  width: var(--pip); height: .42rem; border-radius: 99px;
  background: rgba(255,255,255,.28);
  transition: width .4s var(--ease), margin-left .4s var(--ease),
              opacity .28s var(--ease), background .25s;
}
/* The active pill is a track, and the white is the time left in it. It used to
   be solid white, which said which slide you were on but not that the hero was
   about to move - so the change always arrived unannounced. */
.hero .pip.on { width: var(--pip-on); background: rgba(255,255,255,.3); }

/* ---- and while a trailer plays, the stepper becomes one readout ----
   Twenty-seven pixels is a token, not a gauge: it says a trailer is running
   without ever saying how much of it is left. So the other dots close up and
   the active pill takes the row on its own.

   The row's outside width does not change - the pill is given exactly what
   the dots and the gaps between them give up, computed from the same three
   custom properties they are drawn from, so the two can never disagree. --n
   is the pip count, set once by heroEl. The dots are right-anchored, so the
   pill opens leftward into the space and folds back the same way.

   Width, which is layout, and deliberately so: this happens twice in a
   trailer rather than per frame, it is eight boxes in one flex row, and the
   alternative - scaling the pill on X - would drag its rounded ends out into
   ellipses and taper a bar that is only six pixels tall. */
.hero .dots.trprog .pip { width: 0; opacity: 0; }
.hero .dots.trprog .pip + .pip { margin-left: 0; }
.hero .dots.trprog .pip.on {
  opacity: 1;
  width: calc(var(--pip-on) + (var(--n, 1) - 1) * (var(--pip) + var(--pip-gap)));
}
.hero .pip i {
  position: absolute; inset: 0; border-radius: inherit;
  background: #fff;
  transform: scaleX(0); transform-origin: left center;
}
/* scaleX rather than width: one property, no layout, and on the car's software
   compositor a 27px box is cheap either way - but this cannot reflow the row
   of dots next to it. --dwell is set in heroEl from the same constant the
   rotation timer uses. */
.hero .pip.on i { animation: pipfill var(--dwell, 9000ms) linear both; }
.hero .dots.held .pip.on i { animation-play-state: paused; }
@keyframes pipfill { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/* While a trailer plays, the pill stops counting down to the next slide and
   becomes the trailer's own progress. That is the same fact told better: the
   rotation is held for exactly as long as the trailer runs, so how far through
   the trailer you are IS how long until the hero moves. Before this it simply
   froze wherever the dwell had reached - a pill stuck at 40% for ninety
   seconds, which says "paused" and nothing else.

   Driven by a custom property rather than an inline transform, so the cascade
   never has to be fought: `animation: none` here outscores the pipfill rule
   above, and with no animation running the transform below is free to apply.
   heroEl sets --tp from the video's own timeupdate, so a stall freezes the
   pill instead of letting a clock-driven animation walk on and lie about it. */
.hero .dots.trprog .pip.on i {
  animation: none;
  transform: scaleX(var(--tp, 0));
  /* Carries the one honest jump - the fill retracting from wherever the dwell
     had got to back to the start of the trailer - and smooths the ~4 updates
     a second timeupdate gives us into a crawl. Scoped to trailer mode, so it
     can never be live when armPip() restarts the animation. */
  transition: transform .3s linear;
}

@media (prefers-reduced-motion: reduce) {
  /* Back to the plain solid pill: a bar creeping across the screen for nine
     seconds at a time is exactly what this setting is asking not to see. */
  .hero .pip.on i { animation: none; transform: scaleX(1); }
  /* Repeated inside the trailer selector on purpose. The rule above is
     .hero .pip.on i - three classes - and the trailer rule is five, so
     without this the trailer would win and creep a bar across the screen for
     the one person who asked for exactly that not to happen. */
  .hero .dots.trprog .pip.on i { transform: scaleX(1); transition: none; }
  /* The row still hands itself over - that is state, not decoration, and it
     is the only thing left saying a trailer is playing once the fill is
     solid - but it arrives rather than sweeps. */
  .hero .pip { transition: none; }
}

.tag {
  background: var(--elev2); border-radius: 99px; padding: .1rem .55rem;
  font-size: 0.7667rem; font-weight: 700; letter-spacing: .02em;
}
/* The same glass the buttons are made of. A genre pill sits on the artwork
   exactly like a .btn does, and it was the one thing on that line still
   showing the picture through it sharply.
   var(--blur) rather than the .btn's literal 8px: a pill is ~20px tall
   against a button's ~39px, and 8px of blur on a 20px box is most of its own
   height - it reads as a smear rather than as frosting.
   :not(.cert) is load-bearing. .tag.cert sets background: transparent to be a
   hollow outline chip, and a backdrop-filter with no background of its own is
   precisely the construction the header comment above describes the car
   drawing as flat black. The one chip that cannot afford this is the one that
   would get it for free. */
.hero .meta .tag:not(.cert) {
  backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
}

.btn {
  display: inline-flex; align-items: center; gap: .45rem; border-radius: 99px;
  padding: .6rem 1.3rem; font-weight: 800; font-size: 0.9rem; letter-spacing: .01em;
  background: rgba(255,255,255,.13); color: var(--fg);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  transition: transform .15s var(--ease), background .2s, box-shadow .2s;
}
.btn:hover { background: rgba(255,255,255,.24); }
.btn:active { transform: scale(.95); }
.btn.primary { background: #fff; color: #000; box-shadow: none; }
.btn.primary:hover { background: #fff; box-shadow: 0 4px 22px -8px rgba(255,255,255,.42); }

/* ---- a portrait opens with a tilt, from transitions.dev (Pro) ----

   Their image-open-tilt, on the one picture in this app that is worth looking
   at properly: a person's photograph on their own page. Press it and it zooms
   open the way iPadOS launches an app - scale and rotateX/rotateY under a real
   perspective, pitched back hardest about 40% into the flight and settling
   flat by landing, with the corner radius counter-morphing so the small one
   keeps an icon-like corner. Press again and it dives back on a faster clock
   with a smaller opposite tilt.

   WITHOUT THE BEND, and that is not a shortcut. The recipe's organic bow is a
   software warp drawn to a canvas, which needs getImageData - and these
   photographs come from TMDB, cross-origin, so the canvas is tainted the
   moment one is drawn to it. Measured rather than assumed: a plain TMDB image
   throws SecurityError on read, and setting crossOrigin="anonymous" makes it
   fail to LOAD at all, because TMDB sends no CORS headers. The only way to get
   the bend would be to proxy every portrait through the relay to make it
   same-origin, which is a round trip and a cache for a 300ms bow. The tilt is
   the headline of the effect and it is all CSS.

   The flight is transform-only - nothing here reflows, which is why the page
   underneath does not move while a face the size of a poster is over it.

   Licensed from transitions.dev (Pro) on 2026-09-07. */
.ptiltwrap {
  position: fixed; inset: 0; z-index: var(--z-stream);
  display: grid; place-items: center;
  cursor: zoom-out;
}
/* The dim is a LAYER, not the wrap's own background, and that is what makes
   the close visible at all. Fading the wrap fades everything inside it - so
   the card went transparent in 200ms while its 420ms dive was still running,
   and the dive was never seen. The backdrop leaves on its own clock now and
   the card stays fully opaque the whole way down. */
.ptiltwrap::before {
  content: ''; position: absolute; inset: 0;
  background: rgba(0, 0, 0, .82);
  animation: fadein .2s var(--ease);
}
.ptiltwrap.leaving::before {
  animation: fadeout var(--opentilt-close-dur, 420ms) var(--ease) forwards;
}
/* Above the dim. */
.ptiltwrap .ptiltstage { position: relative; }
/* The stage supplies the perspective, so the tilt reads as depth rather than
   as a squash. */
/* THE CLOCK AND THE ANGLES ARE SHARED, THE PERSPECTIVE IS NOT. Both pictures
   that open this way read these back - the JS off the element, the keyframes
   off the cascade - so there is one set of numbers rather than a copy per
   caller. The stills viewer is deliberately absent from the rule below it:
   a `perspective` property resolves its vanishing point to the centre of the
   element that carries it, which for a full-bleed stage is the centre of the
   SCREEN - and a picture that is nowhere near the centre of the screen for
   most of its flight gets sheared rather than pitched. It uses perspective()
   as a transform function instead; see the note beside .shot-tilt. */
.ptiltwrap, .ptiltstage, .shotwrap {
  --opentilt-open-dur: 620ms;
  --opentilt-close-dur: 420ms;
  --opentilt-open-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --opentilt-close-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --opentilt-tilt-x: 22deg;
  --opentilt-tilt-y: -14deg;
}
.ptiltwrap, .ptiltstage {
  --opentilt-closed-scale: .3;
  --opentilt-lift: 70px;
  --opentilt-perspective: 900px;
  perspective: var(--opentilt-perspective);
}
/* Laid out at FULL size and shrunk with scale, which is the recipe's own
   arrangement and the reason the zoom is one continuous spring rather than a
   move between two waypoints. 2:3 because that is the shape TMDB returns a
   person in, and the height is capped so a tall screen does not open a
   portrait taller than itself. */
.ptiltcard {
  appearance: none; border: 0; padding: 0; margin: 0;
  position: relative; cursor: zoom-out;
  /* clamp, not min, and the lower bound is the whole reason. min(26rem, 78vw,
     52vh) is zero the moment a viewport unit is zero - and they can be: this
     project's browser pane reports 0 x 0 while it is not compositing, and the
     card came out 0 x 0 with it, opened and invisible. clamp's floor cannot be
     argued down by a viewport that does not know its own size.
     One custom property so the height cannot drift from the width; 2:3 is the
     shape TMDB returns a person in. */
  --ptilt-w: clamp(12rem, min(78vw, 52vh), 26rem);
  width: var(--ptilt-w);
  height: calc(var(--ptilt-w) * 1.5);
  border-radius: 2rem;
  background: var(--elev);
  overflow: visible;
  box-shadow: 0 1.6rem 5rem -1rem rgba(0, 0, 0, .8);
  -webkit-tap-highlight-color: transparent;
  transform-origin: center;
  /* THE CLOSED STATE IS THE PORTRAIT'S OWN PLACE, not a smaller copy of the
     middle of the screen. The recipe scales in place because its card already
     sits where it belongs; this one lives in a centred overlay, so shrinking
     alone drops it into the middle of the page and it appears to go back
     somewhere it never was.
     translate and scale are their own properties, so they compose with the
     tilt on `transform` without any of the three having to know about the
     others - which is the same trick the recipe uses to keep the zoom one
     continuous spring while the tilt rides over it. JS measures the portrait
     and writes the three numbers. */
  /* No translate or scale here, and NOT IN THE TRANSITION EITHER. The flight
     between the portrait and the middle is a scripted animation - the JS
     measures both ends and states them - because a transition needs the
     closed value resolved as a computed style before the open value is
     written, and measurement showed that start value never being committed:
     no translate or scale transition was created at all, so the card simply
     appeared in the middle and grew from there. Only the corner is left to
     the transition, which has a value from the cascade at both ends and so
     has nothing to commit.
     The durations and easings below stay the single source of truth: the JS
     reads them back off this element. */
  transform: rotateX(0deg) rotateY(0deg) translateZ(0);
  will-change: translate, scale, transform;
  transition: border-radius var(--opentilt-close-dur) var(--opentilt-close-ease);
}
.ptiltcard img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  display: block; object-fit: cover; pointer-events: none;
  border-radius: inherit;
}
.ptiltcard.is-open {
  border-radius: 1rem;
  transition: border-radius var(--opentilt-open-dur) var(--opentilt-open-ease);
  animation: t-opentilt-open var(--opentilt-open-dur) both;
}
.ptiltcard.is-closing { animation: t-opentilt-close var(--opentilt-close-dur) both; }

@keyframes t-opentilt-open {
  0% {
    transform: rotateX(0deg) rotateY(0deg) translateZ(0);
    animation-timing-function: cubic-bezier(0.3, 0.7, 0.4, 1);
  }
  40% {
    transform: rotateX(var(--opentilt-tilt-x)) rotateY(var(--opentilt-tilt-y))
               translateZ(var(--opentilt-lift));
    animation-timing-function: cubic-bezier(0.45, 0, 0.3, 1);
  }
  100% { transform: rotateX(0deg) rotateY(0deg) translateZ(0); }
}
@keyframes t-opentilt-close {
  0% {
    transform: rotateX(0deg) rotateY(0deg) translateZ(0);
    animation-timing-function: cubic-bezier(0.4, 0.4, 0.5, 1);
  }
  45% {
    transform: rotateX(calc(var(--opentilt-tilt-x) * -0.45))
               rotateY(calc(var(--opentilt-tilt-y) * -0.45))
               translateZ(calc(var(--opentilt-lift) * 0.4));
    animation-timing-function: cubic-bezier(0.45, 0, 0.3, 1);
  }
  100% { transform: rotateX(0deg) rotateY(0deg) translateZ(0); }
}
/* The portrait that opens it says so. zoom-in rather than pointer, because
   what happens is a zoom and the cursor is the only place to promise it on a
   desk; a touch screen has neither cursor nor hover and gets the same result
   by pressing the obvious thing. */
.pface.big { cursor: zoom-in; }
.pface.big:focus-visible { outline: .12rem solid var(--fg); outline-offset: .2rem; }

@media (prefers-reduced-motion: reduce) {
  /* The flight goes, the picture still opens. Removing the transition as well
     as the animation matters: left in, the scale would still spring while the
     tilt did not, which is a stranger thing to watch than either. */
  .ptiltcard, .ptiltcard.is-open, .ptiltcard.is-closing {
    animation: none; transition: none;
  }
}

/* ---- the Play button's rim glow, from transitions.dev (Pro) ----

   Their "Get Pro" button, on the one control this app most wants to feel like
   the thing you came for. It suits it almost unchanged: the recipe is built
   for a plain white pill with dark text. Ours is the ordinary button's dark
   translucent pill instead - see the note on .probtn.primary below - because
   on white these washes come out as pastel smudges and on dark they read as
   colour. The pill is ours either way; only the wash is theirs.

   Seven rotated radial washes drift around the perimeter while a full hue
   rotation cycles the palette, and a radial mask clears the middle so the
   label sits on clean white and the colour reads as atmosphere creeping in
   from the edges rather than a gradient fill. The blobs are inline SVG rather
   than CSS radial-gradient because each carries a rotation and a non-uniform
   scale, which radial-gradient() cannot express.

   WHAT IS DIFFERENT HERE, and it is only this: the pill is ours, sized by
   .btn; the label is lifted by .probtn-l rather than their .t-pro-btn-label;
   and overflow is stated on the button, which .btn does not set and the wash
   needs in order to be clipped to the pill.

   THE CAR, honestly: this animates background-position and filter, which is
   the technique this stylesheet refuses by name three times over - for good
   reason, on a device that rasterises in software. The difference is
   arithmetic rather than principle. That refusal is about hundreds of card
   shimmers; this is ONE button, on screen at a time, about 9rem wide. One
   small pseudo-element is a cost the car can carry where four hundred is not.
   If it ever is not, --probtn-hue-dur and --probtn-drift-dur are the levers,
   and setting animation: none leaves a still rim glow that still reads.

   Licensed from transitions.dev (Pro) on 2026-09-07. */
/* The pill is the ORDINARY button's colour, not the primary's white, and that
   is the whole difference between seeing this effect and not. The recipe is
   drawn for a white pill because it sells an upgrade on a white page; here the
   page is black, and seven colour washes at 70% over white come out as pastel
   smudges - measured side by side, the same wash on the dark pill reads as
   actual colour.

   .primary is KEPT on the element and overridden here rather than dropped:
   other rules and the actions row select on it, and this is a colour decision
   rather than a demotion. The button is still the primary one - the glow is
   what says so now, which is a better job than white was doing.

   Strength goes to 1 for the same reason. On white the mask had to protect a
   dark label from the wash; on dark the wash IS the point and there is nothing
   to protect it from. */
.probtn.primary {
  background: rgba(255, 255, 255, .13);
  color: var(--fg);
}
.probtn.primary:hover {
  background: rgba(255, 255, 255, .2);
  box-shadow: none;
}
.probtn {
  --probtn-strength: 1;
  --probtn-core: 50%;
  --probtn-core-blur: 150%;
  --probtn-blur: 6px;
  --probtn-hue-dur: 4000ms;
  --probtn-drift-dur: 5000ms;
  position: relative;
  overflow: hidden;
}
/* The label rides above the wash. Its own flex, because the button's gap
   belongs to the button's children and the icon and the word are now one
   child rather than two. */
.probtn-l {
  position: relative; z-index: 1;
  display: inline-flex; align-items: center; gap: .45rem;
}
.probtn::before {
  content: ''; position: absolute; inset: 0;
  border-radius: inherit; pointer-events: none;
  background-image:
    url("data:image/svg+xml;utf8,<svg viewBox='0 0 67 42' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%' width='100%' fill='url(%23grad)' opacity='0.88'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(0.34726 -1.3118e-7 8.6482e-7 0.89585 33.5 21)'><stop stop-color='rgba(0,110,245,1)' offset='0'/><stop stop-color='rgba(0,110,245,0)' offset='1'/></radialGradient></defs></svg>"),
    url("data:image/svg+xml;utf8,<svg viewBox='0 0 67 42' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%' width='100%' fill='url(%23grad)' opacity='0.5'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(-1.3 0.25 -1.2633 -2.7471 33.5 21)'><stop stop-color='rgba(0,110,245,1)' offset='0'/><stop stop-color='rgba(0,110,245,0)' offset='1'/></radialGradient></defs></svg>"),
    url("data:image/svg+xml;utf8,<svg viewBox='0 0 67 42' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%' width='100%' fill='url(%23grad)' opacity='0.4'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(1.05 -0.8 0.46018 3.6184 33.5 21)'><stop stop-color='rgba(204,0,167,1)' offset='0'/><stop stop-color='rgba(170,0,204,0)' offset='0.71709'/></radialGradient></defs></svg>"),
    url("data:image/svg+xml;utf8,<svg viewBox='0 0 67 42' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%' width='100%' fill='url(%23grad)'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(-0.51067 -3.3275e-16 -4.3352e-14 -1.1396 33.5 21)'><stop stop-color='rgba(204,0,167,1)' offset='0'/><stop stop-color='rgba(170,0,204,0)' offset='1'/></radialGradient></defs></svg>"),
    url("data:image/svg+xml;utf8,<svg viewBox='0 0 67 42' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%' width='100%' fill='url(%23grad)'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(-0.45 -0.75 1.9052 -0.7091 33.5 21)'><stop stop-color='rgba(255,173,85,1)' offset='0'/><stop stop-color='rgba(255,173,85,0)' offset='1'/></radialGradient></defs></svg>"),
    url("data:image/svg+xml;utf8,<svg viewBox='0 0 67 42' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%' width='100%' fill='url(%23grad)'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(1.25 -0.35 -0.4013 3.5959 33.5 21)'><stop stop-color='rgba(0,204,68,1)' offset='0'/><stop stop-color='rgba(0,196,102,0.75)' offset='0.25'/><stop stop-color='rgba(0,187,136,0.5)' offset='0.5'/><stop stop-color='rgba(0,170,204,0)' offset='1'/></radialGradient></defs></svg>"),
    url("data:image/svg+xml;utf8,<svg viewBox='0 0 67 42' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%' width='100%' fill='url(%23grad)'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(-0.87391 0.73043 -4.1482 -1.9503 33.5 21)'><stop stop-color='rgba(0,170,204,1)' offset='0'/><stop stop-color='rgba(0,170,204,0)' offset='1'/></radialGradient></defs></svg>");
  background-size: 180% 180%, 180% 180%, 180% 180%, 180% 180%,
                   180% 180%, 180% 180%, 180% 180%;
  background-repeat: no-repeat;
  /* Rim only. The ellipse is deliberately smaller than the pill so the opaque
     end of the ramp is reached INSIDE the border - sized past the edge it gets
     clipped and the whole wash goes near-invisible. */
  -webkit-mask-image: radial-gradient(ellipse 46% 46% at 50% 50%,
    transparent var(--probtn-core),
    #000 calc(var(--probtn-core) + var(--probtn-core-blur)));
  mask-image: radial-gradient(ellipse 46% 46% at 50% 50%,
    transparent var(--probtn-core),
    #000 calc(var(--probtn-core) + var(--probtn-core-blur)));
  opacity: var(--probtn-strength);
  animation:
    t-pro-btn-hue var(--probtn-hue-dur) linear infinite,
    t-pro-btn-drift var(--probtn-drift-dur) ease-in-out infinite;
}
/* A full hue cycle, ending where it starts so the loop is seamless. The
   saturate keeps the washes from going pastel at the 180% blow-up; the blur
   turns blobs into atmosphere. */
@keyframes t-pro-btn-hue {
  from { filter: hue-rotate(0deg) saturate(1.3) blur(var(--probtn-blur)); }
  to   { filter: hue-rotate(-360deg) saturate(1.3) blur(var(--probtn-blur)); }
}
/* Each wash is pinned to the ring of edge stops and advances two stops per
   keyframe: one lap of the pill per cycle. */
@keyframes t-pro-btn-drift {
  0%, 100% {
    background-position: 100% 0%, 0% 50%, 100% 50%, 0% 100%, 0% 0%, 50% 0%, 50% 100%;
  }
  25% {
    background-position: 100% 100%, 50% 0%, 50% 100%, 0% 0%, 100% 0%, 100% 50%, 0% 50%;
  }
  50% {
    background-position: 0% 100%, 100% 0%, 0% 100%, 100% 0%, 100% 100%, 100% 100%, 0% 0%;
  }
  75% {
    background-position: 0% 0%, 100% 100%, 0% 0%, 100% 100%, 50% 100%, 0% 100%, 100% 50%;
  }
}
/* Required by the recipe, and the right behaviour anyway: the wash holds its
   first frame, which is still a premium rim glow and no longer moves. */
@media (prefers-reduced-motion: reduce) {
  .probtn::before { animation: none; }
}

/* ---------------------------------------------------------------- rails -- */
.rail { margin: 0 0 var(--row-gap); }
/* One section heading, everywhere: rails, sheets, person pages. They had
   drifted into two near-identical rules that then disagreed. */
.rail h2, .sec {
  display: flex; align-items: baseline; gap: .6rem; margin: 0 0 .6rem;
  font-size: 0.8rem; font-weight: 800; letter-spacing: .18em;
  text-transform: uppercase; color: var(--dim);
}
.rail h2 { padding: 0 var(--pad); }
/* .mt-note is the same thing one level out - the mast's description is a
   sibling of its heading rather than a child, so .sec .note cannot reach it. */
.rail h2 .note, .sec .note, .mt-note {
  font-size: 0.7333rem; color: var(--dimmer); font-weight: 600;
  letter-spacing: .04em; text-transform: none;
}

/* ---- the wait meter, in a rail's own heading ----

   One segment per thing the row is still waiting on. Same language as the
   player's stage meter - unlit segments are flattened and dimmed, a landed one
   stands up to full height - because they mean the same thing and there is no
   reason for a reader to learn it twice.

   currentColor, so it is the heading's own grey and cannot drift from the
   words beside it. Opacity and transform only, which is all the car composites.
*/
.rail h2 .note .rwm {
  display: inline-flex; align-items: center; gap: .22rem;
  margin-right: .5rem; vertical-align: middle;
}
.rail h2 .note .rwm i {
  width: .8rem; height: .22rem; border-radius: 99px; background: currentColor;
  opacity: .35; transform: scaleY(.5);
  transition: opacity .18s linear, transform .18s linear;
}
.rail h2 .note .rwm i.on { opacity: 1; transform: none; }
.rail h2 .note .rws { vertical-align: middle; }
@media (prefers-reduced-motion: reduce) {
  /* Steps rather than eases. The meter still says what it said. */
  .rail h2 .note .rwm i { transition: none; }
}
/* The headroom lives HERE, on the element that does the clipping, rather than
   on a list of the rows that happen to use it.

   It was a list first, and the list was wrong twice within an hour: the cast
   strip and More like this are .track outside a .rail, and so is the franchise
   rail, which is built as a bare <div class="track"> with a .sec heading. Each
   one got found by being seen cut off. A .track is a box with a lid on it -
   overflow-x: auto forces overflow-y to auto - so every .track needs the room,
   and the ones that want MORE top padding add it to this. */
.track {
  display: flex; gap: .8rem; overflow-x: auto;
  padding-top: var(--lift-room);
  margin-top: calc(var(--lift-room) * -1);
  scroll-snap-type: x proximity; scroll-behavior: smooth;
}
.rail .track {
  /* The .75rem is --lift-room; see the token. The .3rem is this row's own
     top gap and is still the number to change if the rhythm needs changing. */
  padding: calc(.3rem + var(--lift-room)) var(--pad) .7rem;
  margin-top: calc(var(--lift-room) * -1);
  scroll-snap-type: x proximity; scroll-behavior: smooth;
  /* Without this the first card snaps to the scrollport edge and hides under
     the padding - it read as a clipped poster at the left edge of every rail. */
  scroll-padding-left: var(--pad);
}
/* In a rail a card is a fixed width; in a grid it fills its cell. Reusing the
   rail width inside .grid made cards wider than their column, so titles ran
   over their neighbours. */
.card { flex: 0 0 auto; width: var(--rail-card); scroll-snap-align: start; cursor: pointer; }
.grid > .card { width: auto; flex: initial; }
.card .artbox {
  position: relative; border-radius: var(--r-m); overflow: hidden;
  background: var(--bg2);
  /* How far a corner overlay sits in from the frame.
   *
   * It has to be read against the CORNER RADIUS, not against the straight
   * edge, and that is what the old numbers missed. On a rounded corner of
   * radius R the arc passes 0.293R in from the corner on the diagonal, so a
   * badge inset by less than that is sitting inside the curve no matter how
   * much clearance it looks like it has from the flat edges. Measured on a
   * phone: radius 13.8px, arc 4.0px in, badge inset 5.5px - one and a half
   * pixels of daylight, which is what "too close to the edges" was.
   *
   * --r-m is 1em and .artbox does not set a font-size, so this rem tracks the
   * radius through --ui and every breakpoint. Written once here because three
   * overlays share the corners and they had drifted to three different
   * numbers: .4rem, .4rem and .3rem. */
  --mark-inset: .6rem;
  /* .32s for the sink, .25s for the shadow. These were two rules for one
     selector - a later `transition: transform .32s` that silently dropped
     box-shadow, so the hover shadow snapped on while the lift eased. */
  transition: transform .32s var(--ease), box-shadow .25s var(--ease);
}
/* A held card sinks, so the hold is felt before the menu arrives. Transform
   only - it is the one thing the car composites cheaply, and the class is put
   on 180ms in rather than delayed in CSS because the reduced-motion block
   zeroes every delay and would flash this on an ordinary tap. */
.card.holding .artbox { transform: scale(.955); }
/* The same sink on a term chip. Held presses reached this screen after the
   chips did, and without this the chip is the one thing in the app that opens
   a menu with no warning that it is about to. */
.sr-term.holding { transform: scale(.955); }
/* The same signal on the two shapes that are not cards. They sink as a whole
   rather than by their picture, because neither has a frame the size of
   itself to scale - the row IS the thing. Sized against each one's own
   :active, so a hold reads as more of a press rather than as a different
   effect. */
.drow, .tl-it { transition: transform .32s var(--ease); }
.drow.holding { transform: scale(.985); }
.tl-it.holding { transform: scale(.985); }
.card .art { width: 100%; aspect-ratio: 2/3; object-fit: cover; }
.card.wide .art { aspect-ratio: 16/9; }
.card .artbox::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  box-shadow: inset 0 0 0 .06em var(--line);
  transition: box-shadow .25s var(--ease);
}
.card:hover .artbox { transform: translateY(-4px); box-shadow: var(--shadow); }
.card:hover .artbox::after { box-shadow: inset 0 0 0 var(--ring-w) var(--ring-hover); }
.card:active .artbox { transform: translateY(-1px) scale(.98); }
/* The caption has a KNOWN height, and that is what lets a placeholder be the
   same size as the card it stands in for.

   min-height in em, not px: line-height 1.3 is unitless, so a clamped line box
   is exactly 1.3em of .t's own font-size and two lines are exactly 2.6em. .s
   inherits the unitless 1.55 from :root, so one line is 1.55em. The same two
   declarations therefore resolve correctly at the desk size AND at the coarse
   branch further down, which raises both font sizes - no second rule to keep
   in step, and no magic pixel figure to go stale when the type is retuned.

   min-height rather than height: the -webkit-box clamp already makes two lines
   a ceiling, so this is a floor that is never also a clip - raising the clamp
   to three later would still work.

   In a rail this changes almost nothing, because .track is a flex row and its
   cards already stretch to the tallest title. In a grid it is the difference
   between a row pitch that changes as you scroll and one that does not, which
   is the same argument .epsec .epc-cap already makes with its fixed 3.9rem. */
.card .t {
  margin-top: .45rem; font-size: 0.8333rem; font-weight: 650; line-height: 1.3;
  min-height: 2.6em;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.card .s {
  font-size: 0.7667rem; color: var(--dimmer); font-weight: 600;
  min-height: 1.55em;
}
.card.wide { width: var(--wide-card); }
.grid > .card.wide { width: auto; }
.card .bar {
  position: absolute; left: 8%; right: 8%; bottom: .45rem; height: 3px;
  border-radius: 99px; background: rgba(255,255,255,.25);
}
.card .bar i { display: block; height: 100%; border-radius: 99px; background: #fff; }

/* -------------------------------------------------------------- details -- */
.detail { padding-bottom: 3.5rem; }
.detail .backbar {
  position: fixed; top: .85rem; left: .85rem; z-index: var(--z-backbar);
}
.iconbtn {
  width: 2.5rem; height: 2.5rem; border-radius: 50%;
  background: rgba(255,255,255,.10); backdrop-filter: var(--blur);
  display: grid; place-items: center; color: var(--fg);
  transition: background .2s, transform .15s var(--ease);
}
.iconbtn svg { width: 1.15rem; height: 1.15rem; }
.iconbtn:hover { background: rgba(0,0,0,.92); }
.iconbtn:active { transform: scale(.9); }

/* The bottom was 0, so the page stopped dead on the last rail - about 59px
   of that rail's own margin and then the edge of the document. Everything
   else that ends a scroll in this app leaves room: the settings sheet takes
   4rem, and the mobile nav gets 3.9rem of clearance from .screen. This is the
   same gesture for the same reason. */
.detail .sheet { padding: 1.2rem var(--pad) 3rem; }

/* Text keeps a readable measure; the horizontally scrolling rails do not -
   they were being squeezed into the text column and clipped at 54rem while
   the rest of a wide screen sat empty.
   .actSlot and .kwrow were missing from this list and are neither rails nor
   grids: measured at 1440, the activity rows ran 1338px and the keyword chips
   1008px while the facts grid directly above them stopped at 907. A timeline
   row is a date, a label and nothing else - stretched across a wide screen it
   is two words at opposite ends of the page. */
.detail .facts-grid, .detail .rows,
.detail .actSlot, .detail .kwrow { max-width: 54rem; }
/* Padded by the sheet, like everything else in it. Switching the person page
   off Timeline and onto a poster view moved the whole wall one --pad right,
   because .grid brings its own gutter for the screens where it is the only
   one. Same reasoning as .boxo and .kwrow above. */
.detail .sheet .grid { padding-left: 0; padding-right: 0; }
.detail .sec, .sheet .sec { margin: 1.6rem 0 .6rem; }
/* Search results sit outside a .sheet, so their heading needs the page gutter
   the rails get from .rail h2. */
/* --------------------------------------------------------------- player -- */
.player { position: fixed; inset: 0; z-index: var(--z-player); background: #000; animation: fadein .3s var(--ease); outline: none; }
.player video { width: 100%; height: 100%; background: #000; }
.player.idle { cursor: none; }


.player .ptop, .player .pbottom {
  position: absolute; left: 0; right: 0; z-index: 3;
  opacity: 1; transition: opacity .3s var(--ease);
}
.player.idle .ptop, .player.idle .pbottom { opacity: 0; pointer-events: none; visibility: hidden; transition: opacity .3s var(--ease), visibility 0s linear .3s; }
.player .ptop {
  top: 0; display: flex; align-items: center; gap: .8rem;
  padding: .85rem 1rem 2.4rem;
  background: linear-gradient(180deg, rgba(0,0,0,.75), transparent);
}
.player .ptop .pt { font-size: 0.9333rem; font-weight: 700; text-shadow: 0 1px 8px rgba(0,0,0,.85); }
.player .pbottom {
  bottom: 0; padding: 2.4rem 1.1rem .9rem;
  background: linear-gradient(0deg, rgba(0,0,0,.82), transparent);
  display: flex; flex-direction: column; gap: .55rem;
}
/* Where you are, in words, above the bar that says it in pixels. It carries
   [hidden] whenever the file has no chapters - which is all but 32 items in
   this library - and a hidden flex child contributes no gap, so the bar does
   not shift for everything else. --pbH is remeasured by a ResizeObserver, so
   the menus and the skip button that sit on top of .pbottom follow it up and
   down on their own. */
.player .pchap {
  font-size: 0.8667rem; font-weight: 700; color: var(--fg);
  text-shadow: 0 1px 8px rgba(0,0,0,.85);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.player .iconbtn { background: rgba(0,0,0,.4); }
.player .ptop .iconbtn { background: rgba(0,0,0,.55); }
/* backdrop-filter over a PLAYING video is per-frame GPU readback + blur of the
   decoded frame underneath - six of them (close, play, subs, audio, speed,
   fullscreen) is the most expensive thing on screen during the app's core
   function. The gradient scrims already supply the contrast; the blur bought
   nothing here. */
.player .iconbtn, .player .btn, .player .pmenu,
.player .skipintro, .player .nextup,
.player .caststrip { backdrop-filter: none; -webkit-backdrop-filter: none; }
.player .pmenu { background: rgba(15,15,15,.97); }
.player .skipintro { background: rgba(0,0,0,.78); }

/* scrubber */
/* The bar stays slim; the thing you hit is the wrapper, which is finger-sized.
   Aiming at a 5px line from a car seat was not realistic. */
/* The bar stays slim and the thing you HIT is the wrapper, which has to be
   finger-sized: aiming at a six-pixel line from a car seat was never going to
   work. What is new is that it now actually clears the floor there.
   1.9rem over 1.35rem is 54.75px at the default interface size, and this is
   the first time this target has passed the house 52px minimum on the car. It
   was 39px. The bump that fixed it existed - and lived inside
   @media (max-width: 700px), which a 1307px dashboard does not match, and
   neither does a landscape phone at 844x390. That is the trap this repo has
   already recorded once: a touch floor hiding in the phone block.
   The top padding is also the gutter the segment marks draw in.
   Margin-top eases from -.6rem to -.35rem so the box stays inside .pbottom's
   own .55rem flex gap and never reaches into the chapter line above it. */
.player .scrubwrap {
  position: relative; padding: 1.9rem 0 1.35rem; margin: -.35rem 0 -.4rem;
  cursor: pointer; touch-action: none;
}
.player .scrub {
  position: relative; height: 6px; border-radius: 99px;
  background: rgba(255,255,255,.22);
  transition: height .15s var(--ease);
}
.player .scrubwrap:hover .scrub, .player.dragging .scrub { height: 9px; }
.player .buf { position: absolute; inset: 0; width: 0; border-radius: 99px; background: hsla(0,0%,50%,.5); }
.player .played { position: absolute; inset: 0; width: 0; border-radius: 99px; background: var(--fg); }
/* Always drawn. It used to appear on :hover, which does not exist on a
   touchscreen - so in the car there was no handle at all, just a hairline. */
.player .knob {
  position: absolute; top: 50%; left: 0; width: 15px; height: 15px; border-radius: 50%;
  background: #fff; transform: translate(-50%, -50%);
  box-shadow: 0 0 0 3px rgba(0,0,0,.45);
  transition: transform .15s var(--ease);
}
.player.dragging .knob { transform: translate(-50%, -50%) scale(1.45); }
.player .marks { position: absolute; inset: 0; pointer-events: none; }
.player .marks i { position: absolute; top: 0; bottom: 0; width: 2px; background: rgba(0,0,0,.55); }

/* ---- where the markers are ----
 * IN the bar, as a stretch of different road, and the reason it is a texture
 * rather than a shade is arithmetic. The track is white at .22 and the buffer
 * is grey at .5, which over black are 56 and 64 - they measure 1.13:1 against
 * each other. Any solid tone dropped between them is a fourth flat grey that
 * cannot be told from the buffer, which moves around unpredictably as the file
 * loads. A DASH can: nothing else on this bar is anything but solid, so the
 * broken edge reads as a different KIND of thing rather than a fifth value on
 * one scale. No hue is spent, which the palette here has none to give.
 *
 * Under the buffer and the played fill on purpose. As you watch through an
 * intro the white fill eats the dashes from the left, so the part still
 * highlighted is the part still to come - which is the same fact the label
 * above the bar is counting down in words.
 */
.player .segs { position: absolute; inset: 0; border-radius: 99px; overflow: hidden; pointer-events: none; }
.player .seg {
  position: absolute; top: 0; bottom: 0;
  min-width: 3px;
  background: repeating-linear-gradient(90deg,
    rgba(255,255,255,.55) 0 3px, rgba(255,255,255,0) 3px 6px);
  opacity: .85; transition: opacity .2s var(--ease);
}
/* A marker the player has decided not to act on is still where somebody says
   the credits are, so it is still drawn - at half the weight, because nothing
   is going to happen when you reach it. */
.player .seg.unused { opacity: .4; }
/* Opacity only. This rasteriser composites opacity and transform and stutters
   on background and colour, which is why .pw-meter turns white down rather
   than reaching for a second one. */
.player .seg.near,
.player .scrubwrap:hover .seg,
.player.dragging .seg { opacity: 1; }

.player .pctrls { display: flex; align-items: center; gap: .3rem; }
/* A floor under the tap target, because the em dash shrank it to its own text.
   The button has no padding and no width of its own, so while the wait shows
   "4" it measured 15px across at the default interface size against 74.8px
   for "0:00 / 0:00" - a control that got harder to hit precisely because it
   stopped lying. flex: none is not optional beside it: a bare min-width beats
   a flex item's automatic minimum size, and on an overflowing 390px row the
   long "1:02:11 / 2:14:52" would then be squeezed into the floor and clipped
   off its own left edge. */
.player .pctrls .time {
  font-size: 0.8333rem; font-weight: 700; font-variant-numeric: tabular-nums;
  margin-left: .3rem; min-width: max(3.5rem, 52px); flex: none; text-align: left;
}
.player .pctrls .time em { opacity: .5; font-style: normal; }
.player .pctrls .pgrow { flex: 1; }
/* Sized for a finger at arm's length in a car, not a mouse on a desk. 33px
   was under every touch-target guideline going; these are 52, and the play
   button is bigger again because it is the one you reach for without looking. */
.player .pctrls .iconbtn { width: 3.25rem; height: 3.25rem; }
.player .pctrls .iconbtn svg { width: 1.6rem; height: 1.6rem; }
.player .pctrls .iconbtn.big { width: 4rem; height: 4rem; }
.player .pctrls .iconbtn.big svg { width: 2.1rem; height: 2.1rem; }
.player .pctrls { gap: .5rem; padding-bottom: .2rem; }
.player .pctrls .time { font-size: 1rem; margin-left: .6rem; }
.player .pctrls .iconbtn.active { color: var(--accent); }

/* pop-up menu */
.player .pmenu {
  position: absolute; right: 1.1rem; bottom: 4.3rem; z-index: 5;
  min-width: 11rem; max-width: min(24rem, 78vw); max-height: 60vh; overflow-y: auto;
  /* No backdrop-filter: see the strip rule above. --panel is already
     rgba(15,15,15,.96) and the later declaration here was quietly
     winning on source order. */
  background: var(--panel);
  border: .06em solid var(--line-hi); border-radius: var(--r-m);
  padding: .35rem; box-shadow: var(--shadow); animation: rise .18s var(--ease);
}
.player .mrow {
  display: flex; align-items: center; gap: .5rem;
  width: 100%; text-align: left; padding: .55rem .7rem;
  border-radius: 8px; font-size: 0.8667rem; font-weight: 600; color: var(--dim);
  transition: background .15s, color .15s;
}
.player .mrow:hover { background: var(--elev2); color: var(--fg); }
.player .mrow.on { color: var(--accent); }
/* Pushed to the right edge now the row is a flex line, rather than trailing
   the text. With labels of wildly different lengths - "Off" against "English
   - Hearing Impaired - SUBRIP - External" - a tick that follows the words
   lands in a different place on every row and reads as clutter. */
.player .mrow.on::after { content: '✓'; margin-left: auto; padding-left: .5rem; }
.player .mrow .ml { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The right-hand column, which today is a chapter's start time. Never shrinks,
   so a long chapter name ellipsises against it rather than shouldering it off
   the row, and tabular figures so a column of times reads as a column. */
.player .mrow .mm {
  flex: none; font-size: 0.8rem; color: var(--dimmer);
  font-variant-numeric: tabular-nums; padding-left: .7rem;
}
.player .mrow.on .mm { color: var(--accent); }

/* ---- the flag column ----
   Square, because the vendored set is: kapowaz/square-flags redraws each flag
   to 1:1 rather than letterboxing a 3:2 one, which is what makes a column of
   them read as a column instead of a row of different-shaped stamps.

   Sized in em rather than rem so it tracks the row - the car media query
   further down puts .mrow up to 1.0667rem, and a flag pinned in rem would
   stay small while the text around it grew.

   overflow hidden is what actually rounds the corners; the image inside is a
   square with none of its own. */
.player .flag {
  position: relative; flex: none; display: block;
  width: 1.3em; height: 1.3em; border-radius: 3px; overflow: hidden;
}
.player .flag img { display: block; width: 100%; height: 100%; }
/* The same column the flags sit in, sized the same way - em rather than rem,
   so it grows with .mrow when the car query enlarges the row instead of
   staying small beside bigger text. */
.player .mico { flex: none; display: block; width: 1.3em; height: 1.3em; }
/* The hairline goes on a layer ABOVE the image, not as an inset shadow on the
   wrapper - an inset shadow paints behind its own content, so the image would
   sit on top of it and the edge would only appear on the empty variants. It
   is there for the pale flags: Japan is a white field with a red disc, and
   with no edge it reads as a white smear on a black panel. */
.player .flag::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .16); pointer-events: none;
}
/* A language nothing can name. The vendored set has an xx flag for this, but
   a track with no Language and an unreadable title never gets that far, and
   an empty box still has to hold the column open or every label below it
   steps sideways. */
.player .flag-blank { background: rgba(255, 255, 255, .04); }
/* Off. The same box struck through - it sits at the top of the language list
   and has to read as "none of these" rather than as a flag that failed. */
.player .flag-off {
  background: linear-gradient(to bottom right,
    transparent calc(50% - .035em), rgba(255, 255, 255, .42) calc(50% - .035em),
    rgba(255, 255, 255, .42) calc(50% + .035em), transparent calc(50% + .035em));
}
.player .msep {
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .16em; text-transform: uppercase;
  color: var(--dimmer); padding: .5rem .7rem .2rem; margin-top: .2rem;
  border-top: .06em solid var(--line);
}
.player .msep:first-child { border-top: 0; margin-top: 0; }

/* skip intro */
.player .skipintro {
  /* Above the controls, not across them. --pbH is the measured height of
     .pbottom, which changes with the viewport; the rem value is the fallback
     for the moment before the first measurement lands. */
  position: absolute; right: 1.4rem; bottom: calc(var(--pbH, 7.5rem) + .9rem); z-index: 4;
  /* No backdrop-filter over a playing video - see the strip rule. */
  background: rgba(0,0,0,.82);
  border: 1px solid rgba(255,255,255,.25); color: #fff;
  padding: .65rem 1.3rem; border-radius: 99px; font-weight: 800; font-size: 0.9rem;
  animation: rise .25s var(--ease); transition: background .2s, transform .15s var(--ease);
}
.player .skipintro:hover { background: rgba(0,0,0,.9); }
.player .skipintro:active { transform: scale(.96); }

/* ------------------------------------------------------- end credits -----
   When the credits roll the episode is over for anyone watching, so the frame
   goes to what comes next rather than to a card in the corner of it. The next
   episode's backdrop fills the screen, its title is set at a size that reads
   from the driver's seat, and the countdown is drawn twice: a number for "how
   long", a bar under the still for "how much longer" - the second one works
   without being read, which is the one that matters while driving. */
.player .nextup {
  position: absolute; inset: 0; z-index: 6;
  display: flex; align-items: flex-end;
  animation: fadein .45s var(--ease);
  /* Transparent, and the episode keeps playing behind it.
     It used to be flat black with the series' backdrop fading in over the top,
     which meant the one thing this screen exists to announce the end of - the
     episode, its credits still rolling - was the thing it covered up. The
     scrim below is what the panel is read against instead, and it was already
     written for that job: it darkens the lower left enough to read over a
     bright shot without dimming the shot. */
  background: none;
}
@keyframes fadein { from { opacity: 0; } to { opacity: 1; } }

/* Two gradients, not one: the vertical lifts the whole lower third so the
   panel has a floor, the horizontal keeps the left column dark enough to read
   over a bright shot without dimming the picture people are looking at. */
.player .nextup::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(to top, rgba(0,0,0,.96) 0%, rgba(0,0,0,.86) 26%,
                    rgba(0,0,0,.45) 52%, rgba(0,0,0,.12) 78%, transparent 100%),
    linear-gradient(to right, rgba(0,0,0,.8) 0%, rgba(0,0,0,.42) 38%, transparent 68%);
}

/* Everything below is sized as a share of the viewport, taken off the design
   this copies rather than picked: the title is ~3.1% of the width there, the
   label 2%, the still 22.5%. Fixed pixel sizes looked right on the desk and
   shrank to nothing on the car's 1307px screen, which is the only screen that
   matters. The ceilings only stop a 2560px monitor from becoming a billboard. */
.player .nextup .nu-panel {
  position: relative; z-index: 1;
  width: 100%; padding: 0 var(--pad) clamp(2rem, 5.5vh, 4rem);
}
.player .nextup .nu-panel > *,
.player .nextup .nu-meta > * { animation: rise .5s var(--ease) both; }
.player .nextup .nu-k { animation-delay: .06s; }
.player .nextup .nu-row { animation-delay: .12s; }
.player .nextup .nu-ep { animation-delay: .16s; }
.player .nextup .nu-t { animation-delay: .20s; }
.player .nextup .nu-o { animation-delay: .25s; }
.player .nextup .nu-len { animation-delay: .29s; }
.player .nextup .nu-btns { animation-delay: .33s; }

.player .nextup .nu-k {
  font-size: clamp(1.15rem, 2vw, 2.7rem); font-weight: 700;
  color: var(--dim); letter-spacing: -.015em; margin-bottom: 1rem;
}

.player .nextup .nu-row {
  display: flex; gap: clamp(1rem, 2.4vw, 2rem); align-items: flex-start;
}

.player .nextup .nu-thumb {
  position: relative; flex: none;
  width: clamp(12rem, 22.5vw, 30rem); aspect-ratio: 16/9;
  border-radius: var(--r-s); overflow: hidden;
  background: var(--bg2); box-shadow: var(--shadow-lg);
}
.player .nextup .nu-still { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The countdown as a filling bar. It sits on the still's own bottom edge, so
   it reads as that episode loading rather than as a separate widget. */
.player .nextup .nu-bar {
  position: absolute; left: 0; right: 0; bottom: 0; height: clamp(4px, .35vw, 7px);
  background: rgba(255,255,255,.25);
}
/* scaleX rather than width. This bar redraws about once a second for the
   length of a countdown, and it does it while the next episode is already
   being transcoded and decoded - the worst moment in the app to be asking
   for a reflow and a repaint. A pre-painted full-width bar squashed by a
   transform composites instead, and looks identical. */
.player .nextup .nu-bar i {
  display: block; height: 100%; width: 100%; background: #fff;
  transform: scaleX(0); transform-origin: left;
  transition: transform 1s linear;
}

.player .nextup .nu-meta { min-width: 0; flex: 1; }
.player .nextup .nu-ep {
  font-size: clamp(.85rem, 1.15vw, 1.35rem); font-weight: 600;
  color: var(--dim); letter-spacing: .02em;
}
.player .nextup .nu-t {
  margin: .1rem 0 0; font-weight: 800; color: #fff;
  font-size: clamp(1.9rem, 3.1vw, 4.8rem); line-height: 1.04; letter-spacing: -.03em;
}
.player .nextup .nu-o {
  margin: .5rem 0 0; color: var(--dim);
  font-size: clamp(.85rem, 1.15vw, 1.35rem); line-height: 1.45; max-width: 44rem;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.player .nextup .nu-len {
  margin-top: .4rem; color: var(--dim);
  font-size: clamp(.85rem, 1.15vw, 1.35rem); font-weight: 600;
}

.player .nextup .nu-btns { display: flex; gap: .6rem; margin-top: clamp(1.1rem, 2vh, 1.8rem); }
/* Sized for a finger at arm's length, like the rest of the player. */
.player .nextup .nu-play,
.player .nextup .nu-x {
  border: 0; cursor: pointer; color: var(--accent-ink);
  background: var(--accent-deep); border-radius: var(--r-s);
  min-height: clamp(3.4rem, 3.2vw, 4.4rem); font: inherit; font-weight: 700;
  transition: transform .12s var(--ease), background .2s;
}
.player .nextup .nu-play {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: 0 clamp(1.2rem, 1.6vw, 2rem); font-size: clamp(.95rem, 1.25vw, 1.45rem);
}
.player .nextup .nu-play svg { width: 1.1em; height: 1.1em; }
.player .nextup .nu-x {
  width: clamp(3.4rem, 3.2vw, 4.4rem); display: grid; place-items: center;
  background: rgba(255,255,255,.82);
}
.player .nextup .nu-x svg { width: clamp(1.1rem, 1.2vw, 1.6rem); height: clamp(1.1rem, 1.2vw, 1.6rem); }
.player .nextup .nu-play:hover, .player .nextup .nu-x:hover { background: #fff; }
/* Once tapped it stops being a button, but it must not start looking broken -
   the UA's grey-out on :disabled reads as "this failed", and it is the last
   thing on screen before the next episode. It stays lit and says what it is
   doing instead. */
.player .nextup .nu-play:disabled { opacity: 1; background: #fff; cursor: default; }
.player .nextup .nu-play:active, .player .nextup .nu-x:active { transform: scale(.94); }
.player .nextup .nu-count { font-variant-numeric: tabular-nums; }

/* While it is up, the transport underneath is not what anyone is aiming at -
   and .taps covers the whole frame, so leaving it live means a tap meant for
   Play seeks the episode nobody is watching any more. */
.player.nextup-on .ptop,
.player.nextup-on .pbottom,
.player.nextup-on .skipintro,
.player.nextup-on .taps { display: none !important; }

/* The hero body is bottom-anchored and grows upward, which is right on a wide
   screen where there is room above it and wrong on a phone where there is not.
   At this width the hero sits on its 26rem floor - 16:9 of a phone is far
   shorter than that - while the stack inside it is a logo, two wrapped rows of
   genres, the plot, the cast strip and a button. That is taller than the box,
   so the top of it ran up underneath the floating header and the logo came out
   sitting behind the nav.
   Three things, in order of how much they matter. The body gets an explicit
   top, so overlap is impossible by construction rather than by arithmetic that
   holds until a title has a longer name. The hero gets room, so nothing has to
   be clipped to respect that top. And the two greediest items - the plot and
   the logo - are trimmed, because the alternative is a hero that is mostly
   text on the screen with the least room for it. */
@media (max-width: 700px) {
  /* Scoped to .heroSlot, which is where the HOME hero lives and the only
     place these rules are true. Unscoped, `top` here reached the detail
     pages' hero too - whose .body is position:relative, not absolute, so an
     explicit top SHIFTS it rather than anchoring it: the whole stack slid
     ~64px down and the buttons were clipped to white half-pills at the
     hero's bottom edge. Found on the Silo page, on the phone.

     Enough for the stack and the header above it, not a screenful. The
     content needs about 360px; 54px of header, and a floor of 30rem so a
     short title still gets a hero rather than a strip. 86vh was the first
     try and swallowed the page - Continue watching has to stay in view. */
  .heroSlot .hero { height: clamp(30rem, 64vh, 38rem); }
  /* The top clamp is now unconditional, above. Only the tighter bottom is
     particular to a phone. */
  .heroSlot .hero .logo { max-height: 5.4rem; }
  /* And its trailer size, here rather than up with the rest of the trailing
     rules, because the line above is (0,3,0) and so is `.hero.trailing .logo`
     - equal weight, and this one is further down the file, so the shrink was
     being quietly ignored on a phone while working everywhere else. Kept
     beside the number it has to stay in proportion to. */
  .heroSlot .hero.trailing .logo { max-height: 3.2rem; }
  /* These three are about A HERO ON A PHONE, not about .heroSlot's absolute
     layout, and the detail page is where a hero is tallest - so it is the one
     that needed them most and the one that never got them.

     Measured on a 414px phone before this: the detail hero was 454px, 50.6% of
     the screen, with the logo at its full 7.5rem, three lines of synopsis and
     the genre chips wrapping the meta line to two. The `top` in the rules
     above stays scoped, because that one really is particular to a body that
     is absolutely positioned - unscoped it slid the detail stack 64px down and
     clipped the buttons, which is what the note above records. */
  .heroSlot .hero .plot, .detail .hero .plot { -webkit-line-clamp: 2; }
  .heroSlot .hero .meta, .detail .hero .meta { gap: .35rem; font-size: 0.8rem; }
  .detail .hero .logo { max-height: 5.4rem; }

  /* ---- the rail heading, on a phone ----
     It is three columns - the title, a sentence describing the row, and an
     "all N" link - laid out as one flex line. At 375px all three shrink
     until each wraps internally, so a heading that is one line on a desktop
     becomes six on a phone and you scroll past a paragraph to reach the
     first poster. Measured on "Critics love, you will too": two lines of
     title, two of description, two of link.

     The description goes. The title is the label and the sentence is a gloss
     on it; a gloss is the first thing to cut when there is no room, and the
     row underneath says the rest.

     The letter-spacing comes down with it. .18em on uppercase is a third of
     the width again, and it is the reason a three-word title needed two
     lines. */
  /* The description drops to its own line and becomes a subtitle. Side by
     side, the three columns each shrank until each wrapped INSIDE itself -
     two lines of title, two of description, two of link, for one heading.
     Stacked, the sentence survives and costs one line instead of four.

     order puts it after the link even though it comes before it in the
     markup, so line one stays title + "all N" and the sentence takes line
     two on its own. Scoped to the home rows: on a detail page the same
     .note is a count like "17" and belongs beside its title, not under it. */
  .rail h2, .hs h2 { flex-wrap: wrap; }
  .rail h2 .note, .hs h2 .note {
    display: block; order: 1; flex: 0 0 100%;
    margin-top: .2rem; line-height: 1.4;
    font-size: 0.7rem; letter-spacing: .01em;
  }
  /* A mast row is logo, then title, then the description under them both.
     The description used to live inside the h2, and the h2 is the second
     flex item beside the logo - so wrapping it to "full width" only ever
     meant full width of the title's column, and it came out indented under
     the title with the logo sitting to its left. It is a sibling of the
     heading now, so the width it fills is the row's.

     Reading order is the argument: the mark says which genre, the title says
     which cut of it, and the sentence qualifies both. Hanging it off the
     title alone said it only qualified the title. */
  .mt-head { flex-wrap: wrap; }
  .mt-note {
    flex: 0 0 100%; margin-top: .2rem; line-height: 1.4;
    font-size: 0.7rem; letter-spacing: .01em;
  }

  /* The link takes the far end of the title line rather than trailing the
     words, so it lands in the same place on every row - measured flush at
     the 12px gutter on all of them.

     Qualified with .screen only to outrank the base .rail h2 .go, which sits
     LATER in this file at equal specificity and otherwise wins and puts its
     own margin back. In one hand-written stylesheet with no build step,
     source order is part of the API. */
  .screen .rail h2 .go { margin-left: auto; white-space: nowrap; }

  /* .18em on uppercase is about a third of the width again, and it was the
     reason a three-word title needed two lines on its own. */
  .rail h2, .sec { letter-spacing: .1em; }
  /* Genres come off the hero on a phone. With them the meta line wrapped to
     two rows and stranded a separator at the end of the first - the dots are
     their own elements, so one is left behind whenever the row after it
     wraps. Year, score and the status badge answer "what is this"; the
     genres are one tap away on the page this button opens. The certificate
     chip stays: it is two characters and it is the one that changes whether
     you press play. */
  .heroSlot .hero .meta .tag:not(.cert),
  .detail .hero .meta .tag:not(.cert) { display: none; }

  /* ---- the hero's actions, at a size a thumb can hit ----
     Measured at 414px: 36px tall, with no min-height at all, against the 44px
     Apple asks for and the 3.5rem this file already gives every control on the
     car. The one screen where you decide whether to watch something had the
     smallest targets in the app.

     3.2rem is 44px at the phone's 13.8px root. The padding shrinks as the
     height grows, so five buttons still make two rows rather than three -
     the row costs about 17px more and the trimmed logo, synopsis and meta
     line give back four times that. */
  /* Unscoped from .hero for the same reason the row above it was, and this is
     the one that fails silently: left as `.hero .btns .btn`, a row that has
     moved into the sheet falls back to .btn's own `padding: .6rem 1.3rem` and
     goes back to about 31px - reintroducing exactly the defect the note above
     was written to fix, on exactly the screen it was measured on, with nothing
     visibly broken. (0,2,0) is enough here: .btn.primary and .btn.iconlabel set
     no box metrics, and .sd-more .btn and .cfm-row .btn cannot co-apply.
     The fourth rule this list used to name, .hero.trailing .btns .btn, no
     longer exists - see the note beside the coarse-pointer floor above. */
  .btns .btn {
    min-height: 3.2rem; padding: 0 1.05rem; font-size: 0.8667rem;
  }
  .btns { gap: .45rem; }

  /* The floor moved to the coarse block above, with .btns .btn and for the
     same reason. What is left here is the padding trim, which IS about width:
     this button sits beside another and at 393px they need the room. The
     floor matters more here than on most controls - one press writes a dozen
     episodes to a remote record, so a mis-tap is expensive to undo. */

  /* The company mark gets the cell rather than half of it. There is one of
     them on a phone now, so the only thing it shares the line with is the
     "+N" - and 47% of a 178px cell was eleven characters of a studio name. */
  .facts-grid .fv-co .mark { max-width: calc(100% - 2.2rem); }
  .facts-grid .fv-co .mark:only-child { max-width: 100%; }
  /* And the separator that belonged to each one. The dots are their own
     elements, so hiding a tag leaves its dot behind - the first attempt at
     this ended the line with "8.1 · ·", which is more litter than the tags
     were. :has() picks the dot immediately before a hidden tag, which is
     exactly the two that go and leaves the one between year and score. */
  .heroSlot .hero .meta .dot:has(+ .tag:not(.cert)),
  .detail .hero .meta .dot:has(+ .tag:not(.cert)) { display: none; }

  /* A still card that finally has a phone size. 15.5rem is 260px, which on a
     375px screen is 1.35 cards - a row that shows one and a third of an item
     does not read as a row at all, and you cannot tell it scrolls. At 11.5rem
     you get one whole card and most of the next, which is what says "there is
     more this way". */
  :root { --wide-card: 11.5rem; }

  .player .nextup .nu-row { flex-direction: column; gap: .9rem; }
  .player .nextup .nu-thumb { width: min(16rem, 52vw); }
  .player .nextup .nu-o { -webkit-line-clamp: 3; }
}
@media (prefers-reduced-motion: reduce) {
  .player .nextup, .player .nextup .nu-panel > *, .player .nextup .nu-meta > * { animation: none; }
  .player .nextup .nu-bar i { transition: none; }
}

/* ------------------------------------------------- the player's wait --
 *
 * This was a ring and the word "Preparing...", centred, for anything between
 * one second and thirty-five - and after one setTimeout at six seconds,
 * nothing on the screen changed again for twenty-nine consecutive seconds.
 * Both halves were wrong. A ring is the glyph every broken page shows, so it
 * reads as a fault; and centring puts the app's anxiety in the middle of the
 * frame, which during a rebuffer is directly over the picture that never went
 * away.
 *
 * What this app has already proved about its own buffering is that the link is
 * the governor - six of seven investigations ended in "change nothing". So the
 * honest job is to say WHO IS BUSY and HOW BIG THE JOB IS, and to refuse to
 * draw a fraction for a wait that has no measurable progress.
 *
 * Nothing draws a bar for the cold open, because Jellyfin exposes no probe
 * progress: no byte counter, no read position, no event. Exactly one state has
 * a measurement - a mid-playback refill, where the seconds in front of the
 * playhead climb back from zero - and that state gets the only graphic here.
 *
 * Placement: bottom-left above the controls while the app is WORKING, which on
 * black is a slate and over a held frame is a caption. Centred only when the
 * player is DEAD, where there is no picture left to protect and the way out
 * must not be hiding in a corner.
 *
 * --pbH is the measured height of .pbottom, the same anchor .skipintro and
 * .po-corner use, so this clears the controls without knowing their height. */
.player .loading {
  position: absolute; inset: 0; z-index: 2; pointer-events: none;
  display: grid; align-items: end; justify-items: start;
  padding: 0 var(--pad) calc(var(--pbH, 7.5rem) + .9rem);
}
/* ---- the title card, on a cold open only ----
   The plate below stays exactly where it is and keeps its job; this fills the
   black above it with what was pressed. Two rows rather than a new overlay, so
   .pw-dead's centring rule is untouched - it is the one state that wants the
   plate in the middle, and a design that broke it would break the screen
   people see when a title genuinely fails.

   display: none in the base rule and turned on only by .pw-cold. That gate
   direction matters: a future edit that forgets the class shows nothing, which
   is today's screen, rather than painting a poster over a film that is
   rebuffering. */
.player .pw-art { display: none; }
/* minmax(0, 1fr) and not 1fr, which is the whole bug rather than a
   refinement: an fr track's automatic minimum is its CONTENT, so the poster
   set the floor for row one and pushed row two - the status plate, and at 22
   and 28 seconds the escape and the exit - down through the padding that
   exists to clear the control bar. The bar is z-index 3 against .loading's 2,
   so it painted over them and owned their taps. Measured on a phone held
   sideways at 844x390: at the largest interface size the plate ran 87px under
   the bar and elementFromPoint at the centre of "Try 1080p" returned the
   transport - the one control that gets you out of a failing wait, unpressable
   on the screen that offers it. With minmax(0, 1fr) every case clears by 26px
   or more. */
.player .loading.pw-cold:not(.pw-dead) {
  /* minmax(0, auto) and align-content, not 1fr. Both matter and for different
     reasons. The pair is one object now, so it is centred as a PAIR - an fr
     row absorbs every spare pixel and pins the card to the bottom of the
     screen with two thirds of it empty above. And the explicit 0 minimum is
     the old fix in its new form: an auto track's automatic minimum is its
     content, which is what once let the poster push this row's buttons under
     the control bar. Written as minmax(0, auto) the row still cannot do that. */
  grid-template-rows: minmax(0, auto) auto; align-content: center;
  /* THE SHARED COLUMN. .loading has one implicit column, and an auto track
     stretches to the container by default - which is what put the card in the
     middle and the readout hard against the left gutter, two objects on two
     axes. justify-content: center stops that stretch, so the track sizes to
     max-content - the wider of the two children - and centres as a track.
     justify-items: stretch then makes both children fill it. That is the whole
     mechanism: they share a measure because they share a column, nothing
     measures anything, and no JS is involved. */
  justify-content: center; justify-items: stretch;
}
.player .loading.pw-cold:not(.pw-dead) .pw-art {
  display: flex; align-items: center; justify-content: center; gap: 1.7rem;
  /* Bottom of its row rather than the middle of it, so the pictures sit down
     on the rule instead of floating a third of a screen above it. The gap is a
     margin and NOT a row-gap: a gap is subtracted before the fr track
     resolves, which is the same arithmetic that once pushed this row's buttons
     under the control bar. */
  align-self: end; justify-self: stretch;
  margin-bottom: clamp(1.4rem, 4vh, 2.8rem);
  max-width: min(58rem, 86vw);
}

/* ---- the light the picture throws ----
   Two lamps in the colours of whatever is on the card - the episode's own
   still, or the film's poster - so the black behind a cold open is the black
   that picture would cast rather than nothing at all.

   HUNG ON .pw-art, WHICH IS THE WHOLE GATE. That element is display:none in
   the base rule above, turned on only by .pw-cold:not(.pw-dead), and turned
   off again in the max-height:400px block. A pseudo-element of a display:none
   box is never generated, so all three of those states are inherited free and
   there is no second selector to keep in sync: a rebuffer keeps the film's own
   held frame untinted, the failure screen stays black, and the short-landscape
   case where the pictures are dropped drops the light with them.

   POSITIONED AGAINST .loading, NOT .pw-art. .pw-art is a centred flex box a
   few hundred pixels wide; the containing block for an absolutely positioned
   pseudo is the nearest POSITIONED ancestor, and .pw-art sets no position, so
   these resolve against .loading's padding box - the whole screen. Gating and
   geometry come from two different elements on purpose.

   z-index -1 keeps them inside .loading's stacking context (position absolute
   + z-index 2), so they paint above the video's own black and below every word
   on the card. They cannot reach the video, the taps layer or the controls.

   inset -8% because the layers MOVE. A gradient still carrying colour at its
   own box edge shows that edge the moment it translates. -8% is 105px of
   overhang across and 60px down at 1307x747, against a largest excursion of
   2.6% of the layer; the painted extent stays off screen at both ends of both
   animations.

   NO COLOUR, NO DECLARATION - the same failure shape as the index doors.
   rgba(var(--c1), .3) with --c1 unset is invalid at computed-value time and
   takes the whole background-image with it, so a picture that yields no colour
   leaves exactly the black screen that shipped before this. */
.player .pw-art::before,
.player .pw-art::after {
  content: ''; position: absolute; z-index: -1; inset: -8%;
  pointer-events: none; opacity: 0;
  transition: opacity .8s var(--ease);
}
/* The colours are read off the picture and arrive a moment after the card, so
   the light rises into place rather than appearing with it. */
.player .loading.pw-lit .pw-art::before,
.player .loading.pw-lit .pw-art::after { opacity: 1; }
/* Where the lamps are is a contrast decision, not a composition one. The card
   sits centred with the picture left and the words right, and .ptop/.pbottom
   never fade during a cold open - the idle class needs a playing video - so
   the top 99px and bottom 126px are already under 75% and 82% black. Both
   lamps therefore sit in the middle band, and the low one is centred on the
   PICTURE rather than the text: the brightest part of the field lands where
   the artwork is, which is both the point of the effect and the cheapest
   place to spend the contrast.

   Three stops, not two. A straight ramp to transparent leaves a visible
   contour ring on a software rasteriser; a mid stop at 43% carrying about
   .43 of the peak turns the falloff into an ease-out and spreads the same
   alpha over more pixels per step.

   --lit is the equal-ink term, and it is why the alphas below are multiplied
   rather than fixed. A banded colour still spans 15x in luminance across this
   library - a near-black navy at one end, a bright yellow at the other - so
   one alpha for all of them is two mistakes at once: at the bright end the
   episode line drops to 4.87:1 against a 4.5 floor, and at the dark end the
   wash is invisible and the work is wasted. The colour is left exactly as
   _band produced it and only the amount of it changes, so this is not a second
   colour register - it is the same idea as IX_INK, which sizes a wordmark by
   its ink rather than its box. Measured worst cases are in the commit. */
.player .pw-art::before {
  background-image: radial-gradient(58% 52% at 30% 62%,
    rgba(var(--c1), calc(.30 * var(--lit))) 0%,
    rgba(var(--c1), calc(.13 * var(--lit))) 43%, transparent 74%);
  animation: pwlamp1 23s var(--ease) infinite;
}
.player .pw-art::after {
  background-image: radial-gradient(50% 46% at 76% 34%,
    rgba(var(--c2, var(--c1)), calc(.20 * var(--lit))) 0%,
    rgba(var(--c2, var(--c1)), calc(.086 * var(--lit))) 43%, transparent 72%);
  animation: pwlamp2 19s var(--ease) infinite;
}
/* Transform only, which is the law for the car - it composites transform and
   opacity and nothing else, and animated background-position is refused by
   name three times in this file for exactly this reason.

   `from` and `to` are the SAME frame and it is the REST pose, with the
   excursion in the middle. That is not a stylistic choice: the two global
   reduced-motion blocks set animation-duration to .01ms AND iteration-count
   to 1, so a loop does not freeze where it stands - it runs once, instantly,
   and lands on its FINAL keyframe. With the rest pose at both ends, reduced
   motion lands exactly where the design wants it and needs no rule of its own.
   A scoped `animation: none` would not have worked anyway: the base selectors
   here outrank anything a media query could add without repeating them.

   23s and 19s so the two lamps never come back into phase inside a wait. */
@keyframes pwlamp1 {
  from, to { transform: translate3d(0, 0, 0); }
  50% { transform: translate3d(2.6%, -1.8%, 0); }
}
@keyframes pwlamp2 {
  from, to { transform: translate3d(0, 0, 0); }
  50% { transform: translate3d(-2.2%, 2.4%, 0); }
}
/* Sized against the VIEWPORT as well as the type, because this is the one
   surface in the app that has to fit beside a control bar on a 390px phone AND
   on an 844x390 phone turned sideways - the case a plain rem width gets wrong
   and a max-width media query only catches by accident. */
.player .pw-poster {
  flex: none; width: min(13rem, 24vw, 32vh); aspect-ratio: 2 / 3;
  object-fit: cover; border-radius: var(--r-m); background: var(--elev2);
  box-shadow: 0 1rem 2.6rem rgba(0, 0, 0, .72);
}

/* ---- an episode gets its own picture too ----
   The still says which EPISODE and the poster says which show, and the two
   answer different questions - which is the argument for showing both rather
   than picking one.

   Side by side and NOT overlapping. Overlapping them was the first attempt and
   it read as the poster covering the still rather than as two pictures - the
   still is the more informative of the two, and putting a card on top of it
   hides the one thing it was added to say.

   Equal HEIGHTS, derived rather than eyeballed: 2:3 makes the poster 1.5x its
   width and 16:9 makes the still 0.5625x its own, so a still 2.67x the
   poster's width comes out exactly as tall. One min() written once and
   multiplied, so the pair stays square to each other at every viewport and
   interface size instead of drifting apart at the breakpoints.

   Both are smaller than the film's single poster - 11.25rem tall against 19.5
   - so an episode can never be the case that crowds the status plate, which
   is the failure this overlay was just fixed for. */
.player .pw-shot {
  display: none; flex: none;
  width: calc(min(7.5rem, 13vw, 20vh) * 2.67);
}
.player .loading.pw-cold:not(.pw-dead) .pw-shot:not([hidden]) { display: block; }
.player .pw-still {
  width: 100%; aspect-ratio: 16 / 9; display: block;
  object-fit: cover; border-radius: var(--r-m); background: var(--elev2);
  box-shadow: 0 1rem 2.6rem rgba(0, 0, 0, .72);
}
/* Only when a still is actually beside it. The sibling selector is what keeps
   one rule serving both states: a film, and an episode whose still never
   arrived, get the full-size poster above and never see this. */
.player .pw-shot:not([hidden]) + .pw-poster { width: min(7.5rem, 13vw, 20vh); }
.player .pw-id { min-width: 0; display: grid; gap: .5rem; justify-items: start; }
.player .pw-logo { max-width: min(22rem, 46vw); max-height: 4.2rem; object-fit: contain; }
.player .pw-name {
  font-size: clamp(1.5rem, 3.4vw, 2.6rem); font-weight: 800;
  letter-spacing: -.03em; line-height: 1.1; color: var(--fg);
  /* Clamped, because a long series name at the largest interface size would
     otherwise push the plate off the bottom of a phone. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.player .pw-ep {
  font-size: 0.9333rem; font-weight: 700; color: var(--dim);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.player .pw-plot {
  margin: 0; max-width: 42ch;
  font-size: 0.9rem; line-height: 1.5; color: var(--dimmer);
  display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* The phone turns it: a 13rem poster beside 42ch of text does not fit 390px,
   and stacking is what a phone does with a two-column layout everywhere else
   in this app. The plot goes rather than wraps - there is no room for four
   lines of it above a control bar on a screen this size.
   The flex-direction is the point of this block and it was missing: without
   it the comment described a stack the CSS never did, and the name kept a
   173px column at 390px - "Everything Everywhere All at Once" truncated
   mid-word on the two-line clamp. */
@media (max-width: 700px) {
  .player .loading.pw-cold:not(.pw-dead) .pw-art {
    flex-direction: column; gap: 1.1rem;
  }
  .player .pw-poster { width: min(8.5rem, 30vw); }
  .player .pw-plot { display: none; }
  /* The art stacks into a column here, and a 16:9 still with a poster under it
     is two thirds of the card before a word of status. The still alone answers
     "which episode", and the show is named in the line beside it, so the
     poster is the one to drop. */
  .player .pw-shot { width: min(15rem, 52vw); }
  .player .pw-shot:not([hidden]) + .pw-poster { display: none; }
}
/* Landscape on a phone is the tightest case in the app: 390px of HEIGHT with a
   control bar in it. Everything but one picture and the name goes - the still
   for an episode, the poster otherwise.
   flex-direction back to row, because a 667x375 phone matches the width query
   above as well as this one and a stack there is what overlaps the plate. This
   block is later at equal specificity, so it wins.
   And DIVIDE. aspect-ratio: 2/3 makes height = width x 1.5, so a 26vh cap on
   the HEIGHT is a width of 26vh/1.5 - `26vh * 1.5` asked for 39vh of width,
   which never binds below 7rem and so did nothing at all. Measured at 844x390
   the poster was 7rem at every interface size; divided, it is 67.6px wide and
   101px tall and the plate clears the bar at all four. */
@media (max-height: 460px) {
  .player .loading.pw-cold:not(.pw-dead) .pw-art { flex-direction: row; }
  .player .pw-poster { width: min(7rem, 26vh / 1.5); }
  /* Same 26vh height cap, divided by ITS ratio: 16:9 makes height = width x
     .5625, so the width that lands on 26vh is 26vh/.5625. A width cap borrowed
     from the poster would have been 138px tall at the largest interface size
     against the poster's 101px, and this block was measured with 101px. */
  .player .pw-shot { width: min(13rem, 26vh / .5625); }
  .player .pw-shot:not([hidden]) + .pw-poster { display: none; }
  .player .pw-plot, .player .pw-ep { display: none; }
  .player .pw-name { font-size: 1.25rem; }
  /* The wordmark shrinks with the name it replaces. Left at its full 4.2rem
     it is 79px against this block's 23px name, and the art row is as tall as
     whichever side is taller - so the untouched logo, not the picture, would
     have set the height here and pushed the plate back into the control bar. */
  .player .pw-logo { max-height: 2.6rem; }
}
/* Below this there is no honest way to fit a picture: at 844x390 and the
   largest interface size, once the 22s escape and the 28s exit are up, sizing
   the poster to the space left gives it 28px of width. A card nobody can see
   is not worth a plate nobody can press, so the card goes and the wait is
   exactly what it was before this work. */
@media (max-height: 400px) {
  .player .loading.pw-cold:not(.pw-dead) .pw-art { display: none; }
}

/* Dead is a different situation from busy and one law for both was wrong. */
.player .loading.pw-dead {
  align-items: center; justify-items: center;
  padding: 0 var(--pad);
}

/* ---- the readout, in two forms ----
   One component, two grounds, and the ground decides the form.

   THE PLATE is the base rule, and that is deliberate. During a rebuffer this
   renders over a HELD VIDEO FRAME that could be anything, including snow, and
   a soft shadow does not carry dim text over a white sky. Flat rgba following
   .player .skipintro's precedent, because backdrop-filter does not paint on
   the target machine at all. Making it the base means a future edit that
   forgets the cold class gets a plate - legible on any ground - rather than
   bare text on live video.

   It is .86 rather than the .72 it was. The old comment said the plate was
   "exactly invisible over black on a cold open", which stopped being true the
   day the cold ground was lit, and .72 was never doing its job in the state it
   was built for: dim facts over a white sky measured 2.89:1, under the 4.5
   floor, in exactly the case the plate exists to fix.

   THE BASELINE is the cold form below. There the ground is ours - a dim wash
   we chose - so the box comes off entirely and the readout becomes the card's
   own baseline instead of a second object beside it. */
.player .pw {
  max-width: min(34rem, 78vw);
  display: grid; gap: .4rem; justify-items: start;
  padding: .8rem .95rem .78rem; border-radius: var(--r-s);
  background: rgba(0, 0, 0, .86);
  /* For the rule and the hairline, which pin to this box's own top edge. */
  position: relative;
}

/* ---- the cold form: the card's baseline ----
   The complaint that produced this was "it looks out of place", and the fault
   was compositional rather than stylistic. The screen held two objects with
   two axes, two measures and two grounds: a centred title card in the middle,
   and a dark chip in the bottom-left corner anchored to the control bar, with
   nothing saying they were about the same thing - which they entirely are.
   One says what you pressed, the other says what is happening to it.

   So the chip becomes a baseline. A hairline the card's own width, the state
   hung under its left end and the file's specification under its right, on one
   shared baseline. An end slate rather than a notification.

   THE WIDTH IS SHARED, NOT MEASURED. justify-content: center stops .loading's
   single implicit column from stretching, so the track sizes to max-content -
   the wider of the two children - and centres. Both children then stretch to
   it. Neither element measures the other and there is no JS: they share a
   measure because they share a column. .why is capped at 34ch so the status
   line wraps rather than out-measuring the card and leaving a rule with a card
   inside it, which is the one way this inverts.

   Shared by BOTH live forms, which is the point: a cold open and a stall are
   the same object at two scales, and only the measure differs. Dead is the one
   that keeps the plate, because it is centred in the middle of the picture
   where a scrim anchored to the foot of the screen never reaches it. */
.player .loading:not(.pw-dead) .pw {
  max-width: none; background: none; border-radius: 0;
  padding: .9rem 0 0;
  grid-template-columns: 1fr auto;
  column-gap: 2.6rem; row-gap: .5rem;
  align-items: baseline;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18);
}
.player .loading:not(.pw-dead) .pw-acts { grid-column: 1 / -1; }
/* ---- the mid-playback form: a lower third, not a slab ----
   This is the state the plate was built for, and on a real stall it was the
   worst of the three: a hard-edged black rectangle with the film still visible
   around it, growing a second line and a button as the wait went on. A box
   over a picture is the thing that looks pasted on; the picture is the ground
   and the readout has to sit IN it.

   So the box becomes a scrim: one gradient the full width of the frame, fading
   upward, with no vertical edges anywhere because it has none to have. It is
   the same material and the same direction as .pbottom, which is already
   painting under the controls a few pixels below - so the bottom of the screen
   darkens once, continuously, instead of carrying a widget and a scrim that
   disagree about where the furniture starts.

   Strong at the foot and near-flat across the band the words occupy, which is
   the whole reason a linear gradient beats the radial one tried first: a
   radial fades horizontally too, so the right-hand end of the facts line sat
   at less than a third of the ground the left-hand end had, and no size of
   ellipse fixed it without covering half the picture.

   The readout keeps the rule, the sweep, the meter and the type it has on the
   cold open, so the two forms are one design at two scales. */
.player .loading:not(.pw-cold):not(.pw-dead)::before {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0;
  height: 52%; z-index: -1; pointer-events: none;
  background: linear-gradient(0deg,
    rgba(0, 0, 0, .92) 0%, rgba(0, 0, 0, .90) 38%,
    rgba(0, 0, 0, .55) 68%, transparent 100%);
}
/* A fixed measure, not a shrink-wrap. There is no card here to share a column
   with, and letting the block size to its own content made the rule a 256px
   stub floating over the picture - a short line that means nothing, which is
   the same fault as the marks it sits above. Given a measure it reads as a
   lower-third rule, and the sweep has a distance to travel. */
.player .loading:not(.pw-cold):not(.pw-dead) .pw {
  width: min(36rem, 78vw); --pw-sweep: 3s;
}
/* The 34ch cap exists only to stop the cold form's rule out-measuring the card
   it is the baseline of. There is no card here, so the longest of these
   sentences - "The link has stalled - waiting for it to come back" - gets to
   be one line instead of two ragged ones. */
.player .loading:not(.pw-cold):not(.pw-dead) .why { max-width: 50ch; }
/* Narrow, and the justified split is a 40px gap rather than a composition. */
@media (max-width: 700px) {
  .player .loading:not(.pw-dead) .pw {
    grid-template-columns: 1fr; column-gap: 0; row-gap: .45rem;
    align-items: start;
  }
}

/* ---- proof that something is still happening ----
   Everything else in this plate is words and a buffer reading, and during a
   real stall none of them change for seconds at a time. A panel that has gone
   completely still is indistinguishable from one whose page has died, which is
   the thing a reader most often gets wrong about a buffering screen - so one
   element keeps moving for exactly as long as something is still expected.

   Transform only, on a 38% segment. This rasteriser composites transform and
   opacity and stutters on everything else, which is the same reason the pips
   below are opacity rather than colour. It is hidden outright when the wait
   ends in failure and when the device is off the network, because in both of
   those cases nothing IS arriving and a moving line would be the panel
   claiming otherwise. */
.player .pw-live {
  position: absolute; left: 0; right: 0; top: 0; height: 1px;
  overflow: hidden; border-radius: var(--r-s) var(--r-s) 0 0;
}
/* A bloom, not a bar. The old form was a 38% opaque stub sliding along a 2px
   track, which is the shape of a download bar - and a shape that looks like a
   measurement is a claim, on a screen that measures nothing here. A soft
   gradient running a 1px rule reads as light in a wire instead.

   The gradient is PAINT and the transform is the MOVEMENT. Nothing animates a
   background, a position or a colour: the car composites transform and opacity
   and stutters on everything else, which is the same reason this file replaces
   animated background-position with a translated layer three times over. */
.player .pw-live i {
  position: absolute; top: 0; bottom: 0; left: 0; width: 30%;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .92) 50%, transparent);
  animation: pwsweep var(--pw-sweep, 2.2s) linear infinite;
}
/* Per form, so the two read at the same SPEED rather than the same period: the
   cold rule is the card's full width and the plate's is a third of that. The
   old single 1.6s across a chip was the tempo that made the panel feel
   anxious, which is the opposite of what a normal ten-second wait should say. */
.player .loading.pw-cold:not(.pw-dead) .pw { --pw-sweep: 3.4s; }
@keyframes pwsweep {
  from { transform: translateX(-110%); }
  to   { transform: translateX(345%); }
}
.player .loading.pw-off .pw-live { display: none; }

/* Two things can be offered now - a smaller stream while there is still
   something to save, and the way out - so they take a row rather than each
   claiming the foot of the plate. */
.player .pw-acts { display: flex; flex-wrap: wrap; gap: .5rem; }
.player .pw-acts:empty { display: none; }

/* ---- how far in it is about to open, and the way back to the top ----

   This was a dialog in front of the player: a question with Cancel as its
   first button, asked before anything had started, and three equal columns
   that could not hold "Resume from 1:11:24" - the label wrapped to three
   lines inside a pill and the primary answer came out as an egg.

   It belongs here instead. The card is already up, already carries the
   poster and the title, and is already what you are looking at while the
   file opens. Resuming is simply what happens; this says how far in, and
   offers the one other answer without demanding an answer at all.

   A rule rather than a bar in a box. The plate around it is a hairline over
   nothing, so a filled track with its own border would be the only boxed
   thing on it - two weights of the same white say the same thing and stay
   part of the plate. No hue: the palette has two and neither means
   "progress".
*/
/* justify-self, because the plate sets justify-items: start and a full-width
   row is the one child that has to ignore it - without this the rule and the
   sentence shrink to their own content and sit in the left third of a plate
   whose other rows reach both edges. */
.player .pw-res {
  grid-column: 1 / -1; justify-self: stretch;
  display: grid; gap: .55rem;
}
.player .pw-resbar {
  height: .14rem; border-radius: 99px; overflow: hidden;
  background: rgba(255, 255, 255, .16);
}
.player .pw-resbar i { display: block; height: 100%; background: var(--fg); border-radius: 99px; }
.player .pw-resrow {
  display: flex; align-items: center; justify-content: space-between;
  gap: .8rem; flex-wrap: wrap;
}
.player .pw-resat {
  font-size: 0.8rem; font-weight: 700; color: var(--dim);
  font-variant-numeric: tabular-nums;
}
/* The button's ground, height and - the one that actually matters - its
   pointer-events come from the .failback rule further down, which it now
   shares. .loading is pointer-events: none across its whole surface so a
   cold open never eats a tap meant for the picture, and a button inside it
   that does not opt back in is not a button at all: the press goes through
   to the tapzone underneath and pauses the film instead. That is exactly
   what this one did on its first run on the real player. */
/* The button owns the width on a phone rather than sitting in a ragged row
   with the sentence above it. */
@media (max-width: 700px) {
  .player .pw-resrow { gap: .55rem; }
  .player .loading .pw-restart { width: 100%; }
}
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .player .pw-resat { font-size: 0.9rem; }
  .player .loading .pw-restart { font-size: 0.9rem; }
}

/* The same offer, once the card has gone.

   A cold open here runs to tens of seconds and the card is up for all of it,
   but an already-probed file can be playing inside a second, and an offer
   that appears and vanishes within that second is not an offer. So it moves
   to a corner for a few seconds more.

   Left, not right: "Skip intro" and the paused corner both anchor bottom
   right at this same measure, and resuming into an intro puts the first of
   those on screen at exactly this moment. Shares that shape and its ground -
   no backdrop-filter over a playing picture, which the car cannot paint
   anyway.
*/
.player .resfloat {
  /* Appended to .player, not to .loading, so it needs no pointer-events
     opt-in - and could not live in .loading anyway, which is display:none by
     the time this exists. */
  position: absolute; left: 1.4rem; bottom: calc(var(--pbH, 7.5rem) + .9rem); z-index: 4;
  background: rgba(0, 0, 0, .82);
  border: 1px solid rgba(255, 255, 255, .25); color: #fff;
  padding: .65rem 1.3rem; border-radius: 99px; font-weight: 800; font-size: 0.9rem;
  animation: rise .25s var(--ease); transition: background .2s, transform .15s var(--ease);
}
.player .resfloat:hover { background: rgba(0, 0, 0, .9); }
.player .resfloat:active { transform: scale(.96); }
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .player .resfloat { min-height: 3.5rem; display: inline-flex; align-items: center; }
}
@media (prefers-reduced-motion: reduce) { .player .resfloat { animation: none; } }
.player .loading.pw-dead .pw {
  justify-items: center; text-align: center; padding: 1.1rem 1.4rem;
}

/* One step above the title in .ptop, deliberately: for these seconds this line
   is the primary fact and the title is context. Then it is gone.

   Bigger and LIGHTER than the 1rem/700 it was, which is the whole quietness
   trade: 700-weight body text is what makes a small panel shout, and once the
   line has a rule over it and air around it, size carries the emphasis that
   weight was carrying badly.

   34ch is structural, not typographic. The cold form's rule is as wide as the
   widest child of a shared max-content column, so an unwrapped status line -
   the 22s rung is 58 characters - would out-measure the title card and leave a
   rule with a card inside it rather than a card with a baseline under it. */
.player .loading .why {
  font-size: 1.125rem; font-weight: 600; color: #fff; line-height: 1.35;
  letter-spacing: -.006em; max-width: 34ch; text-wrap: balance;
}
.player .loading.pw-dead .why { font-size: 1.25rem; }
.player .pw-say { display: flex; align-items: baseline; gap: .7rem; min-width: 0; }

/* Not smaller than this. It is the line carrying the whole argument.
   Kept at --dim rather than lifted to --fg: this app pairs a white heavy line
   with a --dim secondary line on every player surface it has, and flattening
   the two registers here to win a contrast argument would make this the one
   screen that reads differently. The contrast is won by the ground instead -
   the plate went to .86, and the cold form's ground is ours. */
.player .pw-facts {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .4rem;
  font-size: 0.9rem; font-weight: 500; color: var(--dim);
  font-variant-numeric: tabular-nums; letter-spacing: .04em;
}
/* Weight carried by white. Both hues in this palette already mean something
   else - amber in particular means "results are being hidden", which is not
   what a heavy release is. Same direction the version rows take. */
.player .pw-facts .heavy { color: var(--fg); font-weight: 800; }
.player .pw-n:not(:empty)::before { content: '\00b7'; margin-right: .4em; opacity: .55; }

/* ---- the one measurement ----
   Three slots, one per three seconds of runway in front of the playhead.
   Discrete because the number is discrete: the encoder writes three-second
   segments and the buffer only advances a whole fragment at a time, so a
   continuous bar would render 0.02, then 0.75, then vanish - a smoothness the
   pipeline cannot produce. Not a target but a scale: playback resumes long
   before three fragments, so this characteristically lights one or two and
   disappears, the way a signal meter does.

   Opacity only. This rasteriser composites opacity and transform and stutters
   on background and colour, which is why an empty slot is the same white
   turned down rather than a second colour.

   ---- and the same shape, counting the other thing ----
   The meter has two meanings and never both at once. On a REFILL it is three
   slots of three seconds of runway, exactly as above. On a COLD OPEN it is
   four slots of pwStep - which stage of the pipeline is running - and pwStep
   is already a real, event-driven, monotonic 1-4 that until now was thrown
   away as soon as it had chosen a sentence. It moves only when pwStage moves,
   so it carries the same guarantee the words do: no timer advances it and
   nothing interpolates.

   It is emphatically not a progress bar. It stays at signal-meter scale beside
   the words rather than spanning the rule, because four quarters lighting
   across a 900px baseline WOULD be a progress bar, and one that sits at 3 of 4
   for twenty seconds of encoding is the false claim this file refuses three
   times over.

   Drawn only when hls.js is driving. Stages 3 and 4 exist solely as
   MANIFEST_PARSED and FRAG_BUFFERED handlers, and there are two paths that
   assign video.src directly - on those, a four-slot meter would sit at 2 of 4
   for the whole wait and then vanish, which reads as having stopped halfway.

   The gap before the third slot is the handover: slots 1-2 are TorBox opening
   the file, 3-4 are Jellyfin encoding it. Drawn with space rather than a
   legend or a second colour, which is what the closed palette asks for.

   State by SHAPE as well as brightness. An unreached slot is the same white
   turned down AND scaled to a thin line; a reached one is a solid block. The
   brightness alone had to be very dim to read as unlit, which made the
   denominator - "of four", half the information - almost invisible. Opacity
   and transform, which are the two things the rasteriser composites. */
.player .pw-meter { display: inline-flex; align-items: center; gap: .25rem; flex: none; }
.player .pw-meter i {
  width: 1rem; height: .3rem; background: #fff;
  opacity: .45; transform: scaleY(.42);
  transition: opacity .18s linear, transform .18s linear;
}
.player .pw-meter i.on { opacity: 1; transform: none; }
.player .pw-meter[data-of="4"] i:nth-child(3) { margin-left: .5rem; }
/* Frozen, not live. The failure screen keeps the stage meter because "it
   reached 2 of 4" beside "Stopped while starting the encoder" is a diagnosis,
   but a frozen meter that looks exactly like a running one is not. */
.player .loading.pw-dead .pw-meter i.on { opacity: .55; }

/* The overlay stays transparent to taps - it sits over the whole video and
   must not eat them - so the one thing in it that IS pressable asks for them
   back explicitly. Sized to the touch floor, because it is now offered seven
   seconds BEFORE the failure and has to be hittable at arm's length. And given
   a real ground: .btn leans on backdrop-filter, which .player nulls because it
   does not paint here, leaving its only ground at 13% white over live video. */
/* Same recipe as .failback beside it, because they sit in one row and a pair
   of buttons that disagree about their height reads as a mistake. */
.player .loading .pw-down,
.player .loading .failback,
.player .loading .pw-restart {
  pointer-events: auto; margin-top: .35rem;
  min-height: 3.5rem; padding: 0 1.6rem;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255, 255, 255, .17);
  box-shadow: inset 0 0 0 1px var(--line-hi);
}

@media (prefers-reduced-motion: reduce) {
  /* So the meter steps rather than fades, in both of its counts. */
  .player .pw-meter i { transition: none; }
  /* The hairline stops travelling and becomes a full one. The global rule
     would zero its duration and leave a 30% bloom parked off the left edge,
     which reads as a channel with nothing in it. A flat full-width line says
     "there is a live channel here" without pretending to measure anything -
     and it has to replace the gradient rather than fade it, because a gradient
     turned down is still transparent at both of its ends. */
  .player .pw-live i {
    animation: none; width: 100%;
    background: none; background-color: rgba(255, 255, 255, .34);
  }
}

/* ---------------------------------------------------------------- misc -- */

.empty { padding: 2.2rem var(--pad); color: var(--dim); font-size: 0.9rem; }
/* .empty carries the page gutter because it is sometimes dropped straight into
   an unpadded parent (a rail slot, or chrome() itself) where it IS the gutter.
   Everywhere else its container has already applied one, and it was indenting
   twice - a step of a whole --pad between "Nothing recorded yet." and the
   paragraph directly above it. .sfail already fixed this for one screen; this
   is the same fix for the rest.

   And when it lands in a grid it is one CELL, so a sentence was being laid out
   at poster width: 124px wide, five lines, one word each.

   .fxwrap, not .fixwrap. The class is fxwrap everywhere it is written and
   everywhere it is read; this selector was the only thing in either file
   spelling it the other way, so the corrections screen has been taking the
   exact double indent this rule exists to remove for as long as the rule has
   existed - starting with the "Loading..." line that is the first thing that
   screen shows. */
/* .wlbody is NOT in this list, and that is the same call .sfail already
   made forty lines below: ".wlbody and .results are not grids and carry no
   horizontal padding, so they are deliberately not in this list - .sfail's
   own gutter IS the gutter." The reasoning applies here unchanged and had
   not been carried back. .wlbody supplies no horizontal padding of its own
   - the rule at .wlbody .sec exists precisely because it does not, and adds
   var(--pad) by hand to the one other child that needed it - so an .empty
   dropped straight into it had nothing else to indent it and sat flush
   against the window, which is the single case the .empty gutter exists
   for. */
/* .askp is here because every sentence in that panel - each error, the
   nothing-found line, the end of a question - sits under a heading that
   already carries var(--pad), and .empty adds its own on top. The old .sai
   never joined this list, so every one of those sentences stood a whole
   gutter right of the heading above it. */
.detail .sheet .empty, .fxwrap .empty, .histbody .empty,
.grid .empty, .listgrid .empty, .pickgrid .empty, .askp .empty {
  padding-left: 0; padding-right: 0;
}
/* .histbody was missing from this list, and has been for as long as the
   history page has been a wall: "Nothing recorded yet." was being laid out in
   one 21rem track with three empty tracks beside it - 352px of sentence at
   1600, 411px at 900 - which is the exact bug the paragraph above describes
   and fixes for the other three. */
.grid > .empty, .listgrid > .empty, .pickgrid > .empty { grid-column: 1 / -1; }
/* .histbody is multicol now, so its full-width children span columns rather
   than grid tracks. */
/* .histbody is a plain block now, so a block child is already full width.
   `column-span: all` was here for the multicol wall and is a no-op. */
.histbody > .empty { display: block; }

/* Amber, once, for the only failure amber has ever described.
 *
 * A failed fetch has no answer to qualify: no number, no delta, and no control
 * on screen that would restore it. Painting that amber would make
 * "36,252 - 40 here" and "the server is down" the same colour, and then
 * neither is readable. Amber keeps exactly one job - THIS ANSWER IS NOT THE
 * WHOLE ANSWER - and gains the case it should always have had: a wall that
 * came back with nine rows of twelve, or from a cache the relay has FAILED to
 * refresh.
 *
 * Failure with nothing behind it stays grey prose in the same .empty box,
 * carried entirely by wording. --bad is already #fff for that reason, with the
 * comment "failed - carried by wording, not hue" beside it.
 *
 * Placement: after the .empty rules above so the file reads in one order,
 * though nothing depends on it - .empty.cut is (0,2,0) against .empty's
 * (0,1,0), and the rule above is the only one in this file that sets a colour
 * on .empty at all. --hide-tint is on :root, so a custom property declared
 * 2,800 lines below resolves here regardless.
 */
.empty.cut { color: var(--hide-tint); }

/* .sfail was written for .results and .sres, which are plain blocks. It is now
 * also dropped into four grids - .histbody, .listgrid, .pickgrid and .grid -
 * and it is display:flex, so without this it becomes ONE grid item: at car
 * width .histbody is two or three 21rem tracks, and the failure sentence would
 * wrap over three lines in column one with empty space beside it.
 *
 * The padding is zeroed for the same reason the .empty rule above zeroes it,
 * and the comment there records this exact bug being fixed once already: all
 * four of these parents supply var(--pad) themselves, and .sfail supplies its
 * own, so the message was indented by two gutters against the heading above
 * it. .wlbody and .results are not grids and carry no horizontal padding, so
 * they are deliberately not in this list - .sfail's own gutter IS the gutter
 * there, which is what it was written for.
 *
 * (0,2,0) against .sfail's (0,1,0), so source order is irrelevant.
 *
 * .fxwrap is in the list and is not a grid - grid-column on a block child is
 * ignored - because the padding half is what it needs. It supplies var(--pad)
 * itself, so the retry form that now stands in for "Could not read the
 * backlog." would be indented by two gutters against the tools row directly
 * above it. Same bug, same fix, same screen as the .empty rule above.
 */
.grid > .sfail, .listgrid > .sfail, .pickgrid > .sfail,
.fxwrap > .sfail {
  grid-column: 1 / -1; padding-left: 0; padding-right: 0;
}
.histbody > .sfail { padding-left: 0; padding-right: 0; }
/* Poster columns are fluid rather than stepped. A wide screen should show
   MORE posters, not the same number blown up; auto-fill already does that and
   the clamp only stops a card getting mean on a phone or silly on a desktop.
   This replaces a hard minmax(10.5rem) that switched on at exactly 1800px and
   reached only one of the three places .grid is used. */
.grid {
  display: grid; gap: .85rem; padding: .2rem var(--pad) 2rem;
  grid-template-columns: repeat(auto-fill, minmax(clamp(7.6rem, 8.5vw, 10.5rem), 1fr));
}
/* ---- the toast stack ----
   The layer is the fixed thing; the toasts inside it are absolutely positioned
   against its bottom edge and differ only by --i, their depth in the pile.
   Offset and scale are arithmetic on that one number, so there is no
   third-place rule sitting somewhere else waiting to be forgotten.

   width and height 0: the layer is an anchor, not a panel, and must never
   cover the screen it floats over. */
.toaststack {
  position: fixed; left: 50%; bottom: 1.5rem; width: 0; height: 0;
  z-index: var(--z-toast); pointer-events: none;
}
.toast {
  --i: 0;
  position: absolute; left: 0; bottom: 0; white-space: nowrap;
  background: var(--panel); border: .06em solid var(--line-hi); border-radius: 99px;
  padding: .6rem 1.2rem; font-size: 0.8667rem; font-weight: 600;
  box-shadow: var(--shadow); pointer-events: auto;
  transform: translateX(-50%) translateY(calc(var(--i) * -0.62rem))
             scale(calc(1 - var(--i) * 0.055));
  opacity: calc(1 - var(--i) * 0.36);
  transition: transform .32s var(--ease), opacity .32s var(--ease);
}
/* Untappable behind the front one. An undo button whose label you cannot read
   would undo something other than the thing you just did.

   Only the pointer rule is a class - the dimming is arithmetic on --i like the
   offset and the scale, so the third one is genuinely further back than the
   second rather than merely also-dimmed. */
.toast.behind { pointer-events: none; }

/* ---- the first-run note ----
   A card at the bottom edge, over an app that already works. It is not a
   dialog and takes no focus trap: everything behind it is usable, which is
   the point being made.

   ONE STEP BELOW A TOAST, derived from the toast's own token rather than
   written as a number, so the two cannot drift into disagreeing about which
   reports on which. A toast fired while this is up must be readable over it.

   Bottom centre, and on a phone it clears the tab bar - the bar only exists
   under 700px, so the raise lives in that same query and cannot outlive it. */
.welcome {
  position: fixed; z-index: calc(var(--z-toast) - 1);
  /* Inset from both edges and centred by auto margins rather than by a
     percentage translate. It costs nothing and it keeps 100vw out of the
     width: on a desktop with a visible scrollbar 100vw is WIDER than the
     space there is, so a vw-sized card sits a scrollbar's width too far
     right and the gutter on one side is not the gutter on the other. */
  left: var(--pad); right: var(--pad); margin-inline: auto; max-width: 30rem;
  bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px));
  background: var(--panel); border: .06em solid var(--line-hi);
  border-radius: var(--r-l); box-shadow: var(--shadow);
  backdrop-filter: var(--glass); -webkit-backdrop-filter: var(--glass);
  animation: welin .28s var(--ease) both;
}
.wel-in { padding: 1rem 1.1rem 1.1rem; }
.wel-t {
  margin: 0 0 .35rem; font-size: 0.9333rem; font-weight: 800;
  letter-spacing: .01em;
}
.wel-b {
  margin: 0; font-size: 0.8rem; line-height: 1.55; color: var(--dim);
}
.wel-b b { color: var(--fg); font-weight: 800; }
.wel-do {
  display: flex; gap: .5rem; margin-top: .9rem; flex-wrap: wrap;
}
/* Both reach a thumb. The pair fills the row on a narrow card and sits at
   its natural width on a wide one. */
.wel-do .btn { flex: 1 1 auto; justify-content: center; min-height: 2.8rem; }
.wel-go { flex-grow: 2; }
.wel-no { color: var(--dim); }

@keyframes welin {
  from { opacity: 0; transform: translateY(1rem); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes welout {
  to { opacity: 0; transform: translateY(.6rem); }
}
/* .24s, which is what the script waits before removing the element. Two
   numbers, one duration - if this one grows the card vanishes mid-flight. */
.welcome.wel-out { animation: welout .24s var(--ease) both; pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  .welcome { animation: none; }
  .welcome.wel-out { animation: none; opacity: 0; }
}

/* NO PHONE BREAKPOINT FOR THE HEIGHT, and that is the point. The phone's tab
   bar is 64px tall here and NOTHING declares that: it is an icon plus a label
   plus two paddings, so it moves with the root font size and with the label's
   own line box. A number written here to clear it was 2px short - measured,
   at 375x812 - and would be wrong again the next time either changes. So the
   lift is measured from the bar itself when the note is built; this is only
   the case with no bar under it. */
@media (max-width: 700px) {
  .welcome { max-width: none; }
}

@media (max-width: 640px) {
  .hero .plot { -webkit-line-clamp: 3; }
  .card .bar { bottom: .35rem; }
}

/* ------------------------------------------------------- detail: TMDB bits -- */
/* The one line in this stack with no shadow, which is why it is the one that
   goes illegible over a bright frame - .hero h1 carries one and .hero .plot
   carries the pair below. It was left out while the tagline's own future was
   open; it stays, so it gets what its neighbours have. Same value as
   .hero .plot, and text-shadow rather than a filter for the reason that rule
   states: filter does not paint at all in the browser this targets. */
.hero .tagline {
  margin: 0; font-size: 0.9667rem; font-style: italic; color: var(--dim);
  letter-spacing: .01em; max-width: 34rem;
  text-shadow: 0 1px 8px rgba(0, 0, 0, .9), 0 0 2px rgba(0, 0, 0, .7);
}
.hero .meta .dot { opacity: .35; }
.tag.cert {
  background: transparent; box-shadow: inset 0 0 0 .08em var(--line-hi);
  color: var(--dim); font-size: 0.7rem; letter-spacing: .06em;
}

/* Facts: a quiet definition grid, not a wall of prose. */
.facts-grid {
  display: grid; gap: 1.1rem 2.2rem; margin: 1.5rem 0 .2rem;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
}
.facts-grid > div { min-width: 0; }
.facts-grid dt {
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .16em;
  text-transform: uppercase; color: var(--dimmer); margin-bottom: .25rem;
}
.facts-grid dd {
  margin: 0; font-size: 0.9rem; font-weight: 600; color: var(--fg);
  line-height: 1.4;
}
/* ---- the fixed cell, which is what makes every shape below legal ----
   A value that grows must not make its row taller than its neighbours, and
   until now one did: "Meg LeFauve, Josh Cooley, Pete Docter" took two lines
   where its row-mates took one, so the whole band stepped down. Measured at
   42px against 21px.

   height, not min-height. A minimum is a floor, and -webkit-line-clamp counts
   line boxes rather than pixels, so neither of them caps anything on its own;
   a fixed height with overflow makes every cell the same box by construction
   instead of by arithmetic luck. Two lines of 0.9rem at 1.4 is 2.52rem.

   Only from 480px up. Below that the auto-fit grid resolves to a single
   column, where a fixed height is dead space with no sideways alignment to
   justify it. */
@media (min-width: 480px) {
  .facts-grid dd { height: 2.52rem; overflow: hidden; }
}
/* The clamp lives here and not on the dd, because a dd set to -webkit-box
   could not also be the inline-flex a flag row needs, nor hold a two-line
   figure stack. All three properties together: a line-clamp without
   box-orient and overflow is inert, which is why every other clamp in this
   file carries the same trio. */
.facts-grid .fv {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}

/* ---- a figure with its context under it ----
   For the two rows that were sentences: "7.9 from 23,789 votes" and "82% from
   60 ratings on PublicMetaDB". A figure is recognised where a sentence has to
   be read, which is the whole difference at arm's length.

   line-height 1 on the figure so the dd's own strut cannot inflate it: 22.5 +
   1.5 + 12.65 is 36.65px inside the 37.8px box, and the coarse sizes below
   are budgeted the same way. */
.facts-grid .ffig {
  display: block; font-size: 1.5rem; font-weight: 800; line-height: 1;
  letter-spacing: -.01em; font-variant-numeric: tabular-nums; color: var(--fg);
}
.facts-grid .ffig-s {
  font-size: .5333em; font-weight: 700; color: var(--dimmer); line-height: 1;
}
.facts-grid .fq {
  display: block; margin-top: .1rem;
  font-size: 0.7333rem; font-weight: 600; color: var(--dim); line-height: 1.15;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ---- the one chip ----
   Used by exactly one row, Status, and only when the answer is not the
   default. Deliberately the same construction as .detail .es-next, which this
   app already ships as a non-interactive state pill on this same screen -
   rather than a new shape doing an existing shape's job.

   Sentence case, never uppercase: the label above it is already uppercase and
   two stacked uppercase runs is shouting. */
.facts-grid .fchip {
  display: inline-block; padding: .2rem .6rem; border-radius: 99px;
  background: var(--elev2); color: var(--fg);
  font-size: 0.7333rem; font-weight: 800; letter-spacing: .04em;
  line-height: 1.35; white-space: nowrap;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}

/* ---- the overflow count ----
   Two names and a +1 beats three names and a wrapped line. */
.facts-grid .fmore {
  margin-left: .35em; font-size: 0.7333rem; font-weight: 800;
  color: var(--dimmer); font-variant-numeric: tabular-nums;
}

/* ---- a flag beside a name ----
   The flag never yields space to the word: it is flex: none and the word
   takes the ellipsis, because a shrunken flag is unreadable while a shortened
   country name is still a country name. */
.facts-grid .fv-flag { display: inline-flex; align-items: center; gap: .45em; max-width: 100%; }
.facts-grid .fv-flag .flag {
  position: relative; flex: none; display: block;
  width: 1.15em; height: 1.15em; border-radius: 3px; overflow: hidden;
}
.facts-grid .fv-flag .flag img { display: block; width: 100%; height: 100%; }
.facts-grid .fv-flag .flag::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .16); pointer-events: none;
}
.facts-grid .fv-n { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---- a company's mark ----
   The HEIGHT is capped and the width left alone. Capping width instead turns
   a short wordmark into a monolith and a long one into a hairline, since
   these arrive at wildly different aspect ratios - the Pixar mark is 185x39.
   object-position left so a row of them would share a left edge. */
/* Several marks, wrapping inside the cell rather than running out of it.
   column-gap is wider than row-gap on purpose: side by side two wordmarks
   need air between them to read as two, stacked they do not. */
.facts-grid .fv-co {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: .12rem .5rem; max-width: 100%;
}
/* Each mark gets at most half the cell, so a long wordmark cannot take the
   whole line and push its neighbours out of a box that cannot grow. */
/* ---- a mark is a chip, and the chip says which kind it is ----
   A name-only mark is the dark chip this file already ships for Status, so
   two constructions are not doing one job. A mark with ART flips to the light
   plate, and that flip is the whole change: TMDB's company logos are 64%
   black ink on transparency (202 of 315 measured across the library), so they
   cannot be drawn on this near-black page at all - which is why they used to
   be repainted white, at the cost of the 40% that carry real colour.

   The plate is --accent-deep, the ground a primary button already stands on.
   No new surface, no third hue, and since these marks are now buttons the
   borrowed shape is telling the truth rather than imitating one.

   So a row reads as: light chip = this company's own mark, dark chip = its
   name. Both are the same height and radius, so it is a row either way. */
.facts-grid .mark {
  position: relative; display: inline-flex; align-items: center;
  max-width: 47%; min-width: 0; box-sizing: border-box;
  padding: .22rem .5rem; border: 0; border-radius: 6px;
  background: var(--elev2); color: var(--fg);
  font: inherit; text-align: left;
}
.facts-grid .fv-co .mark:only-child { max-width: 100%; }
.facts-grid .mark-t {
  display: block; font-size: 0.7333rem; font-weight: 800; letter-spacing: .03em;
  line-height: 1.35; color: inherit;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The plate, once the art has actually loaded and been accepted. Set from
   JavaScript rather than from :has(img), because the image is only adopted
   after onload has confirmed its aspect ratio - a chip that turned light the
   moment an <img> was appended would flash light, then dark again for every
   mark that fails the ratio test. */
.facts-grid .mark-art {
  background: var(--accent-deep); color: var(--accent-ink);
  padding: .2rem .42rem;
  /* Not full brightness: a light chip is a glare source on a night dashboard,
     and this is the same .92-.94 the index marks already sit at. Opacity
     rather than a filter, which never paints on the car - and it dims plate
     and ink together, so the contrast inside the chip is untouched. */
  opacity: .94;
}
.facts-grid .mark-i {
  display: block; height: 1.15rem; width: auto; max-width: 100%;
  object-fit: contain; object-position: center;
}

/* ---- pressable ----
   The visible chip is about 26px tall and the cell it lives in is a fixed
   2.52rem with overflow hidden, so a 52px target cannot be had by growing the
   chip - it would grow the row. The hit area is extended instead, by a
   pseudo-element that takes no layout: it fills the cell vertically and
   claims half the column gap on each side. Adjacent marks meet without
   overlapping, because the gap is .5rem and each side takes .25rem. */
.facts-grid .mark-b { cursor: pointer; }
.facts-grid .mark-b::after {
  content: ''; position: absolute; inset: -.6rem -.25rem;
}
/* Transform only, and that is a fix rather than a saving. The chip goes dark
   to light at the moment its art is accepted, so any transition on
   background-color or opacity animates THAT - every mark visibly fades in
   from the wrong colour on every render, cache or no cache, which is the same
   trap .artbox img already falls into. Colour changes here are instant; the
   press is what moves.

   Transform is also the cheap one: it and opacity are the only two things the
   car's rasteriser does without repainting. No transition-delay anywhere in
   here either - the reduced-motion rule zeroes durations only, so a delay
   survives it as a stall. */
.facts-grid .mark-b { transition: transform .12s ease; }
.facts-grid .mark-b:active { transform: scale(.96); }
.facts-grid .mark-b.mark-art:active { opacity: 1; }
.facts-grid .mark-b:not(.mark-art):active { background: var(--elev2); color: var(--accent); }
@media (hover: hover) {
  .facts-grid .mark-b:not(.mark-art):hover { background: var(--accent-deep); color: var(--accent-ink); }
  .facts-grid .mark-b.mark-art:hover { opacity: 1; }
}

/* The initial fills whatever frame it is in; the frame owns the shape. This
   used to be its own 6rem circle, which would now sit as a disc inside a
   rectangular card. */
.person .pnoimg { font-size: 1.5rem; }

.sec .scope { font-size: 0.7667rem; font-weight: 700; color: var(--accent); letter-spacing: .02em; text-transform: none; }

/* person page facts line */
.personhead .pfacts {
  font-size: 0.8333rem; font-weight: 600; color: var(--dim); margin-bottom: .45rem;
}
.personhead .pfacts .dot { opacity: .35; margin: 0 .4rem; }

/* --------------------------------------------------------- refresh panel -- */
/* Below the player, not above it. At z-index 200 this sat over .player (100),
   over the releases dialog (160) and over the phone's tab bar (60), so a pull
   started before playback parked an opaque 390x278px panel on the video's
   bottom-right controls and took their taps - and one touch on it cancelled
   the auto-close, which made that permanent. At 90 it is above the header and
   the tab bar and under everything that is meant to be in front, so a
   fullscreen player simply covers it and the run continues unseen.

   No backdrop-filter. --panel is rgba(15,15,15,.96), so the blur was filtering
   4% of a pixel for a compositing layer everywhere - and on the car it never
   painted at all. */
.sheet-pop {
  position: fixed; right: 1rem; bottom: 1rem; z-index: var(--z-pop);
  width: min(26rem, calc(100vw - 2rem)); border-radius: var(--r-m); overflow: hidden;
  /* The same ground as the header capsule and the two corner buttons, rather
     than a flat opaque slab. This panel comes out of one of those buttons and
     sits on top of the artwork the whole time it is open; solid black was the
     only piece of the interface that stopped the picture dead.
     Darker than the capsules' .5 because this one carries paragraphs and
     numbers rather than five words, and it keeps a solid colour underneath for
     the browser that never paints a backdrop filter - the car - where .62 flat
     black is still darker than the ground that measured 4.7:1 on white text. */
  background: rgba(0, 0, 0, .62); color: var(--fg);
  backdrop-filter: var(--glass);
  -webkit-backdrop-filter: var(--glass);
  border: .06em solid var(--line-hi); box-shadow: var(--shadow-lg);
  /* Out of its own corner, because that is where it lives - it is anchored
     bottom-right, so growing from the centre reads as a thing that arrived
     from nowhere rather than one that opened where it sits. */
  transform-origin: 100% 100%;
  animation: popIn .24s var(--ease);
}
/* The exit already existed further down this file as .sheet-pop.leaving with
   the sheetout keyframe, and it is the one that wins on source order. What was
   missing was anything CALLING it: closeRefresh deleted the panel outright, so
   the animation only ever ran on the auto-dismiss. Nothing to add here. */
@keyframes popIn {
  from { opacity: 0; transform: translateY(8px) scale(.94); }
  to   { opacity: 1; transform: none; }
}
/* The step list collapses the same way the correction families do. */
.stepswrap {
  display: grid; grid-template-rows: 0fr; margin-top: 0;
  transition: grid-template-rows .26s var(--ease), margin-top .26s var(--ease);
}
.stepswrap.open { grid-template-rows: 1fr; margin-top: .55rem; }
/* The list's own top margin moves out here, so it opens WITH the row. Left on
   the list it would be a margin on a grid item, which counts toward the track
   and leaves a gap above a collapsed panel that is closed. */
.stepswrap > .steps { overflow: hidden; min-height: 0; margin-top: 0; }
/* 700px is where the tabs become a bottom bar, so it is where this has to get
   out of their way - it was clearing at 640 and covering the navigation on
   anything between. */
@media (max-width: 700px) {
  .sheet-pop {
    right: .5rem; left: .5rem; width: auto;
    bottom: calc(4.1rem + env(safe-area-inset-bottom, 0px));
  }
}

.sheet-pop .hd {
  display: flex; align-items: center; gap: .55rem;
  padding: .7rem .5rem .65rem .9rem; border-bottom: .06em solid var(--line);
}
/* min-width and flex are not decoration: at --ui 1.26 the title wrapped to two
   lines and squeezed the close button from 30px to 21px wide - a border-radius
   50% element drawn as an ellipse, and the only control that dismisses this
   was the first thing the layout gave up. */
.sheet-pop .hd .hdt {
  flex: 1 1 auto; min-width: 0; font-size: 0.8rem; font-weight: 800;
  letter-spacing: .14em; text-transform: uppercase;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sheet-pop .hd .t {
  flex: none; font-size: 0.8rem; color: var(--dimmer);
  font-variant-numeric: tabular-nums;
}
.sheet-pop .hd .x {
  flex: none; width: 2.2rem; height: 2.2rem; border-radius: 50%;
  font-size: 1.1333rem; line-height: 1; color: var(--dim);
  display: grid; place-items: center;
  transition: background .2s, color .2s, transform .12s var(--ease);
}
.sheet-pop .hd .x:hover { background: var(--elev2); color: var(--fg); }
/* A press that misses and a press that lands looked identical: both controls
   expressed their whole state through :hover, which a touchscreen never fires. */
.sheet-pop .hd .x:active { background: #fff; color: #000; transform: scale(.92); }

/* ---- the track: six segments, one per step ---- */
/* The glanceable half of the step list, and on the 96.7% of runs that change
   nothing it is the only progress there is. Transform and opacity only: the
   old active marker animated box-shadow on an infinite loop, which is a full
   repaint per frame for the whole 9.7s of a pull on a software rasteriser -
   and the halo it animated overrode the inset ring it was meant to pulse, so
   the running step was the one step with no visible marker at all. */
/* The one .track in the app that holds no cards - it is the refresh panel's
   row of progress segments, and nothing in it ever lifts. Its own padding
   shorthand already replaces the headroom; the negative margin has to be
   cancelled by hand or the whole panel rides up by it. */
.sheet-pop .track {
  display: flex; gap: .2rem; padding: .75rem .9rem .1rem; margin-top: 0;
}
.sheet-pop .seg {
  position: relative; overflow: hidden; flex: 1 1 0; height: .2rem;
  border-radius: 99px; background: var(--line);
  transition: background .2s, transform .2s var(--ease);
}
.sheet-pop .seg.done { background: #fff; }
.sheet-pop .seg.stopped { background: var(--dim); }
/* A step that did not apply lies flat instead of sitting empty, so three unlit
   segments at the end of a clean run do not read as an unfinished job. */
.sheet-pop .seg.na { transform: scaleY(.34); opacity: .5; }
.sheet-pop .seg.on { background: rgba(255, 255, 255, .4); }
.sheet-pop .seg.on i {
  position: absolute; inset: 0; background: #fff;
  transform: translateX(-101%); animation: tbseg 1.15s linear infinite;
}
@keyframes tbseg { to { transform: translateX(101%); } }

/* ---- the verdict: the one line that answers "did that get me anything?" -- */
.sheet-pop .verdict { padding: .55rem .9rem .1rem; }
.sheet-pop .vh {
  font-size: 1.45rem; font-weight: 800; letter-spacing: -.02em;
  line-height: 1.15; color: #fff;
}
/* While it runs this line is a phase, not a verdict - "Sorting 7,536 names
   into shows and movies" at 1.45rem is three wrapped lines of nothing. */
.sheet-pop[data-state="running"] .vh { font-size: 1.0667rem; letter-spacing: 0; }
.sheet-pop .vs { font-size: 0.8333rem; color: var(--dim); margin-top: .15rem; }
.sheet-pop .vh:empty, .sheet-pop .vs:empty { display: none; }

/* One line for the thing that needs saying out of band: files that failed to
   write, a stale answer, or the unmatched count. Amber only for the last of
   those - it is the app's one meaning for the colour, results being hidden,
   and unnamed files on the account are exactly that. */
.sheet-pop .flag {
  margin: .5rem .9rem 0; padding: .4rem .55rem;
  font-size: 0.7667rem; line-height: 1.4; color: var(--fg);
  border-radius: var(--r-s); background: var(--elev);
  box-shadow: inset 0 0 0 .06em var(--line-hi);
}
.sheet-pop .flag.amber {
  color: var(--hide-tint); background: var(--hide-fill);
  box-shadow: inset 0 0 0 .06em var(--hide-ring);
}

/* ---- the steps, behind Details ---- */
/* No top margin: .stepswrap owns the gap now so that it opens WITH the row.
   This rule and .stepswrap > .steps have equal specificity and this one is
   further down the file, so a margin here silently wins and leaves a band
   above a panel that is closed. */
.sheet-pop .steps { list-style: none; margin: 0; padding: 0 .9rem; }
.sheet-pop .st {
  display: flex; align-items: center; gap: .6rem;
  padding: .28rem 0; font-size: 0.8667rem; color: var(--dimmer);
}
.sheet-pop .st .dot {
  flex: none; width: 1.05rem; height: 1.05rem; border-radius: 50%;
  display: grid; place-items: center; font-size: 0.6667rem; font-weight: 800;
  box-shadow: inset 0 0 0 .09em var(--line-hi); color: #000;
}
/* Four outcomes, four looks, no hue: did it, doing it, did not have to, and
   stopped here. The old stylesheet had one class for the last two, so the step
   that crashed was drawn exactly like the ones never attempted. */
.sheet-pop .st.done { color: var(--fg); }
.sheet-pop .st.done .dot { background: #fff; box-shadow: none; }
/* ---- thinking ----
   The step that is running shimmers, so a panel that is waiting on a slow
   TorBox link looks like it is working rather than like it has stopped. This
   matters here more than anywhere else in the app: a library pull takes
   minutes, the numbers on the line can sit unchanged for a long stretch of
   it, and the honest answer to "is this still going" was previously nothing at
   all.

   A band moved with transform, not a background-position or a mask - those
   animate off the compositor, and this runs for the whole length of a pull. */
.sheet-pop .st.active { color: #fff; font-weight: 700; position: relative; overflow: hidden; }
.sheet-pop .st.active::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(90deg, transparent 20%,
              rgba(255, 255, 255, .13) 50%, transparent 80%);
  transform: translateX(-100%);
  animation: thinkSweep 1.9s var(--ease) infinite;
}
@keyframes thinkSweep {
  0%        { transform: translateX(-100%); }
  55%, 100% { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
  .sheet-pop .st.active::after { animation: none; opacity: 0; }
}
.sheet-pop .st.active .dot {
  box-shadow: inset 0 0 0 .09em #fff;
  animation: tbwait 1.4s ease-in-out infinite;
}
/* Opacity, not box-shadow: it is one of the two properties this screen can
   animate without repainting, and it survives reduced motion as a plain
   fully-drawn ring rather than as nothing. */
@keyframes tbwait { 50% { opacity: .38; } }
.sheet-pop .st.stopped { color: #fff; font-weight: 700; }
.sheet-pop .st.stopped .dot { box-shadow: inset 0 0 0 .09em #fff; color: #fff; }
.sheet-pop .st.noop { opacity: .6; }
.sheet-pop .st.noop .dot { box-shadow: inset 0 0 0 .09em var(--line-hi); color: var(--dim); }
.sheet-pop .st.skip { opacity: .38; }
.sheet-pop .st.skip .dot { box-shadow: inset 0 0 0 .09em var(--line); }
.sheet-pop .st .lb { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sheet-pop .st .sub {
  margin-left: auto; padding-left: .5rem; flex: none;
  font-size: 0.7667rem; color: var(--dimmer);
}
.sheet-pop .st .sub:empty { display: none; }

/* ---- one scroller, and what is in it ---- */
/* .items used to carry its own max-height and overflow inside a .body that
   already scrolled: two nested scroll regions about 165px and 390px tall, so a
   drag inside the list of titles trapped in the inner one and the way past it
   was a 14px gutter. One region owns the panel now, and it contains its
   overscroll the way every other scrollable surface in this file does. */
.sheet-pop .body {
  padding: 0 0 .5rem; max-height: min(22rem, 50vh);
  overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
}
.sheet-pop .detail { padding: 0 .9rem; }
.sheet-pop .quiet { font-size: 0.8333rem; color: var(--dim); padding: .5rem 0; }
.sheet-pop .items { list-style: none; margin: .5rem 0 0; padding: 0; }
.sheet-pop .items li {
  display: flex; gap: .5rem; font-size: 0.8rem; padding: .16rem 0;
  color: var(--dim); align-items: baseline;
}
.sheet-pop .items li span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sheet-pop .items .b { flex: none; font-weight: 800; color: var(--fg); width: .7rem; }
.sheet-pop .items li.minus { opacity: .7; }
.sheet-pop .items .more-n { color: var(--dimmer); font-style: italic; }

/* ---- the two controls at the foot ---- */
/* Full-width halves rather than a 67x23px link tucked beside a 24px close
   button in the header. Nothing in this panel was ever sized for a
   touchscreen - it is the one interactive surface in the file with no
   coarse-pointer rules at all. */
.sheet-pop .acts { display: flex; border-top: .06em solid var(--line); }
.sheet-pop .acts .lnk {
  flex: 1 1 0; min-width: 0; min-height: 3rem;
  font-size: 0.7333rem; font-weight: 800; letter-spacing: .12em; text-transform: uppercase;
  color: var(--dim); background: none; border: 0;
  transition: color .2s, background .2s;
}
.sheet-pop .acts .lnk + .lnk { border-left: .06em solid var(--line); }
.sheet-pop .acts .lnk:hover { color: #fff; background: var(--elev); }
.sheet-pop .acts .lnk:active { color: #000; background: #fff; }

/* ---- history view inside the same panel ---- */
.sheet-pop .sum { display: flex; gap: 1.4rem; padding: .5rem 0 .35rem; }
.sheet-pop .sum .n { font-size: 1.35rem; font-weight: 800; letter-spacing: -.02em; }
.sheet-pop .sum .l {
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .14em;
  text-transform: uppercase; color: var(--dimmer);
}
.sheet-pop .dayhd {
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .16em; text-transform: uppercase;
  color: var(--dimmer); margin: .75rem 0 .2rem;
  padding-top: .5rem; border-top: .06em solid var(--line);
}
.sheet-pop .dayhd:first-of-type { border-top: 0; padding-top: 0; }
.sheet-pop .hrow { display: flex; align-items: center; gap: .45rem; padding: .18rem 0; }
.sheet-pop .hw { font-size: 0.7667rem; color: var(--dim); font-variant-numeric: tabular-nums; }
.sheet-pop .pill {
  font-size: 0.7rem; font-weight: 800; border-radius: 99px; padding: .05rem .42rem;
  background: #fff; color: #000;
}
.sheet-pop .pill.minus { background: transparent; color: var(--fg);
  box-shadow: inset 0 0 0 .09em var(--line-hi); }

/* ---- the countdown to closing itself, so it never just vanishes ---- */
.sheet-pop .drain {
  position: absolute; left: 0; right: 0; bottom: 0; height: .12rem;
  background: var(--line); overflow: hidden;
}
.sheet-pop .drain i {
  display: block; height: 100%; width: 100%; background: #fff;
  transform-origin: left center; animation: drain linear forwards;
}
@keyframes drain { from { transform: scaleX(1); } to { transform: scaleX(0); } }
/* Held by a touch or a hovering mouse. The bar stays, full and dim, and says
   so: it used to be deleted outright, so the panel silently stopped being
   temporary with nothing on screen different from one still counting down. */
.sheet-pop .drain.held { opacity: .4; }
.sheet-pop .drain.held i { animation: none; transform: scaleX(1); }
/* Under reduced motion the countdown is a word rather than a bar - the old
   override drew the bar already empty and then closed the panel between 4.5
   and 12 seconds later anyway, which is exactly the ambush the bar exists to
   prevent. This variant is in normal flow, under the two buttons. */
.sheet-pop .drain.quiet {
  position: static; height: auto; background: none;
  padding: .3rem .9rem .4rem; text-align: right;
  font-size: 0.7rem; letter-spacing: .04em; color: var(--dimmer);
  border-top: .06em solid var(--line);
}

.sheet-pop.leaving { animation: sheetout .26s var(--ease) forwards; pointer-events: none; }
@keyframes sheetout { to { opacity: 0; transform: translateY(10px) scale(.985); } }
@media (prefers-reduced-motion: reduce) {
  .sheet-pop.leaving { animation: none; opacity: 0; }
}

/* The panel is the only report a screen reader gets of a job that deletes
   files, and it had no role, no live region and no accessible name on its
   close button. */
.sheet-pop .sr {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* ---- the header button while a pull is running ---- */
/* Keyed off the root element. chrome() rebuilds the header on every screen
   render, so a class written onto the button was thrown away by the next
   navigation and the icon stopped spinning while the job kept going. */
html.tb-pulling .top .icon[data-go="refresh"] { color: #fff; }
/* The arrows, not the ring around them - the ring carries the angle and a
   ring that also rotated would be reporting nothing twice. */
html.tb-pulling .fab svg:not(.fabring) { animation: spin 1.5s linear infinite; }
.top .icon.busy { color: #fff; }
@media (prefers-reduced-motion: reduce) {
  html.tb-pulling .top .icon[data-go="refresh"] svg { animation: none; }
}

/* ---- touch ---- */
/* 52px is the house minimum and nothing here was near it: a 24px close button
   and a 17px History link, adjacent, both with hover as their only feedback.
   The general coarse rule covers the 375px phone; the car block that follows
   takes it the rest of the way. */
@media (pointer: coarse) {
  .sheet-pop .hd .x { width: 2.9rem; height: 2.9rem; }
  .sheet-pop .acts .lnk { min-height: 3.2rem; font-size: 0.7667rem; }
}
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .sheet-pop { width: min(30rem, calc(100vw - 2rem)); right: 1.2rem; bottom: 1.2rem; }
  .sheet-pop .hd { padding: .8rem .6rem .75rem 1.1rem; }
  .sheet-pop .hd .hdt, .sheet-pop .hd .t { font-size: 0.9rem; }
  .sheet-pop .hd .x { width: 3.5rem; height: 3.5rem; font-size: 1.4rem; }
  /* 3.5rem is the 52px target at this root size; stated as min-height so the
     number is the rule rather than a by-product of the line height. */
  .sheet-pop .acts .lnk { min-height: 3.5rem; font-size: 0.8333rem; }
  .sheet-pop .track { padding: .9rem 1.1rem .1rem; }
  .sheet-pop .verdict { padding: .7rem 1.1rem .1rem; }
  .sheet-pop .vh { font-size: 1.6rem; }
  .sheet-pop[data-state="running"] .vh { font-size: 1.1333rem; }
  .sheet-pop .vs { font-size: 0.9rem; }
  .sheet-pop .flag { margin: .6rem 1.1rem 0; font-size: 0.8333rem; }
  .sheet-pop .steps { padding: 0 1.1rem; }
  .sheet-pop .st { font-size: 0.9333rem; padding: .34rem 0; }
  .sheet-pop .st .sub { font-size: 0.8333rem; }
  .sheet-pop .detail { padding: 0 1.1rem; }
  .sheet-pop .items li { font-size: 0.8667rem; padding: .22rem 0; }
  .sheet-pop .body { max-height: min(20rem, 46vh); }
  .sheet-pop .drain.quiet { padding: .35rem 1.1rem .45rem; font-size: 0.7667rem; }
}

/* ------------------------------------------------------- scrub preview -- */
.player .preview {
  position: absolute; bottom: 2.6rem; left: 0; transform: translateX(-50%);
  width: 12rem; border-radius: var(--r-s); overflow: hidden;
  background: rgba(0,0,0,.92); box-shadow: var(--shadow-lg);
  border: .06em solid var(--line-hi);
  /* riseC, not rise. This box centres itself on the pointer with
     translateX(-50%), and `rise` animates transform - so for the whole 160ms
     of the entrance the centering was cancelled and the panel sat with its
     LEFT edge under the cursor, then jumped about 90px sideways when the
     animation released it. Nobody saw it while previews only appeared under a
     finger mid-drag; a mouse opens one every time it touches the bar.
     A local keyframe rather than a fix to `rise`, which 21 other rules use. */
  animation: riseC .16s var(--ease); pointer-events: none; z-index: 4;
}
@keyframes riseC {
  from { opacity: 0; transform: translateX(-50%) translateY(14px); }
  to   { opacity: 1; transform: translateX(-50%); }
}
.player .preview canvas { display: block; width: 100%; height: auto; background: #0b0b0b; }
.player .preview canvas.empty { height: 4.2rem; }
.player .preview .pvmeta { padding: .35rem .5rem .45rem; text-align: center; }
.player .preview .pvch {
  display: block; font-size: 0.7667rem; font-weight: 700; color: var(--fg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.player .preview .pvt {
  font-size: 0.8rem; font-weight: 800; color: var(--dim);
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------- tap to seek -- */
.player .taps { position: absolute; inset: 0; z-index: 1; display: flex; }
.player .tapzone { flex: 1; position: relative; }
.player .tapzone .ripple {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(.7);
  width: 5.5rem; height: 5.5rem; border-radius: 50%;
  display: grid; place-items: center; color: #fff;
  background: rgba(255,255,255,.14);
  opacity: 0; pointer-events: none;
}
.player .tapzone .ripple svg { width: 2.2rem; height: 2.2rem; }
.player .tapzone.flash .ripple { animation: tapflash .5s var(--ease); }
@keyframes tapflash {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(.7); }
  35%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(1.15); }
}

/* ----------------------------------------------------------- subtitles --
   The default cue is sized for a laptop and boxed in black, which is heavy on
   a dark film and small from a car seat. --cue-size is set from the player's
   own setting; the three styles are the ones worth having. */
.player video::cue {
  font-family: Inter, system-ui, sans-serif;
  font-size: var(--cue-size, 3.3vw);
  line-height: 1.35;
  color: #fff;
  background: transparent;
  text-shadow: 0 .08em .28em rgba(0,0,0,.95), 0 0 .06em rgba(0,0,0,.9);
}
.player[data-cue="box"] video::cue {
  background: rgba(0,0,0,.72);
  text-shadow: none;
}
.player[data-cue="outline"] video::cue {
  text-shadow:
    -.045em -.045em 0 #000,  .045em -.045em 0 #000,
    -.045em  .045em 0 #000,  .045em  .045em 0 #000,
     0 .06em .18em rgba(0,0,0,.8);
}

/* ------------------------------------------------------------- motion --
   One entrance for the controls rather than a hard cut, and a press response
   on every target - on a touchscreen with no hover, the press IS the feedback. */
.player .ptop, .player .pbottom { animation: ctrlsin .28s var(--ease); }
@keyframes ctrlsin { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.player .ptop { animation-name: ctrlsdown; }
@keyframes ctrlsdown { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
.player .iconbtn { transition: background .18s, transform .12s var(--ease); }
.player .iconbtn:active { transform: scale(.88); background: rgba(255,255,255,.22); }
/* A crop other than Fit is a state the picture is in, and it is not otherwise
   visible - the bars are gone, which looks like the film simply is that shape.
   White, because that is this app's word for "you are here", and no hue is
   added. */
.player .iconbtn.crop.on { background: #fff; color: #000; }
.player .pctrls .iconbtn.big:active { transform: scale(.92); }
.player .skipintro { animation: rise .3s var(--ease); }
@media (prefers-reduced-motion: reduce) {
  .player .ptop, .player .pbottom, .player .preview, .player .skipintro { animation: none; }
  .player .tapzone.flash .ripple { animation: none; }
}

/* -------------------------------------------------------- pause screen -- */
.player .pauseover {
  position: absolute; inset: 0; z-index: 3;
  display: flex; align-items: center;
  padding: 0 var(--pad);
  /* Heavier than it looks like it needs to be: a bright frame - a studio
     ident, a snow scene - will otherwise shout over the text. The picture
     stays legible on the right, just clearly subordinate. */
  background: linear-gradient(90deg, rgba(0,0,0,.94) 0%, rgba(0,0,0,.86) 34%,
              rgba(0,0,0,.66) 60%, rgba(0,0,0,.45) 100%);
  animation: pausein .5s var(--ease);
  cursor: pointer;
}
@keyframes pausein { from { opacity: 0; } to { opacity: 1; } }

/* The pause panel replaces the transport rather than sitting over it.
 *
 * Both were on screen at once: a scrubber, a play button and a row of icons
 * laid over "You're watching" and a plot, each competing for the same corner
 * of a frame that is already carrying a picture. The panel is what you are
 * looking at while paused, so it gets the screen.
 *
 * The top bar is NOT hidden with it, and that is the whole reason this is not
 * a one-line rule. The back arrow is navigation, not playback - and pausing in
 * order to leave is an ordinary thing to want. Hiding it would mean resume,
 * wake the controls, then go back: three steps to undo one.
 *
 * It has to be lifted, though. .pauseover is z-index 3 and comes AFTER .ptop
 * in the markup, so at equal z-index the panel already painted over the back
 * button and swallowed its taps - while paused, that exit was unreachable
 * before this rule existed. Raising the bar one level puts it back on top of
 * the thing that was covering it. */
.player.paused-over .pbottom,
.player.paused-over .skipintro { display: none !important; }
.player.paused-over .ptop { z-index: 4; }
/* Only the arrow survives. The bar's label is "Rick and Morty - S8E6 - The
   CuRicksous Case of Bethjamin Button" and the panel underneath it is already
   showing the series as its own wordmark, then Season 8: Ep. 6, then that same
   title as a heading - so keeping the bar whole printed the episode twice and
   the show three times, in the one place on the screen with room to say it
   properly. The gradient goes with the text: a black band across the top of a
   panel that has its own gradient is a seam, not a background. */
.player.paused-over .ptop { background: none; }
.player.paused-over .ptop .pt { display: none; }

.player .po-body { max-width: min(38rem, 52%); }
.player .po-body > * { animation: rise .5s var(--ease) both; }
.player .po-body > :nth-child(1) { animation-delay: .04s; }
.player .po-body > :nth-child(2),
.player .po-body > :nth-child(3) { animation-delay: .10s; }
.player .po-body > :nth-child(4) { animation-delay: .16s; }
.player .po-body > :nth-child(5) { animation-delay: .21s; }
.player .po-body > :nth-child(6) { animation-delay: .27s; }
.player .po-body > :nth-child(7) { animation-delay: .33s; }

.player .po-k {
  font-size: 0.8667rem; font-weight: 600; color: var(--dim);
  margin-bottom: .5rem;
}
.player .po-logo {
  max-width: min(26rem, 100%); max-height: 7rem; object-fit: contain;
  object-position: left center; margin-bottom: .2rem;
  filter: drop-shadow(0 .12em .5em rgba(0,0,0,.7));
}
.player .po-title {
  margin: 0; font-size: clamp(2rem, 4.4vw, 3.4rem); font-weight: 800;
  letter-spacing: -.025em; line-height: 1.05; color: #fff;
}
.player .po-ep {
  margin-top: .55rem; font-size: 1.15rem; font-weight: 800; color: #fff;
}
.player .po-sub {
  margin-top: .3rem; font-size: 1.05rem; font-weight: 700; color: #fff;
}
.player .po-plot {
  margin: .7rem 0 0; font-size: 0.9rem; line-height: 1.6; color: var(--dim);
  max-width: 34rem;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical;
}

.player .po-corner {
  position: absolute; right: var(--pad); bottom: calc(var(--pbH, 7.5rem) + .7rem);
  display: flex; flex-direction: column; align-items: flex-end; gap: .3rem;
  max-width: min(22rem, 46vw);
  animation: rise .5s var(--ease) .3s both;
}
.player .po-crow { display: flex; align-items: baseline; gap: 1rem; }
/* The chapter reads as the heading of the pair: it is the answer to "where
   did I stop", and the clock line underneath is the consequence of it. */
.player .po-chap {
  font-size: 0.9333rem; font-weight: 800; color: #fff; text-align: right;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.player .po-left { font-size: 0.8667rem; color: var(--dim); font-variant-numeric: tabular-nums; }
.player .po-word {
  font-size: 0.8667rem; font-weight: 800; letter-spacing: .16em;
  text-transform: uppercase; color: #fff;
}

/* While it is up, the controls stay but the film sits back.

   By OPACITY, like .ix-bg and .vb img, and for the reason both of those
   write out: filter never paints on the car, and this was the last dim in
   the file still asking it to. On that screen the panel's white text was
   sitting over an undimmed picture - on the one screen where the picture is
   the whole window and the text is the only thing you need to read.

   It composites to the same image rather than to an approximation of it,
   because there is black behind: .player is background #000 and .player
   video is background #000, so a video at .6 over that ground IS
   brightness(.6). What is lost is saturate(.85), which has no opacity
   equivalent - though fading toward black takes chroma down with it, so the
   difference is a fraction of an already small effect, and the alternative
   is an effect that does not happen at all in the car. */
.player.paused-over video { opacity: .6; transition: opacity .5s var(--ease); }

@media (max-width: 700px) {
  .player .po-body { max-width: 74%; }
  .player .po-plot { -webkit-line-clamp: 3; }
  .player .po-corner { bottom: calc(var(--pbH, 6.4rem) + .6rem); }
}
@media (prefers-reduced-motion: reduce) {
  .player .pauseover, .player .po-body > *, .player .po-corner { animation: none; }
  .player.paused-over video { transition: none; }
}

/* --------------------------------------------------------- cast strip --
   Three overlapping headshots and their names, used in both heroes and on the
   pause screen. It answers "who is in this?" without leaving the artwork.

   The pill matters: this sits over a photograph that could be any colour, and
   white names on an unknown backdrop is exactly the case a scrim cannot cover
   everywhere. Sized in em so one rule serves a hero at 1307px and the pause
   panel, which set their own font-size. */
/* Only in the hero, where it lands after that slot's ladder. The pause
   panel builds its strip before the panel is shown, so it needs nothing. */
.hero .caststrip { animation: arrive .22s var(--ease) both; }
.caststrip {
  display: inline-flex; align-items: center; gap: .6em;
  max-width: 100%; padding: .25em .95em .25em .3em;
  /* The ground stays black and stays at .45. It is not decoration: white
     names sit over an arbitrary photograph, and blur is a spatial filter -
     it redistributes detail without lowering luminance, so a blurred bright
     frame is still bright. Swapping this for the buttons' white .13 would
     composite to near-white over pale artwork and lose the names entirely. */
  background: rgba(0, 0, 0, .45);
  backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  border: .06em solid rgba(255, 255, 255, .1);
  border-radius: 99px;
}
/* Only where it goes somewhere. .cs-go is put on by castJump, which is called
   from the title page alone - the home hero and the pause panel draw the same
   strip with nothing to open, and must keep reading as a caption. */
.caststrip.cs-go { cursor: pointer; transition: border-color .18s, background .18s; }
.caststrip.cs-go:hover, .caststrip.cs-go:focus-visible {
  background: rgba(0, 0, 0, .62); border-color: rgba(255, 255, 255, .28);
}
/* Transform, not a shadow or a filter: the car composites this cheaply and
   paints the other two not at all. */
.caststrip.cs-go:active { transform: scale(.985); }
/* Where the page stops when the strip is tapped. Stated here because this
   stylesheet owns the bar's height; .5rem past it so the heading is not
   sitting on the rule. */
/* Where a hero's cast cluster lands when it is pressed. .epCast is the
   episode page's answer to .castSlot - a different name because it holds the
   guest cast rather than the title's - and it needs the same offset or the
   sticky bar covers the heading it just travelled to. */
.castSlot, .detail .epCast { scroll-margin-top: var(--clear); }
.caststrip .cs-faces { display: flex; flex: none; }
.caststrip .cs-faces img {
  position: relative;
  width: 1.9em; height: 2.5em; border-radius: .45em;
  object-fit: cover;
  /* Headshots frame the face high, so a centred circular crop cuts the chin
     and keeps the forehead. */
  object-position: 50% 22%;
  background: var(--bg2);
  box-shadow: 0 0 0 .12em rgba(0, 0, 0, .6);
}
.caststrip .cs-faces img + img { margin-left: -.55em; }
/* Leftmost on top, so the cluster reads front-to-back in reading order. */
.caststrip .cs-faces img:nth-child(1) { z-index: 3; }
.caststrip .cs-faces img:nth-child(2) { z-index: 2; }
.caststrip .cs-faces img:nth-child(3) { z-index: 1; }
.caststrip .cs-names {
  font-size: .95em; font-weight: 600; color: #fff; letter-spacing: -.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  /* A flex item will not shrink below its content width without this, so on a
     phone three long names would push the pill past the edge of the screen
     instead of ending in an ellipsis. */
  min-width: 0; flex: 0 1 auto;
}

/* In a hero it sits under the synopsis, above the buttons, and inherits the
   slide's entrance stagger with the rest of the body. */
.hero .castSlotHero:empty { display: none; }
.hero .castSlotHero { font-size: 0.9333rem; }
.detail .hero .castSlotHero { margin-top: .1rem; }

.player .po-cast:empty { display: none; }
.player .po-cast { margin-top: .85rem; font-size: 0.8667rem; }

@media (max-width: 640px) {
  .hero .castSlotHero, .player .po-cast { font-size: 0.8333rem; }
  .caststrip { padding-right: .8em; }
}

/* ==================================================== history / watchlist ==
   Three surfaces built from PublicMetaDB, sharing one idea: a record set in
   type. Dates and totals are the headlines, artwork sits underneath them, and
   the only new vocabulary in the whole set is the READY mark below. */

/* ------------------------------------------------------- the READY mark --
   A title that is in the library and playable right now. Two signals, both
   true at rest, because a touchscreen has no hover to discover a third: the
   ring is what you catch sweeping past, the pill is what confirms when you
   stop.

   The ring is not new - .card:hover already paints exactly this and can never
   fire on the car, so promoting it to a state costs nothing and retires some
   dead CSS. */
/* The Ready pill and its ring lived here: a play badge and a white inset
   border on the artwork of anything already downloaded. Both went with the
   same reasoning that took the dimming off the episode grid - whether a copy
   is on this disk is a fact about the machine, not about the film, and a
   poster is a picture of the film. Nothing marks ownership on artwork now;
   the title page says Play or Find releases, which is where the question
   actually gets answered. */

/* ------------------------------------------------------------- figures --
   The refresh panel's totals promoted from a panel to a page masthead: same
   weights, same tracking, same tabular numerals. */
.figures {
  display: flex; flex-wrap: wrap; gap: clamp(1.6rem, 4vw, 4rem);
  padding: .1rem var(--pad) 1.4rem;
}
.fig-n {
  font-size: clamp(2rem, 4vw, 3.2rem); font-weight: 800;
  letter-spacing: -.035em; line-height: 1; font-variant-numeric: tabular-nums;
}
/* A figure that is not knowable yet. Both declarations are load-bearing.
   .skelh's own 9rem is a NAME's width and a numeral is not a name. And the
   line-height: 1 above is the trap this rule exists for: an empty .fig-n
   measures zero, so a figures band reserved without a real line box in it
   reserves nothing at all - the non-breaking space inside the bar is what
   generates one - while an inline-block resting on that line's baseline hangs
   its descent below a strut with no half-leading left to absorb it, and the
   hole comes out taller than the digits it is standing in for. Aligned to the
   top instead, the bar is exactly the 1em the numerals will occupy at
   whatever font-size the breakpoint and the car block give them. */
.figures .fig-n .skelh { width: 3.2rem; vertical-align: top; }
.fig-l {
  margin-top: .3rem; font-size: 0.6667rem; font-weight: 800; letter-spacing: .2em;
  text-transform: uppercase; color: var(--dimmer);
}
.stand {
  margin: 0 0 1.6rem; padding: 0 var(--pad);
  max-width: calc(var(--prose) + 2 * var(--pad));
  font-size: 0.9rem; line-height: 1.6; color: var(--dim);
}
/* One declaration. There were two of these, both unconditional, with
   different padding - the second won and the first only looked like it was
   doing something. */
.morewrap { display: flex; justify-content: center; padding: .5rem var(--pad) 3rem; }
.morebtn { min-width: 12rem; justify-content: center; }

/* ----------------------------------------------------------------- day --
   A two-column spread. The dateline sticks while its own day scrolls past, so
   it is always clear which day is being read. */
/* The measure belongs to the whole page, not to the body alone. Centring only
   the entries left the masthead flush left and the record floating in the
   middle of a 2560px screen - two different pages stacked. On the car, 84rem
   is 1260px against a 1307px viewport, so this changes nothing there and
   fixes the desktop. */
/* Days as cards in a flowing grid, rather than one full-width band each.
 *
 * The old shape was a 12rem date column beside a full-width list, one day per
 * row. That is right on a phone and wasteful on anything wider: most days here
 * hold one or two sittings, so each day spent a whole band of the screen on a
 * single line of content, and the time was pushed to the far edge leaving a
 * hand's width of nothing between a title and its clock. On a landscape screen
 * the page was mostly gap.
 *
 * A day is a self-contained card now and they flow into as many columns as
 * fit. auto-fill rather than auto-fit: with auto-fit a single remaining card
 * stretches to the full width and the last row stops matching the rest.
 * align-items: start keeps a short day short instead of padding it out to its
 * neighbour's height. */
/* The calendar's second wall is gone. September was drawn twice - once ahead
   and once under Previous - and one month grid holds a month's past days and
   its future days in the same thirty cells, which is what a month is. That
   deletes the duplicate band and its wrong tally rather than patching them. */

/* The month grid that stood here is gone with the last screen that drew one.
   The Calendar is a wall of what is coming and History a wall of nights, and
   neither draws a day with nothing on it - so a cell for one, the hatch that
   marked the unread ones, and the six helpers that built them all went too.

   One rule survives it, because both walls still use it: a hairline that sits
   BETWEEN a heading's name and its count, written as a real element so it
   lands where it is written rather than after everything like an ::after. */
.mo-rule { flex: 1; height: .06em; background: currentColor; opacity: .18; }


/* ======================================================= History: nights === */
/* Not the calendar backwards. The calendar asks what is COMING and its unit is
   the release; this asks what you DID and its unit is the evening. A night is
   drawn at the size it actually was, because a ten-episode sitting of Silo and
   one episode of Ted Lasso are not the same event and a ledger of identical
   rows says they are. */

.hnight { margin: 0 0 2.4rem; }

/* The silence, measured. Across twenty months a median 18% of days carry a
   play, so the month grid this replaces spent most of itself on holes. Saying
   "29 nights, nothing" carries the same fact, draws no empty day, and grows
   with the gap - --g is set per night in histNight and capped there, so half a
   year of quiet is a long rule rather than a screen of one. */
.hn-gap {
  display: flex; align-items: center; gap: .9rem;
  margin: var(--g, 1rem) 0 calc(var(--g, 1rem) * .8);
}
.hn-gap i { flex: 1; height: 1px; background: var(--line); opacity: .55; }
.hn-gap span {
  flex: none; font-size: 0.7rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--dimmer); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.hn-gap.long i { opacity: 1; }
.hn-gap.long span { font-size: 0.7667rem; color: var(--dim); }

.hn-h { align-items: baseline; gap: .7rem; margin: 0 0 .9rem; text-transform: none; }
.hn-h b {
  flex: none; font-size: 0.7333rem; font-weight: 800; letter-spacing: .18em;
  text-transform: uppercase; color: var(--dimmer);
}
.hnight.today .hn-h b { color: #fff; }
.hn-h > span {
  flex: none; font-size: 1rem; font-weight: 700; letter-spacing: -.015em;
  color: var(--fg); text-transform: none;
}
.hn-h u {
  flex: none; text-decoration: none; font-size: 0.7333rem; font-weight: 600;
  letter-spacing: .04em; color: var(--dimmer); font-variant-numeric: tabular-nums;
}
.hn-h em {
  flex: none; font-style: normal; font-size: 0.7667rem; font-weight: 600;
  letter-spacing: .04em; color: var(--dim); text-transform: none;
  font-variant-numeric: tabular-nums;
}

.hn-b { display: flex; gap: 1.7rem; align-items: flex-start; }

/* Three sizes and no more. The middle one is what keeps a two-episode evening
   from reading either like a binge or like a single. */
.hs-hero { flex: none; width: 16rem; }
.hnight.mid .hs-hero { width: 24rem; }
.hnight.big .hs-hero { width: 34rem; }

.hs-hero, .hs-it {
  display: flex; flex-direction: column; gap: .4rem; min-width: 0;
  background: none; border: 0; padding: 0; margin: 0;
  text-align: left; color: inherit; font: inherit; cursor: pointer;
}
.hs-fr { position: relative; display: block; }
.hs-hero .st-plate { width: 100%; border-radius: var(--r-m); }
.hs-it .st-plate { width: 100%; border-radius: var(--r-s); }
/* Solid, because it sits ON a photograph where an outline disappears. */
.hs-n {
  position: absolute; left: .45rem; bottom: .45rem;
  padding: .12rem .45rem; border-radius: 5px;
  background: rgba(0, 0, 0, .78); color: #fff;
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .08em;
  text-transform: uppercase; font-variant-numeric: tabular-nums;
}
.hs-t {
  font-weight: 700; letter-spacing: -.015em; line-height: 1.2; color: var(--fg);
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.hs-hero .hs-t { font-size: 1.0667rem; margin-top: .25rem; }
.hnight.big .hs-hero .hs-t { font-size: 1.4rem; font-weight: 800; letter-spacing: -.03em; }
.hs-it .hs-t { font-size: 0.8rem; }
.hs-s {
  font-size: 0.7667rem; font-weight: 600; color: var(--dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.hs-it .hs-s { font-size: 0.7333rem; }
/* Already in the library. .st.here's own mark, kept. */
.hs-hero.here .st-plate, .hs-it.here .st-plate {
  box-shadow: inset 0 0 0 var(--ring-w) var(--ring-here);
}
.hs-hero:active .st-plate, .hs-it:active .st-plate { transform: scale(.97); }

.hn-rest { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: .6rem; }
.hn-lab {
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .18em;
  text-transform: uppercase; color: var(--dimmer);
}
.hn-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
  gap: 1rem .9rem;
}

/* ---- touch. Bare, so the car and a landscape phone match it too. ---- */
@media (pointer: coarse) {
  .hs-it { min-height: max(3.5rem, 52px); }
}

/* ---- the phone ----
   The hero keeps its picture at full width - it is the thing the night was -
   and everything else turns on its side, which is the density a phone can
   afford. Overrides .hn-b, .hs-hero, .hn-grid and .hs-it. */
@media (max-width: 700px) {
  .hn-b { flex-direction: column; gap: 1rem; }
  .hs-hero, .hnight.mid .hs-hero, .hnight.big .hs-hero { width: 100%; }
  .hnight.big .hs-hero .hs-t { font-size: 1.2rem; }
  .hn-rest { width: 100%; }
  .hn-grid { grid-template-columns: minmax(0, 1fr); gap: .7rem; }
  .hs-it {
    display: grid; grid-template-columns: 7rem minmax(0, 1fr);
    gap: 0 .7rem; align-items: center;
  }
  .hs-it .hs-fr { grid-row: 1 / span 2; }
  .hn-h { flex-wrap: wrap; }
  .hn-h .mo-rule { display: none; }
}

/* =================================================== the calendar wall ===== */
/* The Calendar stopped being a grid of days and became a wall of what is
   coming. The month grid above stays - History still uses it - but this screen
   does not: measured over the four dated months, 48 of the 66 days that carry
   anything carry exactly ONE thing, so a month grid spent most of its area
   drawing days with nothing on them, and spent it on the space where the
   picture should have been. A media calendar is read for the picture. */

/* auto-fill against a rem minimum, so the column COUNT follows the window and
   the card size follows the interface scale - 6 across a 1999px desk, 4 in the
   car, one on a phone. Nothing here is a fixed column count. */
/* 16rem, not 19: at 19 the car fitted four columns where five fit, and every
   column not taken is a row added - measured, 13,459px against 10,770px on the
   same content. A 238px thumbnail is still a thumbnail. */
.cwall {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: 1.5rem 1.4rem;
}
.cwk { margin: 0 0 2rem; }
.cwk > .sec { align-items: center; margin: 0 0 .9rem; }
.cwk > .sec span { flex: none; }
.cwk > .sec em {
  flex: none; font-style: normal; font-size: 0.7333rem; font-weight: 600;
  letter-spacing: .04em; text-transform: none; color: var(--dim);
  font-variant-numeric: tabular-nums;
}

/* One card. No ground, no border, no radius on the card itself - the picture
   is the object and everything else is a caption under it. */
.ccard {
  display: flex; flex-direction: column; gap: .45rem; min-width: 0;
  background: none; border: 0; padding: 0; margin: 0;
  text-align: left; color: inherit; font: inherit; cursor: pointer;
}
.cc-fr { position: relative; display: block; }
/* A run leans its spare stills up and to the right, so the frame needs that
   room or the fan clips the card above it. */
.cc-fr.stack { margin: .75rem .75rem 0 0; }
.ccard .st-plate { width: 100%; border-radius: var(--r-s); }
.cc-b {
  position: absolute; left: 0; right: 0; top: 0; width: auto;
}
.cc-b:nth-of-type(1) { transform: translate(.7rem, -.7rem) scale(.94); opacity: .34; }
.cc-b:nth-of-type(2) { transform: translate(.35rem, -.35rem) scale(.97); opacity: .58; }
/* Solid, not the outlined chip the release types use: this one sits ON a
   photograph, where an outline disappears into whatever is behind it. */
.cc-n {
  position: absolute; left: .45rem; bottom: .45rem;
  padding: .12rem .45rem; border-radius: 5px;
  background: rgba(0, 0, 0, .78); color: #fff;
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .08em;
  text-transform: uppercase; font-variant-numeric: tabular-nums;
}
.cc-w { display: flex; align-items: baseline; gap: .5rem; margin-top: .15rem; }
.cc-w b {
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .16em;
  text-transform: uppercase; font-variant-numeric: tabular-nums; color: var(--dim);
}
.cc-w i {
  font-style: normal; font-size: 0.6667rem; font-weight: 700; letter-spacing: .04em;
  color: var(--dimmer);
}
.ccard.today .cc-w b, .ccard.today .cc-w i { color: #fff; }
.ccard.today .st-plate { box-shadow: inset 0 var(--rule-w) 0 0 #fff; }
.cc-t {
  font-size: 1rem; font-weight: 700; letter-spacing: -.015em; line-height: 1.25;
  color: var(--fg); overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
/* A run of three or more is the evening, and size is how this app has always
   said so - .st.major's own idiom, one screen over. */
.ccard.run .cc-t { font-size: 1.1333rem; font-weight: 800; letter-spacing: -.025em; }
.cc-s {
  font-size: 0.8rem; font-weight: 600; color: var(--dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* justify-self as well as align-self: on the phone .ccard is a GRID, where
   align-self is the block axis and does nothing to width - so the badge grew
   to the full text column and read as a bar rather than a chip. In the flex
   column at every other width justify-self is inert, so one declaration covers
   both. */
.cc-k {
  align-self: flex-start; justify-self: start; padding: 0 .38rem;
  border: .06em solid var(--line-hi); border-radius: 4px;
  font-size: 0.6rem; font-weight: 800; letter-spacing: .09em;
  text-transform: uppercase; color: var(--fg); line-height: 1.15rem; white-space: nowrap;
}
.ccard:active .st-plate { transform: scale(.97); }

/* ---- the nearest day, at size ----
   A wall answers "what is coming" only once you have found the front of it.
   This is the front, said in words, and it covers the whole nearest DAY: 18 of
   the 66 days carry more than one thing, so a hero showing the first and
   saying nothing about the rest would be hiding them. */
.cnext {
  display: grid; grid-template-columns: minmax(0, 26rem) minmax(0, 1fr);
  gap: 1.8rem; align-items: center;
  margin: 0 var(--pad) 2rem; padding: 0 0 2rem;
  border-bottom: 1px solid var(--line);
}
.cn-art {
  display: block; background: none; border: 0; padding: 0; margin: 0; cursor: pointer;
}
.cn-art .st-plate { width: 100%; border-radius: var(--r-m); }
.cn-art:active .st-plate { transform: scale(.98); }
.cn-body { display: flex; flex-direction: column; gap: .5rem; min-width: 0; }
.cn-lab { display: flex; align-items: baseline; gap: .7rem; }
.cn-lab span {
  font-size: 0.7333rem; font-weight: 800; letter-spacing: .2em;
  text-transform: uppercase; color: var(--dimmer);
}
.cn-lab em {
  font-style: normal; font-size: 0.7333rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--dimmer); font-variant-numeric: tabular-nums;
}
.cn-when {
  font-size: 1.6rem; font-weight: 800; letter-spacing: -.035em; line-height: 1;
  color: var(--fg); font-variant-numeric: tabular-nums;
}
.cnext.now .cn-when { color: #fff; }
.cn-t {
  margin: 0; font-size: 1.8rem; font-weight: 800; letter-spacing: -.03em;
  line-height: 1.1; color: #fff;
}
.cn-s { font-size: 0.9333rem; font-weight: 600; color: var(--dim); }
.cn-rest {
  display: flex; flex-direction: column; gap: .6rem;
  margin-top: .4rem; padding-top: .9rem; border-top: 1px solid var(--line);
}
.cn-more {
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .18em;
  text-transform: uppercase; color: var(--dimmer);
}
.cn-row { display: flex; align-items: center; gap: 1.1rem; flex-wrap: wrap; }
.cn-it {
  display: flex; align-items: center; gap: .6rem; min-width: 0;
  background: none; border: 0; padding: 0; margin: 0;
  text-align: left; color: inherit; font: inherit; cursor: pointer;
}
.cn-it .st-plate { width: 5rem; border-radius: 6px; }
.cn-itx { display: flex; flex-direction: column; min-width: 0; }
.cn-itx b {
  font-size: 0.8rem; font-weight: 700; color: var(--fg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cn-itx i {
  font-style: normal; font-size: 0.7333rem; font-weight: 600; color: var(--dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cn-plus {
  font-size: 0.8rem; font-weight: 800; letter-spacing: .06em;
  color: var(--dimmer); font-variant-numeric: tabular-nums;
}

/* ---- the month jump ----
   A jump, not a calendar. It never draws a day; it carries what is under each
   month and scrolls the wall to it, which is the one job the grid was still
   being kept for. */
.cjump {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  margin: 0 var(--pad) 1.4rem;
}
/* A .qtok by another name, so it takes .qtok's measurements exactly - the same
   min-height, the same padding, the same three sizes across the same three
   blocks. It had its own: baseline alignment and a 2.4rem box that the touch
   floor then grew to 52px, which left a 12px label sitting 6px from the top of
   a 53px lozenge with 28px under it. Beside a 33px .qtok on the same screen,
   that is two pill families disagreeing about what a pill is. */
.cchip {
  display: inline-flex; align-items: center; gap: .45rem; flex: none;
  min-height: 2.35rem; padding: 0 .7rem; border-radius: 999px;
  background: var(--elev); border: 0; color: var(--dim);
  font-size: .8rem; font-weight: 700; cursor: pointer;
}
.cchip:active { transform: scale(.97); }
.cchip em {
  font-style: normal; font-size: 0.7333rem; font-weight: 700;
  font-variant-numeric: tabular-nums; opacity: .65;
}
.cchip.now { background: var(--accent-deep); color: var(--accent-ink); }

/* ---- what already came out ---- */
.calpast { margin-top: 1.4rem; }
.calpast > .sec { margin: 0 0 .9rem; padding: 0 var(--pad); }

/* ---- touch ----
   Bare, no width bound, so the car at 1307 and a landscape phone match it too.
   The card is one press target and the picture already gives it height; these
   are the two controls that do not. */
/* .cn-it is a row with a picture in it, not a pill - it takes the floor here
   so the car AND both phone orientations get it. The month chip does NOT: it
   is a .qtok and takes .qtok's car sizing further down, which is where this
   file has always put a pill's touch size. */
@media (pointer: coarse) {
  .cn-it { min-height: max(3.5rem, 52px); }
}

/* ---- the phone ----
   A full-width picture per item is a beautiful card and a 90-card scroll. The
   same card turns on its side instead: the picture stays the first thing, at
   the size a phone can afford, and the page stops being three times longer
   than it needs to be. Overrides .cwall, .ccard, .cc-fr and .cnext. */
@media (max-width: 700px) {
  .cchip { min-height: 2.2rem; font-size: .7667rem; }
  .cwall { grid-template-columns: minmax(0, 1fr); gap: .9rem; }
  .ccard {
    display: grid; grid-template-columns: 8.2rem minmax(0, 1fr);
    gap: 0 .8rem; align-items: center; min-height: 4rem;
  }
  .cc-fr { grid-row: 1 / span 4; }
  .cc-fr.stack { margin: 0; }
  .cc-b { display: none; }
  .cc-t { font-size: 0.9333rem; }
  .ccard.run .cc-t { font-size: 1rem; }
  .cc-s { font-size: 0.7333rem; }
  .cnext { grid-template-columns: minmax(0, 1fr); gap: .9rem; }
  .cn-t { font-size: 1.4rem; }
  .cn-when { font-size: 1.3rem; }
  .cn-row { flex-direction: column; align-items: stretch; }
  .cn-it { width: 100%; }
}



/* Newspaper columns, because this wall is a DATE ORDER and a grid destroyed
   it. Packed, the days came out reading Yesterday, Saturday, Wednesday,
   Thursday down the first column while Wednesday, Friday, Sunday ran down the
   second - so whichever way you read it you were skipping days, and because
   packed rows do not line up there was nothing to say which way was meant.
   Columns give the sequence back: straight down the first, then back to the
   top of the next. Third time today - the person timeline and the Coming up
   agenda had the identical fault and took the identical fix - and the rule
   behind all three is the same one: a grid is for things with no order. */
.histbody { padding: 0 var(--pad) 1rem; display: block; }
/* ------------------------------------------------------- packed rows --
   A grid row is as tall as its tallest card, and days here run from one
   sitting to sixty. So three cards in every four had the rest of their row
   empty underneath them: 2,606px of it at 1600, the worst single hole 246px,
   and 3,328px the day the sixty-sitting card is on screen. That is
   arithmetic, not a spacing bug - with cards of different heights you can
   have row-major reading order or no gaps, and not both.

   Unless the rows are too small to be rows. Two-pixel rows, no row gap, and
   every card spanning as many as its own height needs, so the leftover under
   it is exactly one gutter. Reading order survives because sparse
   auto-placement only moves its cursor forward and never backfills - card K
   is always placed at or after card K-1 - and that same forward-only cursor
   is why an appended page cannot move a card already on screen. That last
   part is the whole reason this is a grid and not columns: a balanced
   multi-column box re-balances on every append, and 27 of this account's 64
   pages return a single day.

   align-items: start stays, and is load-bearing twice over now. It is what
   keeps a card at its own height inside its span, so the space the span
   bought stays a gutter instead of being stretched into card.

   .packed is written by the packer itself, after it has measured. Everything
   this screen paints before then - three skeleton seeds, a .sfail retry,
   "Nothing recorded yet." - matches none of these rules and lays out exactly
   as it does today, and a pack that threw half-way leaves the class off and
   its stale --sp values inert. Verified at two tracks, which is where an
   unspanned child used to land on top of another one.

   minmax(2px, auto) rather than a flat 2px for the one case a class cannot
   catch: a card that has outgrown its span because the window narrowed, or a
   future child nothing measured. An auto row grows to fit whatever spans it,
   so such a card pushes the wall down instead of printing over its
   neighbour. It is free - identical to a flat 2px to the pixel and to the
   gutter when the spans are right - and it turns the only remaining failure
   back into the gap this rule exists to remove. */
/* A day is never split across a column break - its date would head an empty
   space in one column while its sittings head nothing in the next. And the
   vertical rhythm moves onto the day itself: column-gap sets the space
   BETWEEN columns, and there is no row-gap in a multicol flow. */
/* ONE rule, and that is the fix rather than a tidy-up. This was two: the first
   set the margin the paragraph above explains, the second - three lines later,
   same selector, same specificity - set margin-bottom: 0 and won on source
   order. So every day card in a column sat flush against the next one, all
   forty gaps measured at exactly 0px, and the wall read as one welded slab
   instead of a stack of cards.
   The zero was a leftover from when this was a grid and the container's own
   gap did the work. Multicol has no row-gap, which is precisely why the margin
   moved onto the card - and then the leftover cancelled it in the same commit
   that added it. */
/* A day is not a box any more, and that is the whole of the fix.
   Every previous shape here tiled boxes of unequal height and then traded
   reading order against empty space, because that trade is forced: a grid row
   is as tall as its tallest card, packed rows give up row-major order, and a
   column three screens deep is not a column. This is not a fourth way of
   tiling them. The date goes into a margin and the day becomes a line, and
   days stack down the page in date order - which is what the phone has been
   drawing all along, promoted to the base.
   scroll-margin-top is for the Coming up widget's day jump, which today lands
   the chosen day at y=0 underneath the fixed header. .castSlot does the same
   one screen over; this adds the month band's height on top of it. */
.st-plate {
  width: 5.6rem; aspect-ratio: 16/9; border-radius: var(--r-s); overflow: hidden;
  /* No ring. It held the shape of a 5.6rem slot in a row and separated a dark
     still from a dark row; in a month cell it drew OVER the loaded image's
     edge, on every item, so a wall whose whole idea is that nothing has a
     border was in fact a hundred and thirty bordered boxes. The ground stays -
     it is what the shimmer sweeps and what a failed image falls back to - and
     a loaded image covers it, so it is never seen. Deck layers are told apart
     by rotation and opacity below, never by this. */
  background: var(--bg2);
  display: grid; place-items: center; flex: none;
}
/* Something rather than a hole, when there is no picture to be had. */
.st-mono {
  font-size: 1.15rem; font-weight: 800; color: var(--dimmer);
  letter-spacing: -.02em;
}
.st-plate img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* .st-t / .st-sub / .st-when / .st-kind and the .st press rules went with the
   row they belonged to. .ci carries all four jobs now and ports the press
   verbatim; .st-plate and .st-mono stay exactly as they are,
   because .ci reuses them and so does imgSettled's IMG_FRAMES. */
.st-plate { transition: transform .12s var(--ease); }

/* --------------------------------------------------------- watchlist --- */
.wlbody { padding-bottom: 1rem; }
/* The gutter, because .wlbody does not supply one - its .grid children each
   carry their own, so the heading between them was the only thing in the
   column still touching the window. Every other place .sec appears sits in a
   container that already pads (.sheet, .mt-head, .spine, .acthead), which is
   why this is the one exception and not a general rule. Checked against the
   watchlist, both list screens, the person page, collections and a series. */
.wlbody .sec { margin-top: 1.6rem; padding: 0 var(--pad); }

/* -------------------------------------------------------------- motion --
   chrome() puts the page body into .screen as a single child, so the existing
   .screen > * ladder animates the whole page as one block. These pages want
   their parts to arrive in reading order, so they say so - same duration,
   same ease, same step as that ladder. */
.hist > *, .wl > * { animation: rise .55s var(--ease) both; }
.hist > :nth-child(1), .wl > :nth-child(1) { animation-delay: .02s; }
.hist > :nth-child(2), .wl > :nth-child(2) { animation-delay: .08s; }
.hist > :nth-child(3), .wl > :nth-child(3) { animation-delay: .14s; }
.hist > :nth-child(4), .wl > :nth-child(4) { animation-delay: .20s; }
.hist > :nth-child(n+5), .wl > :nth-child(n+5) { animation-delay: .26s; }
/* Days rise one ladder deeper. Sittings inside a day do NOT animate
   individually: thirty rows each with their own delay is a stutter on the
   car, and a day should land as one printed block. */
/* A MONTH is the block that lands now, not a day - the days are inside one.
   Left pointing at .histday this matches nothing and every day silently takes
   the same .05s, which is the kind of miss that has no wrong-looking frame to
   catch it in review. */
.histbody > .cwk, .histbody > .hnight { animation: rise .55s var(--ease) both; animation-delay: .05s; }
.histbody > :nth-child(2) { animation-delay: .11s; }
.histbody > :nth-child(3) { animation-delay: .17s; }
.histbody > :nth-child(4) { animation-delay: .23s; }

.pickSlot > .rail { animation: rise .55s var(--ease) both; }
.pickSlot > .picksmast { animation: rise .55s var(--ease) both; }
.pickSlot > .rail:nth-child(2) { animation-delay: .07s; }
.pickSlot > .rail:nth-child(3) { animation-delay: .14s; }
.pickSlot > .rail:nth-child(4) { animation-delay: .21s; }
.pickSlot > .rail:nth-child(5) { animation-delay: .28s; }

@media (prefers-reduced-motion: reduce) {
  /* The two rail slots join the list rather than being left to the global
     block at the top of the file. That block zeroes durations; a SECOND
     global block further down zeroes every animation-delay and
     transition-delay with !important on the universal selector, which the
     ladder above cannot outrank - so the delays this comment used to say
     survived the reset do not, and the two sentences claiming they did have
     gone. This is belt and braces now rather than the only thing standing
     between the reader and a rail that sits invisible for 150ms.
     One edit for both because the ladder already declares them with one
     selector and the two under it are identical, and fixing one twin and not
     the other is how this file grows disagreements. */
  .hist > *, .wl > *, .histbody > .cwk, .histbody > .hnight, .pickSlot > .rail,
  .discSlot > .rail { animation: none; }
  .st-plate { transition: none; }
}

/* --------------------------------------------------------------- phone --
   The spread collapses to a column and the dateline turns on its side. */
@media (max-width: 700px) {
  /* Everything this block used to say about the day wall is the base rule
     now: no frame, no ground, no padding, one column, and today marked by type
     rather than by a box. The phone was the only device already reading this
     wall in order, so the fix was to promote what it had rather than to add a
     third treatment. What stays below is phone SIZING, which is still its
     own. */
  /* The numeral and the when-block that stood here belonged to the day card
     and its three-column row. The phone's month rules live with the grid
     itself now, in one place, next to the widths they answer. */
}

/* ==================================================== what is coming ====== */

/* An agenda: days in order, everything under each of them, every row the same
   height. The plainness is the design - three attempts before this traded it
   away for an idea, and the last one, columns left at their natural height so
   a busy day was a tall one, read as a wall of mismatched cards with gaps
   under the short ones.

   So there are no cards here, no columns and no chart. One column of identical
   rows under thin day rules. Nothing varies but the words, so there is nothing
   to compare and nothing to leave a gap in - and one list works at 375px and
   at 1300 without being rearranged, because a list always has. */
.calwid .cw-list {
  display: flex; flex-direction: column;
  padding: 0 var(--pad) .2rem;
}

/* The day, as a rule with words on it rather than a card edge: it separates
   without boxing, which is what lets every row keep the same shape whether its
   day holds one thing or four. */
.calwid .cw-hd {
  display: flex; align-items: center; gap: .6rem; width: 100%;
  padding: .7rem .4rem .3rem; margin: 0; background: none; border: 0;
  cursor: pointer; text-align: left; color: var(--dimmer);
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .12em;
  text-transform: uppercase;
  transition: color .18s;
}
.calwid .cw-hd:first-child { padding-top: .1rem; }
.calwid .cw-hd:hover, .calwid .cw-hd:focus-visible { color: var(--fg); }
.calwid .cw-hd.now { color: #fff; }
/* The rule runs from the words to the count, so a day with a short name gets a
   long rule and the eye still lands on the right-hand number. */
.calwid .cw-hd span { flex: none; }
.calwid .cw-hd::after {
  content: ''; flex: 1; height: .06em; background: currentColor; opacity: .18;
}
.calwid .cw-hd em {
  flex: none; font-style: normal; font-variant-numeric: tabular-nums;
  letter-spacing: 0; opacity: .7;
}

.calwid .cw-it {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  align-items: center; gap: .1rem .7rem; width: 100%;
  padding: .35rem .4rem; border-radius: var(--r-s);
  background: none; border: 0; cursor: pointer; text-align: left;
  color: var(--fg); font: inherit;
  transition: background .18s, transform .15s var(--ease);
}
.calwid .cw-it:hover, .calwid .cw-it:focus-visible { background: var(--elev); }
.calwid .cw-it:active { transform: scale(.995); }
.calwid .cw-it u {
  grid-row: span 2; display: block; width: 3.6rem; aspect-ratio: 16/9;
  border-radius: .28rem; overflow: hidden; background: var(--bg2);
  box-shadow: inset 0 0 0 .06em var(--line); text-decoration: none;
}
.calwid .cw-it u img { width: 100%; height: 100%; object-fit: cover; display: block; }
.calwid .cw-it b { min-width: 0; font-weight: 400; }
.calwid .cw-h {
  display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-size: 0.8333rem; font-weight: 800; letter-spacing: -.01em; line-height: 1.3;
}
.calwid .cw-s {
  display: block; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-size: 0.7333rem; font-weight: 600; color: var(--dimmer); line-height: 1.3;
}

/* What did not fit, and the way to it. */
.calwid .cw-rest {
  align-self: flex-start; margin-top: .45rem;
  padding: .3rem .4rem; background: none; border: 0; cursor: pointer;
  color: var(--dim); font-size: 0.7333rem; font-weight: 800;
  letter-spacing: .05em; transition: color .18s;
}
.calwid .cw-rest:hover, .calwid .cw-rest:focus-visible { color: var(--fg); }

.calwid h2 { cursor: pointer; }

/* The hole, before the answer: the same markup, so the height cannot be far
   wrong. How many groups is not a constant - calFillSkel works it out from
   the room it has and how many rows last time filled, so it draws six groups
   at the car's height, four at a desk and three on a phone. It said "two days
   of two" here, which is a shape none of the three devices has ever drawn. */
.calwid.skel .cw-hd, .calwid.skel .cw-it { pointer-events: none; cursor: default; }
.calwid.skel .cw-hd span, .calwid.skel .cw-it u, .calwid.skel .cw-h {
  background: var(--elev2); border-radius: 4px; color: transparent;
}
.calwid.skel .cw-hd span { width: 4rem; height: .7rem; }
.calwid.skel .cw-hd::after { opacity: .08; }
.calwid.skel .cw-h { height: 1rem; }
.calwid.skel h2 { pointer-events: none; cursor: default; }

/* Wide enough to put the episode beside the title instead of under it, which
   takes a row from two lines to one and uses the width a desk actually has. */
@media (min-width: 720px) {
  .calwid .cw-it { grid-template-columns: auto minmax(0, 22rem) minmax(0, 1fr); }
  .calwid .cw-it u { grid-row: auto; }
  .calwid .cw-s { font-size: 0.7667rem; }
}

/* THE FLOOR, AND IT HAS TO COME FIRST BECAUSE THE ONE BELOW NEVER REACHED A
   PHONE. Every row in this widget is a real button, and the block below sizes
   them for a big touch screen - but it is gated on min-width: 820px, so a
   phone, which is the one device that certainly has a thumb, was excluded by
   its own width. Measured at 375x812, where (pointer: coarse) is true: the day
   headings were 21.5px, the programme rows 42.5px and the "N more" button
   26px, against a 44px floor.

   max(), not a bare rem, for the reason app.css already gives at .pw-meter:
   rem here is 15px x --ui, so a rem-only floor moves with the interface-size
   preference and stops being a floor at the small end.

   No width clause on purpose. This says "there is a thumb", which is the
   whole question - and it deliberately does NOT try to answer it for the car,
   which reports pointer: fine and is handled where that is decided. */
@media (pointer: coarse) {
  .calwid .cw-hd { min-height: max(2.6rem, 44px); }
  .calwid .cw-it { min-height: max(3.5rem, 44px); }
  .calwid .cw-rest { min-height: max(3.5rem, 44px); }
}

/* The step up for a big touch screen - bigger type as well as a bigger box,
   because this is the read-from-a-seat size rather than the reach-it-with-a-
   thumb one. It has never applied on the car: the car reports pointer: fine,
   so this asks for something no device this app runs on reports. #/diag has
   the reading that decides whether `any-pointer` is the word it should be
   using instead; until that is read from the seat, the floor above is what
   actually holds. */
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .calwid .cw-it { min-height: 3.5rem; padding: .45rem .5rem; }
  .calwid .cw-it u { width: 4.4rem; }
  .calwid .cw-hd { min-height: 2.6rem; font-size: 0.7333rem; }
  .calwid .cw-h { font-size: 0.9333rem; }
  .calwid .cw-s { font-size: 0.8333rem; }
  .calwid .cw-rest { min-height: 3.5rem; font-size: 0.8333rem; }
}

/* ============================================================ picks ====== */

/* The section says whose lists these are before the first rail does. Home is
   otherwise a run of undifferentiated rails; this is the one place that needs
   to announce a change of voice, from "what the world is watching" to "what
   you watch". A rule and an eyebrow, no box - the same way the day divides
   itself on the history page. */
.picksmast {
  padding: clamp(1.6rem, 3.4vh, 2.8rem) var(--pad) 1rem;
  border-top: 1px solid var(--line);
  margin-top: clamp(.6rem, 2vh, 1.4rem);
}
.pm-head { display: flex; align-items: center; gap: .6rem; }
.pm-spark {
  flex: none; display: inline-flex; color: #fff;
  /* Back to a box the mark fills. The 1.9em here was sized for a SPARSE
     mark - three stars drifting across 60% of the viewBox with only one or
     two up at a time - and a dense two-star glyph at that size crowds the
     heading it sits beside. 1.35em puts it back at the weight of the type. */
  width: 1.35em; height: 1.35em;
  font-size: clamp(1.25rem, 2.2vw, 1.9rem);   /* tracks the heading */
  margin: -.35em 0;                           /* the growth does not open the row */
}
.pm-spark svg { width: 100%; height: 100%; display: block; }
.pm-k {
  font-size: clamp(1.25rem, 2.2vw, 1.9rem); font-weight: 800;
  letter-spacing: -.03em; color: var(--fg); line-height: 1.1;
}
/* Pushed to the end of the line: it balances the sparkle, and it is the only
   way to reach all fifteen - the rails show three and the tile row shows the
   rest of today's, not the page. */
.pm-all {
  margin-left: auto; flex: none;
  display: inline-flex; align-items: center; gap: .35rem;
  min-height: 2.4rem; padding: 0 .95rem;
  background: var(--elev); border: .06em solid var(--line-hi); border-radius: 99px;
  color: var(--fg); font: inherit; font-size: 0.8rem; font-weight: 800;
  letter-spacing: .1em; text-transform: uppercase; cursor: pointer;
  transition: background .2s, border-color .2s, transform .12s var(--ease);
}
.pm-all::after {
  content: ''; width: .42em; height: .42em;
  border-top: .14em solid currentColor; border-right: .14em solid currentColor;
  transform: rotate(45deg); opacity: .8;
}
.pm-all:hover { background: var(--elev2); border-color: #fff; }
.pm-all:active { transform: scale(.95); }
.pm-s {
  /* Flush with the mark, not with the heading text. The indent was meant to
     hang the sentence under the words above it, and it never quite did: the
     1.15em was the ICON's em, copied into a rule whose own font-size is
     0.8333rem, so it resolved to a different number than the thing it was
     matching. Starting at the start needs no arithmetic and cannot drift. */
  margin: .4rem 0 0; max-width: 42rem;
  font-size: 0.8333rem; line-height: 1.55; color: var(--dimmer);
}

/* It does not move, and that is the whole of it.

   There was a twinkle on arrival and, under it, three stars on an endless
   drift - grown, travelled and shrunk away on a 3.017s loop, read frame by
   frame out of a Lottie. It was a lot of care spent on a mark that sits at the
   top of the home screen for as long as the app is open, and the answer to
   "how often should this catch your eye" turned out to be: never. A section
   heading is not an event.

   So the keyframes are gone rather than disabled - twinkle, spDrift, the three
   per-star custom-property blocks and the .live gate that started them. What
   is left is an icon. Nothing here needs a reduced-motion rule any more, which
   is why the block below no longer mentions .pm-spark: there is no duration to
   zero and no delay to strip.

   .live stays on the masthead. picksSection still sets it when the lists land
   and it still means "this section has arrived" - it just no longer has an
   animation hanging off it. Deleting it would be a second change with its own
   blast radius, and it costs nothing where it is. */

@media (max-width: 700px) {
  .pm-all { font-size: 0.7333rem; padding: 0 .75rem; }
  /* Two lines of room for the standfirst, at the one width that needs them.
     The sentence is rewritten late with the reader's own counts - "Ranked
     against the 405 films and 3,395 episodes you have watched." - and at 390px
     the measure is about 364px against a sentence that wants more, so it
     becomes two lines at roughly 700ms on a page that is otherwise now
     completely still. Reserved rather than corrected because the string is
     data and has no upper bound. In em, because what is being reserved is a
     line of this element's own type; it is not taken on the car, where
     max-width: 42rem is far wider than the longest form of the sentence and it
     never wraps. */
  .pickSlot .pm-s { min-height: 3.1em; }
}
@media (prefers-reduced-motion: reduce) {
  /* The two .pm-spark rules that were here are gone with the animations they
     spared. They held the stars at --xm/--ym/--s, custom properties that no
     longer exist, so leaving them would have been a transform and an opacity
     resolving from nothing on the one setting that most needs the mark to just
     be there. The sparkle has no motion to spare now. */
  .pm-all { transition: none; }
}

/* A rail heading that goes somewhere says so at rest. There is no hover on the
   car, so the affordance cannot be something that appears on one. */
.rail h2.golink { cursor: pointer; }
.rail h2 .go {
  margin-left: .6rem; font-size: 0.7rem; font-weight: 800; letter-spacing: .14em;
  text-transform: uppercase; color: var(--dimmer);
  transition: color .2s;
}
.rail h2.golink:hover .go, .rail h2.golink:active .go { color: #fff; }

/* A pick list, as the card the index is made of - three of its own titles
   fanned like a hand, its name beside them, on the same ground.

   This used to be a name over two clamped lines of PMDB's own prose on a flat
   plate, because a pick arrived as a name and a sentence and there was nothing
   else to draw. There is now: picks() folds three posters and their colours
   into the one call this row already makes.

   The FOOTPRINT is unchanged - 15.5rem at 16/9, the width a wide card
   occupies - so the rail keeps its rhythm and only the inside of the card is
   different. That is also why the hand here is bigger than a door's: the card
   is a third wider and half again as tall, and a door's 2.27rem poster would
   rattle around in it.

   --elev rather than --bg2, and that is a fix rather than a preference. #0b0b0b
   is opaque, and an opaque surface punches a flat hole in body::before's wash
   and body::after's grain - which is what these cards have been doing, twelve
   at a time, in the one row on the page with no artwork to hide it. */
.pickcard {
  flex: 0 0 auto; width: 15.5rem; aspect-ratio: 16/9; scroll-snap-align: start;
  display: flex; align-items: center; gap: .85rem;
  padding: 1rem; text-align: left; cursor: pointer; overflow: visible;
  border: 0; border-radius: var(--r-m);
  background-color: var(--elev); color: var(--fg);
  background-image:
    radial-gradient(120% 150% at 18% 50%, rgba(var(--c1), .30), transparent 60%),
    radial-gradient(95% 130% at 100% 100%, rgba(var(--c2, var(--c1)), .17), transparent 58%);
  box-shadow: inset 0 0 0 .06em var(--line);
  transition: background-color .2s, box-shadow .25s var(--ease), transform .25s var(--ease);
}
.pickcard:hover { background-color: var(--elev2);
                  box-shadow: inset 0 0 0 var(--ring-w) var(--ring-hover);
                  transform: translateY(-4px); }
.pickcard:active { transform: translateY(-1px) scale(.98); }
/* The door's hand, at this card's size. Three declarations, the same three the
   car and the phone blocks already override on .ix-hand - the offsets are a
   fraction of --fan, so a size change is not nine numbers. */
.pickcard .ix-hand {
  --fan: 1.15rem; width: 5.9rem; height: 5.4rem;
}
.pickcard .ix-hand img { width: 3.6rem; height: 5.4rem; }
.pc-n {
  flex: 1 1 auto; min-width: 0;
  font-size: clamp(.95rem, 1.2vw, 1.15rem); font-weight: 800;
  letter-spacing: -.02em; line-height: 1.2;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}

/* Every list, as a page. width:auto in a grid mirrors .grid > .card. */
.pickgrid {
  display: grid; gap: .85rem; padding: .2rem var(--pad) 2.5rem;
  grid-template-columns: repeat(auto-fill, minmax(15.5rem, 1fr));
}
.pickgrid .pickcard { width: auto; aspect-ratio: auto; min-height: 8.4rem; }
.pickgrid .pickcard.skel { box-shadow: none; }

/* ---- what the section looks like while it is still a hole ----

   The gutter, forced. A .track is overflow-x: auto and `* { scrollbar-width:
   thin }` gives it a real gutter IN THE LAYOUT the moment it overflows, so a
   landed pick rail carrying a full page of twenty cards is exactly one
   scrollbar taller than a placeholder holding six holes that fit. Three rails
   and a tile row later that was the forty pixels the slot was short - and it
   does not read as a scrollbar, it reads as the heading being wrong by ten,
   which is where two separate attempts to explain it went looking.

   Asked for unconditionally rather than counted, because the number is not a
   number. This file declares scrollbar-width: thin AND a 6px
   ::-webkit-scrollbar height, two different answers on two engine paths, and
   it is zero wherever scrollbars overlay. Forcing `scroll` gets whatever
   answer the landed row would have got, whatever that answer is, on whatever
   this is running on.

   On the SLOT rather than on .skelrail, and that is the point rather than
   convenience: the placeholder here is not swapped out, it is written into, so
   a rule keyed to a class the reconcile removes would hand the gutter back and
   take it away again at 300ms. This way the hole and the row it becomes are
   governed by the same declaration and cannot disagree. An empty gutter draws
   nothing - the track background is transparent and no thumb exists without
   overflow - and overflow-y still computes to auto, so no vertical gutter
   appears with it.

   Deliberately NOT extended to .nextSlot, which has the same ten pixels from
   the other side - and every number this paragraph used to quote had gone
   stale, so these are measured. Continue watching is ONE rail now, not two.
   Its placeholder holds six cards, not four. And at the car's width that
   placeholder already overflows - scrollWidth 1956 against clientWidth 1911 -
   so it carries the gutter the old text said it avoided, as does the landed
   row at sixteen cards.

   The conclusion is unchanged, which is why this is a correction and not a
   rule change: hole and row both overflow, so both already have the gutter,
   and forcing it would only turn a sometimes-10px shove into an always-10px
   one in the other direction - upward, which is the worse one. That row's
   mismatch is a card COUNT, not a gutter, and it is a separate change.

   .railSlot was extended for the same reason and is gone: it held the Library
   tab's four rails, and that tab is the shelf now. The argument is left
   standing here rather than deleted because it is the reasoning .pickSlot
   still runs on, and because it is the record of WHEN to extend it - a slot
   whose rails land more cards than its hole reserves, at a width where that
   overflows, wants the always-gutter; a slot whose rails agree with their
   holes does not, and .nextSlot is that second case with one rail today.
   An empty gutter draws nothing - the track ground is transparent and no thumb
   exists without overflow. */
.pickSlot .track { overflow-x: scroll; }

/* Holes that are not the FIRST hole.
   Home already runs two dozen endlessly sweeping layers on a cold load, on a
   machine that rasterises in software, and reserving four rails instead of one
   takes the slot from six of them to thirty-three - the heading bar counts,
   .skelh carries .skel. One rail sweeps, because one rail is enough to say
   "loading", and the slot settles at seven. content: none rather than a paused
   animation: the point is to remove the layer, not to freeze it.

   The flat ground is not decoration, it is what makes them exist at all.
   .skel's own background is rgba(255,255,255,.045), which on this page
   composites to about #0b0b0b, and skelCard deliberately draws no .artbox so
   there is no outline either - the sweep is the only thing rendering it. Take
   the sweep off and change nothing else and rails two and three are black
   rectangles on a black page: the height is reserved perfectly and the section
   reads as having ended after one rail, which is a different bug rather than a
   smaller one. Static paint, so the layer count above still holds.

   And no `box-shadow: none` on the tiles, whatever .pickgrid does with its
   own. .pickgrid's skeletons keep their sweep and can afford to drop the
   outline; these cannot, and the inset line is the only edge a hushed
   .pickcard has.

   Not home's private property any longer. skelRail has carried a `hush`
   option since this was written, and scoped here it did nothing anywhere
   else - so the Library tab, which reserves four rails and would run
   twenty-eight sweeping layers to do it, could ask for the fix and not get
   it. Unscoping changes not one declaration picks receives. The .10 carries
   over untouched because it was measured against this app's background and
   there is only one background; what it is buying is the same thing here as
   there, which is that a hole with its sweep taken off still EXISTS. */
.rail.hush .skel::before { content: none; }
/* And the organic shimmer itself, which is where the cost actually is now.
   These rules exist because a reserved Library tab is twenty-eight holes and
   twenty-eight sweeping layers; the shimmer that replaced the sweep brings a
   turbulence filter and a 20px blur EACH, so a hush that only silenced
   ::before would have quietly made the expensive case far more expensive. */
.rail.hush .t-shimmer, .rail.hush .t-shimmer-edge { display: none; }
.rail.hush .skel { background: rgba(255,255,255,.10); }

/* The shelf's twenty holes, under the argument directly above and its
   measured ground. .rail.hush cannot reach them - skelBox emits a .grid or a
   .rows and neither is inside a .rail - and this is not a nicety: the Library
   tab hushed three of its four rails today, so the car runs about six
   sweeping layers there, and a wall of twenty unhushed holes would be a
   fourfold increase on a machine that rasterises in software. Hushed it is
   zero.
   The .10 ground comes with the hush and is not optional. .skel's own
   background is rgba(255,255,255,.045), which composites to about #0b0b0b,
   and skelCard draws no .artbox so there is no outline either - take the
   sweep off and change nothing else and twenty holes are black rectangles on
   a black page, which is a different bug rather than a smaller one.
   Scoped to this screen and NOT to .skelgrid, which skelBox puts on every
   skeleton in the app and boxFor reads as a marker - a rule on that class
   would take Discover's twenty holes with it, which is a separate decision.
   The class stays on .results after the wall lands; nothing a card builds
   carries .skel, so it matches nothing and is not worth a mutation to
   remove. */
.shelfskel .skel::before { content: none; }
/* Same reason as .rail.hush above: the hush has to reach the shimmer's own
   elements or it is not a hush any more. */
.shelfskel .t-shimmer, .shelfskel .t-shimmer-edge { display: none; }
.shelfskel .skel { background: rgba(255,255,255,.10); }

/* A heading that has become a live link is not a placeholder any more.
   .skelrail h2 sits back at .72 so the holes under it read as holes, and the
   rail keeps that class until its cards land - but its heading is real,
   named and pressable from the cheap call a second earlier, and a live target
   rendered at 72% is a control that looks disabled. */
.pickSlot .rail h2.golink { opacity: 1; }

/* One line, or an ellipsis. A list description is upstream text of no known
   length and .rail h2 .note has no white-space rule anywhere: as a flex item
   its min-width floors at min-content, so a long one wraps INSIDE its own item
   and makes the landed rail a line taller than the hole reserved for it. That
   is true at every width, not only on the phone where the note is a block -
   the car block gives it 0.8667rem in a heading that does not wrap.

   Truncated rather than clamped. -webkit-line-clamp on a flex item is a
   well-known way to be right until you are not, and this only ever needs one
   line; min-width: 0 is what lets the item shrink at all. It collides with
   nothing in the max-width: 700px block, which sets display, order, flex,
   margin-top, line-height, font-size and letter-spacing and none of these. */
.pickSlot .rail h2 .note {
  min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

@media (max-width: 700px) {
  /* The rail card loses 2rem of width here and the hand has to give some of it
     back, or the name is left 71px to wrap in - measured, "Everyone's talking
     about" took three lines of it. The grid on the Picks page is one column of
     362px on this screen and wants none of this, hence the .track scoping. */
  .pickcard { width: 13.5rem; }
  .track .pickcard .ix-hand { --fan: .9rem; width: 4.6rem; height: 4.2rem; }
  .track .pickcard .ix-hand img { width: 2.8rem; height: 4.2rem; }
}
@media (prefers-reduced-motion: reduce) {
  .pickcard { transition: none; }
}

/* ------------------------------------------------ episode -> its show --
   An episode page used to be a dead end: it said "Pilot" and nothing about
   which show that was. The series name sits where a standfirst goes, above
   the episode title, and is the way back to it.

   Sized as a real target rather than a line of text - this is a car, and the
   whole point is that it can be hit. It reads as a link at rest, because
   there is no hover here to reveal one. */
.detail .hero .showlink {
  align-self: flex-start;
  display: inline-flex; align-items: baseline; gap: .55rem;
  margin: 0 0 .15rem; padding: .45rem .85rem .45rem 0;
  background: none; border: 0; cursor: pointer;
  color: var(--fg); font: inherit;
  font-size: clamp(.95rem, 1.35vw, 1.2rem); font-weight: 800;
  letter-spacing: -.015em; text-align: left;
  transition: color .2s, transform .12s var(--ease);
}
.detail .hero .showlink .sl-ep {
  font-size: .8em; font-weight: 700; color: var(--dimmer); letter-spacing: .04em;
}
.detail .hero .showlink:hover { color: #fff; }
/* Nudged down rather than left. It used to carry a back chevron and slide
   left on press, which put two identical back arrows on the same screen
   pointing at two different places - the header's, which leaves the page, and
   this one, which goes deeper into the show. The name alone is the
   destination now, and the press reads as "opening" rather than "returning". */
.detail .hero .showlink:active { transform: translateY(1px); opacity: .8; }
@media (prefers-reduced-motion: reduce) { .detail .hero .showlink { transition: none; } }

/* The show's own wordmark in place of its name, once one has loaded.
   Sized as a standfirst, not as a title: the page's .hero .logo is 7.5rem
   because that is the headline, and this sits above the headline. Left
   unconstrained an episode's series mark renders 1.46x the height of the
   episode title beneath it, which inverts the hierarchy it is meant to set up.
   The height is locked rather than capped so that a wide wordmark and a
   compact one land on the same line; anything too tall to work at this size
   never gets swapped in at all (see the ratio test in itemScreen).
   1.55em, not a rem value: that is the inherited line-height, so the picture
   occupies exactly the line box the words did. The swap then costs nothing -
   at 2.1rem the button grew 35.6px -> 45px when the artwork decoded and
   shunted the title, plot and Play button down the page a beat after load,
   which in a car is a control moving under a finger already reaching for it. */
.detail .hero .showlink .sl-logo {
  /* 2.2em, up from the 1.55em that matched the line box exactly.
     A wordmark IS the show's name, and at the height of a line of text it was
     smaller than the episode title under it - the thing it names read louder
     than the name. The reserved height below rises with it, which is the part
     that matters: the original 1.55em was chosen so the swap could not move
     anything, and a bigger picture in a box still sized for words would put
     that shunt straight back. */
  display: block; height: 2.2em; width: auto;
  max-width: min(15rem, 48vw);
  object-fit: contain; object-position: left center;
  flex: none;
  filter: drop-shadow(0 2px 10px rgba(0, 0, 0, .55));
}
/* Baseline is right for two runs of text. A replaced element's baseline is
   its bottom edge, so a picture aligned that way hangs the "S8 E6" descender
   space below itself and pads the button with dead height. Only once the
   swap has happened - the text path keeps the baseline it was designed with. */
.detail .hero .showlink.has-logo { align-items: center; }
/* The target must not move when the artwork arrives - the reason this number
   exists at all. Held at the LOGO's height rather than the text's, so the box
   is already the size the picture will need and the swap changes nothing but
   what is inside it. Raised with the logo above: leaving it at 2.4rem would
   have reinstated exactly the shunt it was written to prevent, a control
   moving under a finger already reaching for it. */
.detail .hero .showlink { min-height: 3.4rem; }

/* ============================================================= streams === */

/* The releases live in a dialog now, not down the details page. Sixty rows of
   filename between the synopsis and everything below it was most of that
   page; a choice this long is a thing you go into and come out of. */
.strmwrap {
  position: fixed; inset: 0; z-index: var(--z-stream);
  display: flex; align-items: flex-end; justify-content: center;
  background: rgba(0, 0, 0, .72);
  animation: fadein .2s var(--ease);
}
.strmdlg {
  width: min(56rem, 100%); max-height: min(86vh, 52rem);
  display: flex; flex-direction: column;
  background: var(--bg2); color: var(--fg);
  border: .06em solid var(--line-hi);
  border-radius: var(--r-l) var(--r-l) 0 0;
  box-shadow: var(--shadow-lg);
  animation: sheetup .28s var(--ease);
  /* A phone's home indicator sits over the last row otherwise. */
  padding-bottom: env(safe-area-inset-bottom);
}
/* Taken to its cap by a press, never by an arrival.

   "Find another release" goes to the network for up to fifteen seconds and then
   drops sixty rows into a panel anchored to the bottom edge. Measured at 375:
   the panel goes 293.80 to 730.80 and the held copy, the wrong-show button and
   the button under the finger all travel 350px upward - standing, readable,
   tappable content, moved by an arrival. Growing at the moment of the tap moves
   the same distance, but the reader caused it, there is nothing below the
   button to mis-tap into afterwards, and the arrival then moves nothing at all.
   It is a relocation of the shift, not a removal, and it is worth it because of
   which end of the wait it happens at.

   height rather than a second max-height, so the cap keeps living in exactly
   one declaration: the 90vh overrides at (max-width: 700px) and
   (max-height: 500px) clamp this without knowing it exists. Measured 688 at
   1280x800 and 730.80 at 375x812, which is that 90vh. Never removed except by
   closing the dialog: on a failure or a short list the panel keeps its height
   and lands its message with dead space beneath it, which is ugly and still,
   and keeps its retry control - a better failure than a 495px collapse. */
.strmdlg.sd-grow { height: 100%; }

/* A touch of scale on the way in, so the panel reads as opening rather than
   sliding up from off-screen - it is anchored to the bottom edge and a pure
   translate is indistinguishable from the page scrolling under it. */
@keyframes sheetup {
  from { transform: translateY(14px) scale(.985); opacity: 0; }
  to   { transform: none; opacity: 1; }
}
.strmwrap.leaving { animation: fadeout .18s var(--ease) forwards; }

/* The panel leaves as well as the backdrop. Fading the backdrop alone left the
   sheet hanging at full size in a room that was going dark, which is the one
   moment it looks pasted on rather than part of the screen. */
.strmwrap.leaving .strmdlg { animation: sheetdown .18s var(--ease) forwards; }
@keyframes sheetdown { to { transform: translateY(10px) scale(.99); opacity: 0; } }
@keyframes fadeout { to { opacity: 0; } }

.sd-hd {
  display: flex; align-items: center; gap: .8rem; flex: none;
  padding: 1rem 1rem 1rem 1.15rem;
  border-bottom: 1px solid var(--line);
}
.sd-hd b { font-size: 0.9333rem; font-weight: 800; letter-spacing: -.01em; }
.sd-scope { display: block; margin-top: .12rem; font-size: 0.7667rem; color: var(--dimmer); font-weight: 600; }
.sd-n { margin-left: auto; font-size: 0.7667rem; font-weight: 700; color: var(--dimmer);
        font-variant-numeric: tabular-nums; white-space: nowrap; }
.sd-x {
  flex: none; width: 2.6rem; height: 2.6rem; border-radius: 50%;
  display: grid; place-items: center; border: 0; cursor: pointer;
  background: var(--elev); color: var(--fg);
  transition: background .2s, transform .12s var(--ease);
}
.sd-x svg { width: 1rem; height: 1rem; }
.sd-x:hover { background: var(--elev2); }
.sd-x:active { transform: scale(.92); }
.sd-body { overflow-y: auto; overscroll-behavior: contain; padding: .35rem 0 .6rem; }

/* ------------------------------------------------------------- one row --
   Answering, in the order it gets asked in a car: how good is the picture,
   how big is it, what is the sound, and is it ready now. All of it comes from
   the addon's own parse rather than a regex over the filename - which this
   project already knows lies about its codecs. */
.strm {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center; gap: 0 .7rem;
  padding: .7rem 1rem .7rem 1.15rem;
  cursor: pointer; position: relative;
  transition: background .15s;
}
.strm + .strm { box-shadow: inset 0 1px 0 var(--line); }
.strm:hover { background: var(--elev); }
.strm:active { background: var(--elev2); }
.strm:focus-visible { outline: .12em solid #fff; outline-offset: -.12em; }

.s-top { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
/* The one thing worth reading first, so it is the only thing set in white. */
.s-res {
  font-size: 0.8333rem; font-weight: 800; letter-spacing: .02em;
  background: #fff; color: #000; border-radius: .35em; padding: .1rem .4rem;
}
.s-tag {
  font-size: 0.7rem; font-weight: 800; letter-spacing: .09em;
  text-transform: uppercase; color: var(--dim);
  border: .07em solid var(--line-hi); border-radius: .35em; padding: .12rem .38rem;
}
.s-tag.lit { color: #000; background: #fff; border-color: #fff; }
/* What this release will ask of the connection.
   Numbers line up, so tabular; and the warning is carried by weight rather
   than by a hue, because this app has exactly two and both mean something
   else. A heavy release is brighter and solid; a comfortable one is just
   another quiet tag. */
.s-tag.load { font-variant-numeric: tabular-nums; letter-spacing: .05em; }
.s-tag.load.heavy { color: var(--fg); border-color: var(--fg); }
.s-sub {
  margin-top: .28rem; font-size: 0.7667rem; font-weight: 600; color: var(--dimmer);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.s-size {
  font-size: 0.8667rem; font-weight: 800; font-variant-numeric: tabular-nums;
  white-space: nowrap; color: var(--fg);
}
.s-fn {
  width: 2.1rem; height: 2.1rem; flex: none; border: 0; border-radius: 50%;
  display: grid; place-items: center; cursor: pointer;
  background: transparent; color: var(--dimmer);
  transition: background .2s, color .2s, transform .2s var(--ease);
}
.s-fn svg { width: .95rem; height: .95rem; }
.s-fn:hover { background: var(--elev2); color: var(--fg); }
.strm.open .s-fn { transform: rotate(180deg); color: var(--fg); }
/* The filename is the last resort, not the headline - it is the longest and
   least reliable thing here, so it stays folded until asked for. */
.s-name {
  grid-column: 1 / -1; display: none;
  margin-top: .5rem; padding: .5rem .6rem;
  background: var(--bg); border-radius: var(--r-s);
  font-size: 0.7333rem; line-height: 1.45; color: var(--dim);
  word-break: break-all;
}
.strm.open .s-name { display: block; }

/* Not on the debrid yet: it will have to be fetched before it plays. */
.strm.cold .s-res { background: var(--elev2); color: var(--fg); }
.strm.have { background: rgba(255, 255, 255, .045); }
.strm.busy { opacity: .6; pointer-events: none; }
.strm.added .s-size { color: #fff; }

@media (max-width: 700px) {
  .strmdlg { max-height: 90vh; border-radius: var(--r-m) var(--r-m) 0 0; }
  .strm { padding: .7rem .8rem; }
  .s-sub { white-space: normal; }
}
@media (prefers-reduced-motion: reduce) {
  .strmwrap, .strmdlg { animation: none; }
  .strm, .sd-x, .s-fn { transition: none; }
}

/* Sections inside the dialog: what you hold, then what you could fetch. */
.sd-sec {
  padding: .9rem 1.15rem .45rem;
  font-size: 0.7rem; font-weight: 800; letter-spacing: .2em;
  text-transform: uppercase; color: var(--dimmer);
}
.sd-sec span { color: var(--dim); letter-spacing: .05em; }
.sd-more { padding: .9rem 1.15rem 1.1rem; }
.sd-more .btn { width: 100%; justify-content: center; min-height: 3rem; }

/* The wait line, in either shape it takes: an .empty sentence in the releases
   sheet, a .lrow-boxed one in the save sheet, because in the save sheet the
   smallest answer that can land is a single list row and a wait taller than a
   row would shrink the panel when it arrived.

   Scoped to .sd-body, and that is not tidiness. .lrow sets color: var(--fg) a
   thousand lines further down this file at equal specificity, so an unscoped
   .sd-waiting would lose on source order and the wait line would sit at full
   strength - the same trap the pill fix at .shead.sh-wait .viewsw exists for.
   Two classes deep beats it wherever it sits.

   .empty is this app's TERMINAL message class and this is not a terminal
   message; what separates them is the wording plus subpulse, and under reduced
   motion it is the wording alone. That is survivable because "Searching..."
   and "Reading your lists" cannot be read as answers - but it is why the words
   have to keep carrying it, and why neither may ever be shortened to a noun.

   No reduced-motion rule, and that is a real difference from .vd-wait rather
   than an oversight. subpulse is 0%,100% { opacity: 1 }, so the global block's
   animation-duration: .01ms and animation-iteration-count: 1 leave it resting
   at full strength on their own; .vd-track u needs its explicit switch-off
   because ITS from-state is scaleX(0), a wrong resting state rather than
   motion. If these two are ever de-duplicated with .vd-wait, do not drag
   .vd-wait's `animation: none` across - and do not delete this paragraph,
   because it is the reason there is nothing here to delete. */
.sd-body .sd-waiting {
  color: var(--dim); pointer-events: none;
  animation: subpulse 1.9s var(--ease) infinite;
}

/* --------------------------------------------- the selected version --
   What Play will actually use, said on the page rather than inside a dialog.
   The icons are the shelf's, the same ones the Jellyfin-side scripts draw, so
   a release reads identically in both places; the words beside them come from
   Jellyfin's probe of the file rather than from its name. */
.verSlot:empty { display: none; }
/* And its neighbour, which never had one. progressRow owns its slot and clears
   it before deciding anything, so it returns having painted nothing on every
   title with no resume point - and an empty div is still a flex item. It has
   been spending a full row gap on a bar that was never drawn, on public titles
   too, where that slot has never held anything at all.

   Not a pixel of artwork, though, once .actions exists: in the hero the
   wrapper is itself one flex item of .body and pays the same .6rem the bare
   slot used to, and on a phone the slot is in the sheet and not in the picture
   at all. What this removes is a dead gap inside the group. */
.progSlot:empty { display: none; }
.vstrip {
  display: flex; align-items: center; gap: .7rem; flex-wrap: wrap;
  margin-top: .7rem;
}
/* No chip behind them, and dimmed to sit at the same weight as the words
   beside them. These are the only images in the chrome and the only things in
   it that could carry a hue, so they were the loudest thing on a page that is
   otherwise strictly monochrome - which made a supporting detail read as the
   headline.

   The dimming is opacity, not a filter. Most of these logos are already white
   on transparent, so greyscale changes almost nothing and is kept only for
   any coloured badge in the shelf - which means the effect never depends on
   `filter` painting, and on the car it is documented that sometimes it does
   not. */
.vb { display: inline-flex; align-items: center; height: 1.5rem; }
.vb img {
  height: 1.5rem; width: auto; display: block;
  opacity: .58; filter: grayscale(1);
}
.vw {
  font-size: 0.7667rem; font-weight: 700; color: var(--dimmer);
  letter-spacing: .01em; margin-left: .15rem;
}
.hero .slide.on .verSlot { animation: heroIn .65s var(--ease) .44s both; }
@media (max-width: 640px) { .vb, .vb img { height: 1.3rem; } }
@media (prefers-reduced-motion: reduce) { .hero .slide.on .verSlot { animation: none; } }

/* ------------------------------------------------ icons inside buttons --
   There was no rule for these at all. ICON.play carried an inline
   width/height so it looked deliberate, and every other icon in a button -
   the tick on Mark watched, the heart on Favorite - was an <svg> with a
   viewBox and no dimensions, left to whatever the browser felt was intrinsic.
   Sized here, once, in em so they track the label rather than the root. */
.btn svg { width: 1.05em; height: 1.05em; flex: none; }
/* ICON.play used to carry its size inline, which meant it was the one icon
   CSS could not touch. Every place it appears has its own rule, so the inline
   style is gone and this owns it like the rest. */

/* ------------------------------------------- the detail hero grows now --
   The body is absolutely positioned and stacks upward from the bottom, which
   is right for the home carousel where every slide holds the same few lines.
   The detail page's body has since grown a show link, a cast strip and a
   version strip on top of title, meta, tagline, synopsis and two rows of
   buttons - and on a phone that is taller than the 26rem the hero collapses
   to, so the top of it, the logo, was being pushed up out of the frame and
   under the header.

   So on the detail page the hero is a floor rather than a fixed height, and
   the body sits in normal flow inside it. The artwork still fills whatever
   the result is. */
.detail .hero { height: auto; min-height: clamp(26rem, 56.25vw, 100vh); display: flex; }
.detail .hero .slide { position: relative; inset: auto; width: 100%; display: flex; }
.detail .hero .slide img.bg,
.detail .hero .slide .scrim { position: absolute; inset: 0; }
.detail .hero .body {
  position: relative; left: auto; right: auto; bottom: auto;
  width: 100%; align-self: flex-end;
  /* Clears the floating header, so the logo can never sit under it. */
  padding: calc(var(--topH) + 1.4rem) var(--pad) 2.4rem;
  max-width: none;
}
.detail .hero .body > * { max-width: 38rem; }
/* The two exemptions became one, because the two elements became one child.
   Miss this and the 38rem cap above squeezes the desktop and car row from full
   width to 570px and wraps six buttons onto two rows on the one screen that
   had room for one. .progSlot joins the exemption by being inside the wrapper
   and needs nothing: .detail .progrow caps the bar itself at 26rem. */
.detail .hero .body > .actions { max-width: none; }

/* ================= an episode is lit by its own stills =================

   WHY THIS HERO IS NOT THE OTHER ONE. A backdrop is composed to be looked at
   across a wall; an episode still is a frame grab. The old hero took a w500
   still, asked TMDB for w1280, stretched it over up to 1921px and cropped 22%
   off the top and bottom - and every stillless episode of a show wore the
   series backdrop, so the picture said nothing about the episode at all.

   So the picture stops being the hero and the COLOUR of the picture becomes
   it, which is the one thing a soft 780px frame still carries honestly. Three
   lamps, drifting on 23s / 19s / 31s so they never come back into phase,
   built from the hues pooled across every still the episode has - see
   epPalette. Same vocabulary as the player's wait card and the same equal-ink
   term, so a dim episode and a bright one get the same AMOUNT of light rather
   than the same amount of paint.

   SPECIFICITY, BECAUSE THIS IS THE TRAP. `.detail .hero` is (0,2,0) and so
   are the phone and landscape media blocks that size it - they win over each
   other on SOURCE ORDER, not weight. Anything here at (0,3,0) or above would
   outrank all three at once and silently kill the phone sizing, which this
   file records happening twice already. So NOTHING below writes height,
   min-height or display on .detail .hero.ephero: the field lives inside the
   box the existing rules produce, which it can, because .hero already carries
   position: relative and overflow: hidden.

   --c1/--c2/--c3/--lit are written together by the JS or not at all. --lit
   has no declared fallback anywhere, so a calc() reading it unset is invalid
   at computed-value time and drops the whole background-image - which is
   exactly the right answer for an episode whose stills have no colour: the
   black it would have been anyway. */
.detail .hero.ephero { align-items: flex-end; gap: var(--pad); }
/* The body stops being the only child, so it stops claiming the full width.
   flex rather than a width, so the frame's clamp decides and the words take
   what is left. */
.detail .hero.ephero .body { flex: 1 1 0%; width: auto; min-width: 0; z-index: 1; }

/* ---- the field ---- */
/* HOW LONG EACH LAMP TAKES TO GO OUT AND COME BACK.

   They were 23, 19 and 31 seconds, which put the fastest one at 57.6px over
   11.5s - about five pixels a second across a gradient whose edges are soft
   over hundreds of pixels. That is a drift you can only detect by looking away
   and looking back, and it read as a still picture.

   9, 7 and 13 is about two and a half times that: 12.8, 13.9 and 4.8 pixels a
   second at the car's size, which is movement you can see without watching for
   it. Still prime and still mutually prime, which is the point of the three
   numbers being odd ones - they come back into phase every 13.6 minutes rather
   than every cycle, so the light never repeats while you are reading.

   IT COSTS THE CAR NOTHING. A compositor animating translate3d ticks at the
   display's rate whatever the duration; a shorter cycle changes how far the
   layer moves per frame, not how many frames there are. Nothing repaints:
   these are three transformed layers and the excursion is the only thing that
   changed.

   AMPLITUDE IS THE OTHER KNOB and it is untouched here, deliberately. The
   excursion is 2.6% of a lamp's own box; opening that up makes the light
   travel further rather than faster, which is the quieter way to make the same
   point if this turns out to be too much. */
.detail .hero.ephero .epfield {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  --eplamp-1: 9s; --eplamp-2: 7s; --eplamp-3: 13s;
  /* How long one light takes to become another, and on what curve.

     A DISSOLVE IS NOT A MOVEMENT, and it was using the app's movement curve.
     var(--ease) is cubic-bezier(.2, .8, .2, 1), which is half done at about a
     sixth of its duration - right for something that has somewhere to arrive,
     wrong for two layers trading places, where it reads as a snap followed by
     a long tail. This is a symmetric ease, so the middle of the fade is the
     middle of the change and neither end draws attention to itself.

     And longer. Both sets use the same curve, so the incoming is f(t) and the
     outgoing is 1 - f(t) and the light does not dip or bloom in the middle
     whatever the duration - which means the only thing time costs here is
     patience, and a full-screen colour change deserves more than three
     quarters of a second. */
  --epfade: 1.15s;
  --epfade-ease: cubic-bezier(.4, 0, .6, 1);
}
/* A SET OF LAMPS, of which there are two and only one is ever showing.

   The light follows whatever is at the front of the pile, and changing it
   means changing three colours inside three full-screen gradients. A custom
   property does not interpolate, so transitioning --c1 would re-evaluate and
   repaint all three every frame - the largest surface on the page, on a
   machine that rasterises in software. Painting the new colours into the set
   that is not showing and cross-fading the two costs one opacity transition on
   two layers and repaints nothing.

   The set on its way out is PAUSED by the rule below, so three lamps are
   drifting at any moment rather than six. It resumes where it stopped when it
   is next brought in, which is right: they are meant to be out of phase. */
.detail .hero.ephero .epfield .epl {
  position: absolute; inset: 0;
  transition: opacity var(--epfade) var(--epfade-ease);
}
.detail .hero.ephero .epfield .epl.off { opacity: 0; }
.detail .hero.ephero .epfield i {
  position: absolute; inset: -8%; opacity: 0;
  /* The colours are read after the page is drawn, so the light rises in
     rather than appearing with it - the wait card's own .8s. */
  transition: opacity .8s var(--ease);
}
.detail .hero.ephero.lit .epfield i { opacity: 1; }
/* Three stops rather than two on every lamp. A straight ramp to transparent
   leaves a visible contour ring on a machine that rasterises in software; a
   mid stop at 43% carrying about .43 of the peak turns the falloff into an
   ease-out and spreads the same alpha over more pixels per step. */
.detail .hero.ephero .epfield .epl i:nth-child(1) {
  background-image: radial-gradient(58% 54% at 26% 56%,
    rgba(var(--c1), calc(.40 * var(--lit))) 0%,
    rgba(var(--c1), calc(.172 * var(--lit))) 43%, transparent 74%);
  animation: epLamp1 var(--eplamp-1) var(--ease) infinite;
}
.detail .hero.ephero .epfield .epl i:nth-child(2) {
  background-image: radial-gradient(52% 48% at 72% 30%,
    rgba(var(--c2), calc(.30 * var(--lit))) 0%,
    rgba(var(--c2), calc(.129 * var(--lit))) 43%, transparent 72%);
  animation: epLamp2 var(--eplamp-2) var(--ease) infinite;
}
.detail .hero.ephero .epfield .epl i:nth-child(3) {
  background-image: radial-gradient(46% 60% at 52% 88%,
    rgba(var(--c3), calc(.24 * var(--lit))) 0%,
    rgba(var(--c3), calc(.103 * var(--lit))) 43%, transparent 70%);
  animation: epLamp3 var(--eplamp-3) var(--ease) infinite;
}
/* AFTER the three rules above, and that is the whole reason it is down here.
   `.epl.off i` and `.epl i:nth-child(n)` are both (0,6,1) - a pseudo-class
   counts the same as a class - so nothing but source order separates them, and
   the lamp rules use the `animation` SHORTHAND, which resets play-state to
   running. Written above them, this rule was measured doing nothing at all. */
.detail .hero.ephero .epfield .epl.off i { animation-play-state: paused; }
/* Grain, and it earns its place: it is what stops three wide soft gradients
   banding into rings where they overlap. */
.detail .hero.ephero .epfield b {
  content: ''; position: absolute; inset: 0; opacity: .055;
  background-image: var(--grain);
}
/* And the light dies into the page's own black before the sheet starts, so
   there is no seam to see. */
.detail .hero.ephero .epfield u {
  position: absolute; inset: 0;
  background-image: linear-gradient(to bottom,
    rgba(0,0,0,.30) 0%, rgba(0,0,0,0) 26%, rgba(0,0,0,.55) 82%, var(--bg) 100%);
}
/* Transform only, which is the law for the car. `from` and `to` are the SAME
   frame and it is the REST pose, with the excursion in the middle: the two
   global reduced-motion blocks set animation-duration to .01ms AND
   iteration-count to 1, so a loop does not freeze where it stands - it runs
   once, instantly, and lands on its FINAL keyframe. With the rest pose at
   both ends that landing is where the design wants it and needs no rule. */
@keyframes epLamp1 { from, to { transform: translate3d(0, 0, 0); } 50% { transform: translate3d(2.6%, -1.8%, 0); } }
@keyframes epLamp2 { from, to { transform: translate3d(0, 0, 0); } 50% { transform: translate3d(-2.2%, 2.4%, 0); } }
@keyframes epLamp3 { from, to { transform: translate3d(0, 0, 0); } 50% { transform: translate3d(1.4%, 2.0%, 0); } }

/* ---- the entrance, restated ----
   The shared ladder is written `.hero .slide.on <child>` and this hero has no
   .slide, so without this the words simply appear all at once and nothing
   reports it. Same curve, same delays, and `backwards` rather than `both` for
   the reason recorded beside the original: `both` keeps asserting the final
   keyframe for ever, and an animation outranks a declared value. */
.detail .hero.ephero .showlink,
.detail .hero.ephero h1,
.detail .hero.ephero .meta,
.detail .hero.ephero .plot,
.detail .hero.ephero .castSlotHero,
.detail .hero.ephero .epstill,
.detail .hero.ephero .btns { animation: heroIn .65s var(--ease) backwards; }
.detail .hero.ephero .showlink,
.detail .hero.ephero h1      { animation-delay: .10s; }
.detail .hero.ephero .meta   { animation-delay: .18s; }
.detail .hero.ephero .plot   { animation-delay: .26s; }
/* The cluster arrives between the synopsis and the picture, which is where it
   sits. The picture and the buttons keep the rungs they had - the ladder gains
   a step rather than being re-spaced, so nothing that was already timed moves. */
.detail .hero.ephero .castSlotHero { animation-delay: .30s; }
.detail .hero.ephero .epstill { animation-delay: .32s; }
.detail .hero.ephero .btns   { animation-delay: .40s; }

/* ---- the title carries the page ----
   Light and large rather than heavy and cropped. .hero h1 is SHARED with the
   film page - it draws the title of every film with no wordmark - so this is
   scoped at (0,4,0) and 3041 is left exactly as it is. The shadow goes with
   the scrim that used to need it: nothing is behind these words now but a
   wash whose brightest point is a few percent of white. */
.detail .hero.ephero h1 {
  font-size: clamp(2.2rem, 5.6vw, 4.6rem); font-weight: 200;
  letter-spacing: -.035em; line-height: .96; text-shadow: none;
  cursor: default;
}
.detail .hero.ephero h1:active { transform: none; }

/* ---- the picture, which is a stack of them ----
   card-stack-hover, from transitions.dev (Pro). A pile of prints that fans
   open when you go to it and springs back together when you leave, the one
   under the cursor coming up on its own slower clock.
   Licensed from transitions.dev (Pro) on 2026-09-07.

   WHAT THIS REPLACES AND WHY. The first answer to "put the stills in the
   hero" was a ladder: three frames at 100%, 76% and 58%, stepping up the
   column, all squared to the right-hand gutter. Every number in it was right
   and it read as a staircase - three separate pictures at three sizes, which
   is a diagram of a set rather than a set. The pile says the same thing in
   one object and takes a third of the room to say it: 415.8 x 233.9 at rest
   against the ladder's 415.8 x 513.3, so the hero is quiet again until you
   reach for it.

   EVERY CARD IS THE SAME SIZE, which is what a pile is, and it also retires a
   whole rule: the ladder had to keep its smallest frame above the strip
   thumbnail's 207.9px or it turned into a chip, so it needed two breakpoints
   and three widths. A pile is the front picture's size at every depth, so
   there is nothing to check.

   THE FRONT CARD NEVER MOVES AND NEVER ROTATES. Its fan deltas are all zero.
   It is the picture the page is about - the same frame the episode list, the
   prev/next rail and the calendar draw - and the two behind fan out from
   under it. It also keeps the flight home exact: openShots sizes the dive
   from getBoundingClientRect, which on a rotated element returns the box
   AROUND the rotation, so a card at 4.5deg would land the picture about 4 per
   cent oversize. That is invisible at the end of a dive and it is still not
   worth spending on the one card you press most.

   POSES ARE INLINE, PER CARD, AS PERCENTAGES. --cx/--cy/--rot is where a card
   rests, --dx/--dy/--drot is what OPENING the pile adds - a press and hold, or
   a keyboard focus, never a hover. Percentages on translate resolve against the
   card's own box, so one set of poses holds at 415.8px on the car and at
   307.2px on a laptop with no second table.

   THE PILE HOLDS EVERY STILL, so the poses are computed in epStackPose rather
   than written here, and the fan is scaled to the room above the slot through
   --stack-spread at the moment it opens. Twelve cards at the full 54% step
   would stand 1390px tall on the car against about 720px of column. */
.detail .hero.ephero .epstill {
  /* BIGGER THAN THE PICTURE IT GREW OUT OF. 22rem was inherited from the
     single framed still, and its reason was headroom: `22rem from a w780 file
     is 1.13x at two device pixels per css pixel`. The car is not two device
     pixels per css pixel, it is 1.33 - so 415.8px was 553 device px out of a
     780px file, 1.41x of headroom spent on nothing. 27rem is 510 css px and
     679 device px, still inside the file, and 23 per cent more picture.
     The cards behind stay w500 and are therefore 1.36x upscaled at the moment
     the pile is fanned. They show about a third of themselves there and the
     way to see one properly is to tap it, which fetches the original - w780
     for all of them would be 930 KB on a fan against 300. */
  flex: none; width: clamp(13rem, 27vw, 27rem);
  margin: 0 var(--pad) 2.4rem 0; position: relative; z-index: 1;
  /* The pile is exactly one picture tall. The cards are absolute inside it,
     so this is what puts the front one where the single picture used to be -
     measured 415.80 x 233.88 at (1433.7, 801.3) on the car, unchanged.
     No display declaration on purpose: the landscape block takes the whole
     slot away with display:none and there is now nothing here for it to have
     to outrank. */
  aspect-ratio: 16 / 9;
  --stack-open-dur: 410ms;
  --stack-open-ease: cubic-bezier(0.31, 2.34, 0.64, 1);
  --stack-close-dur: 360ms;
  --stack-close-ease: cubic-bezier(0.34, 1.9, 0.64, 1);
  --stack-scale-dur: 610ms;
  --stack-scale-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --stack-scale: 1.04;
  /* How far the fan opens, and how hard it leans. The recipe ships 1.42 and 1.
     The spread is a FLOOR here rather than a setting: epStackPose lays the fan
     out at one full step per card and the slot's own inline --stack-spread
     scales it down to whatever room was measured above the pile when it opened,
     so this declaration is what a pile of two or three uses unscaled and what
     anything that cannot measure itself falls back to. */
  --stack-spread: 1;
  --stack-rotation: 1;
  /* HOW MUCH OF THE PILE'S OWN SHAPE THIS SCREEN CAN HOLD. The poses are one
     set of numbers, written for a 416px card in a column with 720px of nothing
     above it; the same numbers on a phone put the back of the stack 20px off
     the left edge of the screen and 47px up behind the back button - both
     measured at 375x812 before these existed. Damping is better than a second
     pose table: the shape is the same object, seen in less room. */
  --stack-x: 1;
  --stack-depth: 1;
}
.detail .hero.ephero .epframe {
  /* Absolute and full-bleed, so all three cards are the one box the slot
     measures and the poses are pure translate and rotate. */
  position: absolute; inset: 0; width: 100%; height: 100%;
  padding: 0; border: 0; background: var(--bg2);
  /* The depth, from a number paint() writes as --z rather than as z-index
     itself: an inline z-index would beat every rule in this file, including the
     hover below it, whatever the weight. */
  z-index: var(--z, 1);
  /* The app's small-picture radius, the one every card and thumbnail in it
     wears. .artbox already carries position: relative and overflow: hidden;
     this restates position because the pile needs absolute, and the clip to the
     radius still comes free. */
  border-radius: var(--r-s); cursor: pointer;
  /* pan-y, so a finger travelling DOWN still scrolls the page past the hero -
     the picture is the whole width of it on a phone - while the sideways half
     of the gesture comes to the swipe instead of to the browser. none would
     take both and strand a thumb on the one element it most wants to scroll
     past; manipulation, which this was, gives the sideways half away too.
     user-select and the callout go because a stack of photographs is not text
     and a press on one should not offer to save it. */
  touch-action: pan-y; user-select: none; -webkit-touch-callout: none;
  /* One shadow, the same on every card, because they are the same object at
     three depths and a pile whose sheets throw different shadows is not a
     pile. Shorter than the single picture's 3rem: three 3rem blurs is three
     blurred rectangles for a rasteriser with no GPU to hand them to, and the
     separation a pile needs is between the sheets rather than under the
     bottom one. Never in a transition and never changing. */
  box-shadow: 0 1rem 2.2rem -.7rem rgba(0, 0, 0, .62);
  translate: calc(var(--cx) * var(--stack-x)) calc(var(--cy) * var(--stack-depth));
  rotate: var(--rot);
  scale: 1;
  /* Base timing is the CLOSE, so the collapse is what happens whenever the
     hover rule stops applying - including when the pointer leaves the hero
     entirely. */
  transition:
    translate var(--stack-close-dur) var(--stack-close-ease),
    rotate var(--stack-close-dur) var(--stack-close-ease),
    scale var(--stack-scale-dur) var(--stack-scale-ease);
  /* No will-change, and that is a deliberate departure from the recipe. It
     promotes a layer for the life of the element, and three permanently
     promoted layers sitting over an .epfield that is already painting three
     animated radial gradients is not a trade this machine should make for a
     transition it can promote on demand. */
}
.detail .hero.ephero .epframe .eps-img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
}
/* THE FAN IS A CLASS, AND THE CLASS IS PUT ON BY A TAP.

   The recipe fans on hover of the container and guards that with
   (hover: hover), which was right for a card stack sitting in a well of its
   own. It is wrong here: this pile is in the middle of the one hero the app
   has, on the way to Play and Mark watched, and crossing it flapped the pile
   open every time whether you meant it or not. A press and hold answered that
   and asked a worse question - nothing on a page says an element is holdable.
   One tap opens the pile, a second picks a picture out of it, and a tap
   anywhere else puts it back. The recipe itself is untouched by any of this:
   the springs, the multipliers and the poses are exactly as shipped, and only
   the trigger has moved.

   :focus-visible is the keyboard's tap. It matches only for a keyboard focus
   and never for a click, so tabbing into the pile opens it and clicking a card
   does not - which would otherwise fan the pile behind the viewer that is
   opening over it. */
.detail .hero.ephero .epstill.canfan.fan .epframe,
.detail .hero.ephero .epstill.canfan:has(.epframe:focus-visible) .epframe {
  translate:
    calc((var(--cx) + var(--dx)) * var(--stack-x))
    calc((var(--cy) + var(--dy) * var(--stack-spread)) * var(--stack-depth));
  rotate: calc(var(--rot) + var(--drot) * var(--stack-rotation));
  transition:
    translate var(--stack-open-dur) var(--stack-open-ease),
    rotate var(--stack-open-dur) var(--stack-open-ease),
    scale var(--stack-scale-dur) var(--stack-scale-ease);
}
@media (hover: hover) {
  /* The card under the cursor comes up and comes forward, and only once the
     pile is open - on a closed pile a hover does nothing at all, which is the
     whole point. Open, it is what says which card the next tap will take.
     z-index is not animatable and snaps, which is right: it snaps as the scale
     starts, so a card reads as lifting out of the pile rather than as swapping
     places. */
  .detail .hero.ephero .epstill.canfan.fan .epframe:hover { scale: var(--stack-scale); z-index: 30; }
}
/* The press, on the same property the fan's hover uses so the two cannot
   compose into .985 x 1.04.

   .artbox IS THERE TO PAY FOR A CLASS, and the arithmetic is worth writing
   down because it was got wrong once already. The hover rule above is
   .detail .hero.ephero .epstill.fan .epframe:hover - six classes and a
   pseudo-class, (0,7,0), because .fan and :hover each count. Without the extra
   compound this rule is (0,6,0) and LOSES, so a card pressed inside an open fan
   stayed lifted at 1.04 and the press had no feedback at the one moment the fan
   exists for. At (0,7,0) it ties and wins on source order, which is why it must
   stay below. Not :hover:active instead: a keyboard activation fires :active
   with no hover at all. */
.detail .hero.ephero .epstill .epframe.artbox:active { scale: .985; }
/* The stills past the pile's depth are still CHILDREN - that is what keeps
   strip.children[i] pointing at still i - and display:none is the whole
   requirement. The hidden attribute already does this; the rule is here so a
   future .epstill > * display declaration cannot quietly undo it. */
.detail .hero.ephero .epstill > .ep-ghost { display: none; }

/* A phone has no side column. The frame goes FIRST - it is what says which
   episode - and the words follow it, which is the order the eye wants when
   there is only one of them. flex-direction only: the height still belongs to
   the media blocks that already own it. */
@media (max-width: 700px) {
  .detail .hero.ephero { flex-direction: column; align-items: stretch; gap: 1.1rem; }
  .detail .hero.ephero .body { align-self: stretch; padding-top: calc(var(--topH) + .6rem); }
  /* The picture goes FIRST and keeps its gutter. Inset rather than bled:
     full-bleed is tempting once the frame is gone, but a still run to the
     screen edges IS the old hero, which this redesign exists to reject. */
  /* THE PILE IS HERE TOO - it is the same box the single picture was, with
     edges - but it is damped, and the top margin now carries the depth.

     No lateral drift at all. The card is the full width of the screen here, so
     any sideways offset walks straight off it: measured at 375x812, the back of
     an eleven-card stack sat at x -20.1 with the drift in place. Straight up is
     the only direction there is room in.

     .45 of the depth, and the margin grown by exactly what that leaves, so the
     TOP of the pile lands where the single picture's top used to be rather
     than 47px higher, behind the back button. The front card moves down
     instead, which is the honest trade: the composition keeps its top edge.

     MARGIN, NOT PADDING, and the difference is load-bearing. The cards are
     position:absolute with inset:0, and an absolutely positioned child is laid
     out against its containing block's PADDING box - so padding here stopped
     moving the picture the moment the frame became a pile, and the still went
     back under the top bar. Measured: the card's top read 0 with padding. */
  .detail .hero.ephero .epstill {
    width: auto; order: -1;
    margin: calc(var(--topH) + 2.6rem) var(--pad) 0;
    --stack-x: 0;
    --stack-depth: .45;
  }
  /* Shorter throw. A 2.2rem blur under a translucent top bar on a 375px screen
     is a smudge rather than a shadow, and there is no field either side for it
     to fall on. There is exactly one card here - epRunMax() answers 1 below
     900px, where the hero is one column and a fan would open over the wordmark
     - so this is the only shadow on the phone. */
  .detail .hero.ephero .epframe { box-shadow: 0 .8rem 1.8rem -.7rem rgba(0, 0, 0, .6); }
}

/* Landscape on a phone: there is no room for a picture beside the words and
   the words are the point. The light stays. */
@media (max-height: 500px) and (orientation: landscape) {
  .detail .hero.ephero .epstill { display: none; }
}

/* ---- and the third exemption, which is the wordmark, and is not `none` ----

   The cap two lines up is home's `.hero .body { max-width: 38rem }` said
   again, once per child. It has to be: the detail body is position:relative
   and width:100% so the artwork can grow under a taller stack, and it says
   `max-width: none` on itself. Re-applying the number per child is what puts
   the h1, the tagline, the meta row, the plot and the cast strip on home's
   measure without a word said about any of them.

   The wordmark is the one child home does NOT cap at 38rem. Home caps it at
   `min(22rem, 62%)` from `.hero .logo`, which is (0,2,0) against the cap's
   (0,3,0) - so on this page the rule that puts every OTHER child at parity is
   precisely the rule that takes the logo out of it. logoSwap replaces the
   <h1> with an <img class="logo"> in place, so the mark inherits the h1's
   place as a DIRECT child of .body and the cap catches it there.

   Measured in Chrome at 1400x900, --ui 1.12 (root 16.8px), the same 500x104
   file on both screens: home box 369.59 x 76.88 with the mark filling it,
   detail box 638.39 x 125.98 with the mark painted 605.67 x 125.98. The caps
   are 22rem against 38rem, a flat 1.727x. Flat because the percentage is
   inert up here: --pad is clamp(.95rem, 3.2vw, 3.4rem), so the detail body is
   about 0.936 of the viewport and its 62% arm is 812px at 1400 and 758px at
   the car's 1307 - both far above 22rem, at every --ui.

   So home's expression goes back verbatim, and verbatim is the point. This is
   not "smaller", it is the same expression resolving the same way.

   The percentage is safe even though the box it resolves against is NOT the
   same box - detail's is up to 2.83x home's at 1920. 0.62 x 38rem = 23.56rem,
   which is ABOVE 22rem, so min() has already switched to the 22rem arm before
   home's body reaches its own cap and the two boxes can start to differ;
   below that width neither body is capped, both are viewport - 2 x --pad, and
   62% of each is the same number by construction. There is no width where the
   62% arm binds and the boxes differ. That is a coupling to `.hero .body`
   2800 lines up and nothing there says so: it holds only while that 38rem
   stays above 22 / 0.62 = 35.484rem. Today's margin is 2.516rem.

   Unconditional, and that is the fix for the car. This shipped first inside
   `@media (max-width: 700px)` below, written and measured on a 393px phone -
   227px asked for, 367px taken. The car is 1307x747: it matches neither that
   block nor the landscape one, so the screen this hero was actually designed
   for kept the 38rem the whole time. No car block in this file touches .logo
   and none is needed - ungating is the whole car fix.

   max-width ONLY, and that restraint is load-bearing. `.detail .hero .logo`
   already exists in the phone block far above carrying `max-height: 5.4rem`,
   it is (0,3,0), and it is EARLIER - so a max-height here, where it would
   look like it belonged, silently puts the phone's mark back to 7.5rem.
   Height needs nothing anyway: 7.5rem from `.hero .logo` on both screens at a
   desk and in the car, 5.4rem on both on a phone, already deliberate and
   already equal.

   align-self is left out, and that is a decision rather than an omission. The
   phone rule below carried `align-self: flex-start` on the claim that an
   <img> stretched to a flex column's cross size keeps its own intrinsic
   height and leaves empty box ABOVE the mark. That claim is wrong. The engine
   sizes the img from its CONSTRAINED cross size and the aspect ratio, never
   from the intrinsic height, so `object-position: left bottom` has nothing to
   push against and the vertical slack measured no more than 0.01px. What
   flex-start actually does is horizontal, and it BREAKS the parity this rule
   exists to make: a mark whose FILE is narrower than the cap is upscaled to
   the cap by stretch and painted at its own size by flex-start. Measured, the
   same four files in a 638.39px column against a 1300.4px one:

     180x40   home 369.56 x 82.13   flex-start 180 x 40      0.487x
     300x70   home 369.58 x 86.23   flex-start 300 x 70      0.812x
     500x104  home 369.59 x 76.88   flex-start 369.58 x 76.87
     500x340  home 185.27 x 125.98  flex-start 185.27 x 125.98

   w500 is what tmdb.py asks for and w500 never upscales, so a small clearlogo
   lands in that first band for real. Home is stretched on all four, because
   `.hero .logo` declares no align-self and nobody has asked it to. Parity is
   the same rule resolving the same way, so this rule holds exactly what
   home's holds and no more.

   The mark gets shorter, so the page moves - worth knowing before you go
   looking for it, because nothing about a max-width says so. At 1400x900,
   --ui 1.12, the 500x104 box goes 125.98 -> 76.88, 49.10px out of the body
   stack. `.detail .hero .body` is align-self: flex-end inside a hero whose
   min-height is a FLOOR, so below the floor the stack simply starts 49.10px
   lower and nothing under the hero moves; above it the hero itself shortens
   and the whole page rises by the same amount. Both directions carry this
   hero toward home's frame, which is the intent.

   `.hero.trailing .logo` (0,3,0) asks for min(14rem, 42%) and this line
   outranks it on source order. It costs nothing today: `trailing` is written
   in exactly two places in app.js, both inside heroEl(), which mounts only
   into the home carousel's .heroSlot - the detail heroes are static markup
   and cannot enter the state. The 38rem cap above was already outranking it
   in the same way, so this changes which number wins, not whether one does.
   If a trailer ever reaches the detail hero, restate the trailing size BELOW
   this line - do NOT write :not(.trailing) here, which makes this (0,4,0) and
   drops the trailing state back onto the 38rem cap, which is the original
   defect in the one state you were trying to protect.

   THIS MUST STAY BELOW `.detail .hero .body > *`. Both are (0,3,0), and here
   there is not even a media query to hide behind - source order alone settles
   it, and a copy of this sitting up beside the max-height rule where it looks
   like it belongs is overridden by the cap and does nothing at all. */
.detail .hero .logo { max-width: min(22rem, 62%); }

/* ---- and the same size as the home hero, which it was not ----

   The unconditional clamp above is the same number home uses, so the two
   agreed at a desk and in the car and nowhere else. Both of home's other hero
   sizes are scoped to .heroSlot - the phone block's
   `clamp(30rem, 64vh, 38rem)` and the landscape block's
   `clamp(14rem, 72vh, 22rem)` - and .heroSlot is the home carousel's own
   wrapper, so the detail page fell through both of them to the desktop figure.
   Measured on a 375x812 phone: home 520px against detail 394px, the same
   artwork, one tap apart. The detail one reads as a squatter crop of the same
   image rather than as the same frame.

   min-height rather than height, for the reason the block above exists: the
   detail body carries a show link, a cast strip, a version strip and two rows
   of buttons on top of what a home slide holds, and pinning it would push the
   logo up under the header. So this is a FLOOR that happens to equal home's
   ceiling - a title whose body fits inside it gets exactly home's frame, and
   one whose body does not still grows rather than clipping.

   Placed here, after `.detail .hero` rather than beside the .heroSlot rules
   it mirrors, and that is not a preference. Both selectors are (0,2,0), so
   equal specificity is settled by source order alone - a copy of this sitting
   up at line 2576 with the rest of the phone hero rules would be overridden
   by the unconditional min-height above it and do nothing at all.

   The two blocks are in the same order as their .heroSlot originals, so a
   small landscape phone that matches both resolves the same way on both
   screens. */
@media (max-width: 700px) {
  .detail .hero { min-height: clamp(30rem, 64vh, 38rem); }
  /* The wordmark rule that stood here is gone, both halves of it.

     The width half - min(22rem, 62%) - moved up to an unconditional
     `.detail .hero .logo` directly under `.detail .hero .body > *`, because it
     was never particular to a phone: the car matches neither this block nor
     the landscape one below and sat on 38rem the whole time. The WIDTH here
     is unchanged by that move: the same expression, resolved from one rule
     now instead of two.

     The align-self half was DROPPED rather than moved, and the note on that
     rule carries the measurements. Short version: it did not do what this
     comment used to say - the vertical slack it claimed to remove measured no
     more than 0.01px - and what it actually did was make this mark disagree
     with home's, by painting a sub-cap file at its own size where home
     upscales it to the cap. So the phone DOES change here, and only for a
     mark whose file is narrower than the cap: it now paints the size home
     paints it. A full-width w500 mark, the common case, is unchanged to
     within 0.01px.

     One thing was NOT measured, and it is the reason to look here first if a
     phone ever looks wrong: whether iOS WebKit sizes a stretched replaced
     item from the constrained cross size the way Chrome does. If it does not,
     it does not on home either - both screens now carry the same declarations
     for this element, so they agree on any engine by construction, which is
     the whole point of the change. */
}
@media (max-height: 500px) {
  .detail .hero { min-height: clamp(14rem, 72vh, 22rem); }
}

/* A claim rather than a measurement. Jellyfin only probes what it has read,
   and a .strm is a URL - so a copy it has never opened describes itself from
   its filename, and says so. Not pedantry: an unprobed source is the one
   Jellyfin serves as a live HLS playlist with a made-up duration, which is
   the one where seeking misbehaves. */
.s-tag.dim { color: var(--dimmer); border-color: var(--line); letter-spacing: .07em; }

/* -------------------------------------------- which copy is selected --
   It used to be a white SELECTED pill sitting in the same row as 4K, DV and
   REMUX - which made a state of the row look like one more fact about the
   release. It is a different kind of thing, so it looks like one: the mark
   every list of mutually exclusive choices uses, plus an accent down the edge
   of the row it belongs to.

   Two signals again, for the same reason as the READY mark: the bar is what
   you catch scanning the list, the dot is what confirms when you stop. */
.s-sel {
  position: relative; flex: none;
  width: .95rem; height: .95rem; border-radius: 50%;
  box-shadow: inset 0 0 0 .12em #fff;
}
.s-sel::after {
  content: ''; position: absolute; inset: .26em;
  border-radius: 50%; background: #fff;
}
.strm.sel { background: rgba(255, 255, 255, .06); }
.strm.sel::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0;
  width: .22rem; background: #fff;
}

/* ------------------------------------------------ a screen at arm's length --
 * The car reports 1307x747 CSS px, so the app lays itself out like a small
 * desktop and sizes itself like one: nav at 12px, buttons at 13.5px, card
 * titles at 12.5px, row headings at 12px. Those are right at 60cm on a
 * monitor and too small read from a driver's seat.
 *
 * Scaling the whole page was the obvious move and it does not work. Measured
 * with zoom: 1.28 on body, the hero went from exactly the viewport height to
 * 1671px against a 1305px screen, because vh inside a zoomed subtree resolves
 * against the unzoomed viewport and is then scaled again - and there are ten
 * height constraints in this file that would each need compensating.
 *
 * There is also less to fix than it looks. Everything sized in vw already
 * scales: a hero title works out around 51px on that screen. What never moves
 * is the fixed-px layer, and that is all this touches.
 *
 * min-height is not decoration. A phone in landscape is about 852x393, which
 * is exactly how you would hold it to watch something, and it passes a
 * coarse-pointer min-width test on its own. Height is what separates the car
 * from a phone while keeping tablets in. Placed last so it wins over the
 * min-width: 900px block that sets --rail-card.
 */
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  :root { --rail-card: 12rem; }

  /* chrome */
  .top .navtab { font-size: 1rem; letter-spacing: .07em; padding: .55rem .9rem; gap: .55rem; }
  .top .navtab .nt-i svg { width: 1.5rem; height: 1.5rem; }
  .top .icon { width: 2.9rem; height: 2.9rem; }
  .top .icon svg { width: 1.25em; height: 1.25em; }
  .iconbtn { width: 3.1rem; height: 3.1rem; }

  /* anything you press */
  .btn { font-size: 1.0667rem; padding: .75rem 1.6rem; gap: .55rem; }
  .btn svg { width: 1.15em; height: 1.15em; }
  .chipbtn { font-size: 1rem; padding: .6rem 1.25rem; }
  .morebtn { font-size: 1rem; padding: .75rem 1.9rem; }
  /* 2.4rem is 33px at this root size, which was under the floor the whole time
     and never mattered much, because the button did not exist until the
     section had landed. It exists from the first frame now and is the only
     thing on this section anyone can press for the next second and a half, so
     it gets the same 3.5rem the player's rows get. Free to change: picksMast()
     builds both the reservation and the landed section, so this moves them by
     the same amount and no reserved figure goes stale. */
  .pm-all { min-height: 3.5rem; }

  /* labels and rows */
  .rail h2, .sec { font-size: 0.9667rem; letter-spacing: .15em; }
  /* The search screen's Clear is sized off .sec, and .sec changes HERE - so
     the number it reads changes here too. Kept beside the line it depends on
     rather than down in the search section, because a heading that grew and a
     button that did not would part company silently.
     The value itself lives in the car block further down, AFTER the
     unconditional .sidle rule that declares --sc-f - it stood here, before
     that rule and at equal specificity, so it never applied. */
  .rail h2 .note, .sec .note, .mt-note { font-size: 0.8667rem; }
  .rail h2 .go { font-size: 0.8333rem; }
  .card .t { font-size: 1rem; }
  .card .s { font-size: 0.9rem; }
  .tag { font-size: 0.9rem; padding: .15rem .7rem; }

  /* ---- the day wall: History, and now the Calendar ----
     Neither screen had a line in any of the thirty-four coarse blocks, so the
     two strings a glance actually lands on were left at desk sizes: .hd-day is
     10.0px at a 15px root and .st-when 11.5px, against a block whose own note
     above calls 12.5px card titles too small from a driver's seat. The date
     rail and the right-hand slot are exactly the parts read WITHOUT stopping
     to read - "Thursday", "In 2 days", "Not here" - which is the argument for
     treating them first rather than last.

     One step up from the base each: 1rem to 1.0667 and 0.8rem to 0.8667,
     which is 16px and 13px at a 15px root and 17.9 and 14.6 at the car's
     usual --ui of 1.12.

     A step and not more, only because nobody has read it from the seat. The
     geometry would take more now: measured at 1307, .cwall lays four columns
     of 288px, and the title clamps to two lines inside that - the 152px month
     cell this paragraph used to argue against is gone with the month grid, and
     with it the nine-characters-of-title problem that capped the size here.

     NOT VERIFIED IN THE CAR, which is the whole reason it stops at one step.
     It is on the backlog: the question to answer from the seat is whether 16px
     reads, and if it does not, there is room to go up.

     THIS CHANGES THE HISTORY SCREEN TOO, and that is the point rather than a
     side effect - one vocabulary, one pair of rules. */
  .cc-t, .hs-hero .hs-t { font-size: 1.0667rem; }
  .cc-s, .hs-s { font-size: 0.8667rem; }
  .cchip { min-height: max(3.5rem, 52px); padding: 0 1.1rem; }

  /* the player, where a mis-tap costs the most */
  /* 3.5rem is the 52px touch target at this root size. The padding alone left
     a row at 51 - fine for six speeds, not fine for a chapter list, which is
     thirty-six consecutive targets where a miss seeks the film. min-height
     rather than more padding so the number is the rule, not a by-product of
     whatever line-height the font happens to report. */
  .player .mrow { font-size: 1.0667rem; padding: .7rem .9rem; min-height: 3.5rem; }
  .player .msep { font-size: 0.8rem; padding: .6rem .9rem .25rem; }
  .player .pctrls .time { font-size: 1.1333rem; }
  .player .skipintro { font-size: 1.0333rem; padding: .8rem 1.6rem; }
  .player .mrow .mm { font-size: 0.9333rem; }
  .player .pchap { font-size: 1rem; }
  .player .po-chap { font-size: 1.0667rem; }
}

/* -------------------------------------------------------------- settings --
   Cards in a flowing grid rather than one long column. The page was a narrow
   ribbon of pill rows down the middle of a landscape screen with nothing
   either side - the same shape the history page had, and the same fix. */
/* .sheet only carries page padding under .detail, so this one supplies its own
   rather than widening a rule three other screens depend on. */
.settings .sheet { padding: 0 var(--pad) 4rem; }

/* ---- the spine ----
   A page that shows one section at a time, with the whole shape of Settings
   standing beside it. Sixteen settings over five bands is more than a screen
   holds, and the wall of boxes it used to be could be scrolled but not read:
   nothing on it said how much there was or where a thing you could already
   name would be.

   Two columns only where two columns fit. 1200px is the same threshold the
   detail pages use for the same reason, and the car sits above it. Below, the
   spine becomes a strip across the top - the nav is still the nav, it just
   runs the other way. */
.settings .set-shell { display: block; }
.settings .set-spine { display: flex; flex-direction: column; gap: 1.1rem; margin-bottom: 1.4rem; }
@media (min-width: 1200px) {
  .settings .set-shell {
    display: grid; grid-template-columns: 17rem minmax(0, 1fr);
    gap: clamp(2rem, 3vw, 2.8rem); align-items: start;
  }
  /* Sticky, because the point of a spine is that it is still there when you
     have scrolled past the thing it is describing. */
  .settings .set-spine { position: sticky; top: 1.4rem; margin-bottom: 0; }
}

.settings .set-find { position: relative; display: flex; align-items: center; }
.settings .set-find svg {
  position: absolute; left: .8rem; width: 1.05rem; height: 1.05rem;
  color: var(--dim); pointer-events: none;
}
.settings .set-q {
  width: 100%; min-height: max(2.9rem, 46px); padding: 0 .9rem 0 2.5rem;
  background: var(--elev); border: .06em solid var(--line);
  border-radius: var(--r-s); color: var(--fg);
  font: 700 0.9333rem/1 inherit; font-family: inherit;
  -webkit-appearance: none; appearance: none;
}
.settings .set-q::placeholder { color: var(--dim); font-weight: 600; }
.settings .set-q:focus { outline: none; border-color: var(--line-hi); background: var(--elev2); }
/* Cyan is not decoration here: it is this palette's one word for "you narrowed
   the query", and a filtered Settings page is exactly that. */
.settings .set-narrow .set-q { border-color: var(--ask); }

.settings .set-nav { display: flex; flex-direction: column; gap: .12rem; }
.settings .set-navi {
  display: flex; align-items: center; gap: .6rem; width: 100%;
  min-height: max(2.8rem, 44px); padding: .5rem .75rem; text-align: left;
  background: none; border: 0; border-left: .13em solid transparent;
  border-radius: 0 var(--r-s) var(--r-s) 0;
  color: var(--dim); font: 700 0.9667rem/1.3 inherit; font-family: inherit;
  cursor: pointer;
}
.settings .set-navi:hover { background: var(--elev); color: var(--fg); }
.settings .set-navi.on { background: var(--elev); color: var(--fg); border-left-color: #fff; }
.settings .set-navn {
  margin-left: auto; font-size: 0.8rem; font-weight: 700; color: var(--dim);
  font-variant-numeric: tabular-nums;
}
.settings .set-navi.on .set-navn { color: var(--fg); }
.settings .set-narrow .set-navn { color: var(--ask); }
/* A section with nothing in it is not a result, so its count says nothing
   rather than nought. */
.settings .set-narrow .set-navn:empty { display: none; }

/* The build, under the sections. Quiet, but not below the floor: --dim is
   6.58:1 on this ground where --dimmer is 3.01 against the 4.5 this project
   holds everything else to, and a number nobody can read is worse than no
   number. Tabular figures because it is one, and selectable because the reason
   to look at it is usually to tell someone else what it says.

   Its own margin rather than the spine's gap: the spine spaces a search field
   from a nav, which are two controls, and this is neither - it wants to sit
   nearer the thing it belongs to. */
.settings .set-ver {
  margin: -.5rem 0 0; padding: 0 .75rem;
  font-size: 0.7333rem; font-weight: 650; letter-spacing: .04em;
  color: var(--dim); font-variant-numeric: tabular-nums;
}
/* Level with the chips rather than with a border that is not there. Below the
   threshold the nav is a scrolling row of pills with no left rule to line up
   against, and the .75rem that matched .set-navi's padding leaves it looking
   indented from everything else in the column. */
@media (max-width: 1199px) {
  .settings .set-ver { margin-top: -.6rem; padding: 0; }
}

/* Below the two-column threshold the spine lies down: the search field over a
   row of section chips, which is the same nav with the same counts. */
@media (max-width: 1199px) {
  .settings .set-nav {
    flex-direction: row; gap: .5rem; overflow-x: auto;
    scrollbar-width: none; padding-bottom: .2rem;
  }
  .settings .set-nav::-webkit-scrollbar { display: none; }
  .settings .set-navi {
    flex: none; width: auto; border-left: 0; border-radius: 99px;
    background: var(--elev); padding: .5rem .9rem;
  }
  .settings .set-navi.on { background: #fff; color: #000; }
  .settings .set-navi.on .set-navn { color: #000; }
}

/* The one thing the page says when a search finds nothing. */
.settings .set-shell.set-none .set-body::after {
  content: 'Nothing matches. Try a word from the setting itself — size, language, trailer, key.';
  /* --dim and not --dimmer: this is the only thing on the screen when it
     shows, and --dimmer measures 3.01:1 on black against this project's own
     4.5 floor. --dim is 6.58. */
  display: block; padding: 2.4rem 0; color: var(--dim); font-size: 0.9333rem;
}
/* A stack of BANDS, not a wall of cards. The wall moved down one level, into
   each band, when the five merged subjects stopped being five giant boxes -
   see the note beside the band pass in app.js for the measurements that forced
   it. This element now only spaces the sections. */
.settings .set-grid { display: block; }
.settings .set-sec { margin: 0 0 clamp(1.6rem, 2.6vw, 2.4rem); }
.settings .set-sec:last-child { margin-bottom: 0; }
/* The heading sits ON the band rather than inside a box, so it needs the air a
   card's padding used to give it. */
.settings .set-sec > .set-ct { margin: 0 0 .2rem; }
.settings .set-sec > .set-cn { margin: 0 0 .8rem; }
/* No align-items: start, and that is the point rather than an omission. The
   cards in a row stretch to the tallest of them, so a row is a row of equal
   boxes and the band ends on a straight line. Packing them instead would close
   the gaps BETWEEN the boxes and leave the band's bottom edge ragged, which is
   the shape this was asked to stop drawing. */
.settings .set-cards {
  display: grid;
  /* One column, and the boxes inside it are no longer boxes. Sixteen equal
     cards said every setting was as important as every other and buried the
     sentence under each one that says what it does; a row lets that sentence
     be read. minmax(0, 1fr) rather than 1fr for the reason it always is - an
     fr track's automatic minimum is its content, and a long option row would
     push this one wider than the page. */
  grid-template-columns: minmax(0, 1fr);
}
/* Packed, exactly as .histbody is - see the long note beside that rule for
   why two-pixel rows and a per-card span remove the hole a grid row leaves
   under its shorter cards, and why minmax(2px, auto) rather than a flat 2px is
   what makes an unspanned child impossible to overlap.
   The class is written by packGrid after it has measured, so the "Loading"
   frame and any future non-card child lay out exactly as they do today. */
/* And the last row is spread to fill, by ixFill - the same treatment the index
   bands get, for the same reason: five cards over three columns leaves two in
   the last row, and two cards half a row wide with a hole beside them is the
   gap this change exists to remove. */
.settings .set-card {
  /* A grid item's default minimum is its own min-content, so a card that could
     not shrink pushed its 1fr track wider than the phone: 443px of card in a
     375px screen at the largest interface size, and the whole page scrolled
     sideways. Only at 1.12 and 1.26 - at 1.0 the content happened to fit,
     which is why it was easy to miss and why the size control is the thing to
     test this with.
     min-width: 0 lets the card be as narrow as its column and the text inside
     wrap, which is what it should have been doing. Measured: track 443 -> 339
     at 375px and --ui 1.26, nothing overflowing. */
  min-width: 0;
  background: none; border: 0; border-radius: 0;
  border-bottom: .06em solid var(--line);
  padding: 1.05rem 0;
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: 0 clamp(1.2rem, 2.4vw, 2rem); align-items: start;
}
.settings .set-cards > .set-card:last-child { border-bottom: 0; }
/* display: contents so the group's three children become items of the row
   above rather than a block inside one cell. Nothing else can do this without
   rebuilding the group, and rebuilding the group means rebuilding the writes
   attached to its pills. */
.settings .set-card > .set-group { display: contents; }
/* Everything flows down the first column and the switch is the one exception.
   Placing .set-k and .set-h at named rows instead was wrong: a key card holds
   TWO .set-h elements - the description and the save message - and the second
   landed on top of the first. Nothing here may assume how many children a
   group has. */
.settings .set-card > .set-group > * { grid-column: 1; min-width: 0; }
/* Wide controls sit under the sentence they belong to; a two-state switch sits
   opposite its name. That is the whole rule, and it is why the page reads as
   prose with answers rather than as a form. */
.settings .set-card > .set-group > .set-opts.sh-seg {
  grid-column: 2; grid-row: 1; justify-self: end; align-self: center; margin-top: 0;
}
/* No switch, no second column - otherwise every other row pays its gap. */
.settings .set-card:not(:has(.sh-seg)) { grid-template-columns: minmax(0, 1fr); }
/* The section a row came from, shown only while a search is filtering. */
.settings .set-from {
  margin-left: .5em; font-size: 0.7333rem; font-weight: 700; color: var(--ask);
  letter-spacing: .04em;
}
.settings .set-ct {
  margin: 0; font-size: 0.8rem; font-weight: 800; letter-spacing: .18em;
  text-transform: uppercase; color: var(--dim);
}
/* The trailing store clause. Same values as .rail h2 .note - this IS that
   thing - but written here rather than added to that selector list, and the
   reason is the coarse block. .rail h2 gets bumped to 0.9667rem one line above
   its note's own bump, so that pair scales together; .set-ct had no line in
   the coarse block at all, so sharing the note rule would have given a
   lowercase weight-600 clause at 0.8667rem beside an uppercase weight-800
   heading frozen at 0.8rem - the clause bigger than the heading, on the car
   only, invisible from a desk. Both halves are now in the coarse block
   together, four lines apart, where neither can be changed without the other. */
.settings .set-ct .note {
  font-size: 0.7333rem; color: var(--dimmer); font-weight: 600;
  letter-spacing: .04em; text-transform: none;
}
/* The same aside, on a GROUP heading rather than a card one. When the nine
   cards became five, the store clause moved from two card titles down onto the
   groups it is actually true of - and landed with no rule of its own, so it
   drew at the label's weight and colour and butted straight up against it:
   "Preferredon the media server". Same recipe as the card note above, which is
   what it was before it moved. */
.settings .set-k .note {
  margin-left: .5em; font-size: 0.7333rem; color: var(--dimmer); font-weight: 600;
  letter-spacing: .04em; text-transform: none;
}
.settings .set-cn { margin: .5rem 0 0; font-size: 0.8333rem; color: var(--dimmer); line-height: 1.5; }
/* The first group in a card needs no rule above it - the card edge is the rule. */
/* The group is display: contents now - it has no box to pad or to rule. The
   row's own border-bottom is the separator. */
/* And the last one needs no padding below it - the card's own does that. The
   top was corrected when the cards were built and the bottom was not, so every
   card carried 1.1rem above its title and 1.2 + 1.3rem under its last pill row
   and sat visibly backwards. Restored inside the phone block below, where the
   card has no padding of its own.
   :last-child rather than :last-of-type: the type selector is what broke the
   rule above, and every card's last child is a .set-group - the key cards'
   message line lives inside the group, not beside it. */


/* A state word beside the name, so the card answers "is this working" before
   anything is read. */
.settings .set-pill {
  margin-left: .5rem; padding: .1rem .5rem; border-radius: 99px;
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
  vertical-align: .1em;
}
.settings .set-pill.ok { background: rgba(255,255,255,.14); color: #fff; }
.settings .set-pill.warn { background: transparent; color: var(--dimmer); box-shadow: inset 0 0 0 .08em var(--line-hi); }

/* ---- the Flick profile ----
   The one card on this page with nothing to set. Flick is read-only from this
   app, so what it can usefully be is the only place that shows what that
   record is actually holding - and the rule for what earns a line here is that
   no other screen can say it. */
/* ---- the masthead ----
   Outside .set-grid on purpose: a card among cards is one setting group among
   five, and this is the answer to who you are and what you have watched. It
   gets the page's gutter and a rule under it, and nothing else - no ground, no
   border, because a slab here would read as the biggest setting rather than as
   the top of the page. */
.settings .me { padding: 0 var(--pad) 1.1rem; margin-bottom: .3rem; }
.settings .me-in {
  display: flex; flex-direction: column; gap: .9rem;
  padding-bottom: 1.1rem; border-bottom: 1px solid var(--line);
}
.settings .me-bio { margin: -.4rem 0 0; font-size: .85rem; color: var(--dim); }
/* Side by side where there is room, because they are two readings of one
   history and comparing them is the point; stacked below that, because a
   ten-bar chart in half of 375px is a comb. */
.settings .me-charts { display: grid; gap: 1rem; }
@media (min-width: 46rem) {
  .settings .me-charts { grid-template-columns: 1fr 1fr; gap: 1.6rem; }
}
/* Bigger here than in a card. This is the top of the page and the figures are
   its subject, not a footnote to a setting. */
.settings .me .fk-figs { gap: 1.6rem; flex-wrap: wrap; }
.settings .me .fk-figs b { font-size: 1.6rem; }
.settings .me .fk-name { font-size: 1.3rem; }
.settings .me .fk-av { width: max(3.4rem, 50px); height: max(3.4rem, 50px); }

/* The picture leads, and it is the only round thing on a page of rectangles -
   which is the whole reason it reads as a person rather than as another
   figure. Fixed in both axes with object-fit, because a square crop of an
   already-square avatar is free and a portrait one would otherwise stretch. */
.fk-id { display: flex; align-items: center; gap: .7rem; }
.fk-av {
  flex: none; width: max(3rem, 44px); height: max(3rem, 44px);
  border-radius: 50%; overflow: hidden; background: var(--elev2);
  box-shadow: inset 0 0 0 .06em var(--line-hi);
}
.fk-av img { width: 100%; height: 100%; object-fit: cover; display: block; }
.fk-who { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.fk-name { font-size: 1.05rem; font-weight: 800; letter-spacing: -.01em; }
.fk-user {
  font-size: .8rem; font-weight: 600; color: var(--dimmer);
  display: flex; align-items: center; gap: .1rem;
}

/* Three figures, and the numbers lead. Same shape as the figures band the
   watchlist and history already use, at a card's scale. */
.fk-figs { display: flex; gap: 1.4rem; }
.fk-figs b {
  display: block; font-size: 1.35rem; font-weight: 800; letter-spacing: -.03em;
  line-height: 1.1; font-variant-numeric: tabular-nums;
}
.fk-figs span {
  font-size: 0.7rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--dimmer);
}
.fk-sub { font-size: .8rem; color: var(--dim); margin-top: -.5rem; }

.fk-ch { display: flex; flex-direction: column; gap: .4rem; }
.fk-ct {
  display: flex; align-items: baseline; gap: .5rem;
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .16em;
  text-transform: uppercase; color: var(--dim);
}
.fk-ct .note { letter-spacing: .01em; text-transform: none; font-weight: 600; }

/* A column per year, and per rating. No library and no SVG: a div with a
   height in per cent is exactly as expressive here and composites on a screen
   that paints neither filter nor shadow.
   The bars are one colour. The accent system is closed - white is "you are
   here", cyan a narrowed query, amber hidden results - so a ramp would have to
   invent a fourth meaning to say what length already says. What white IS spent
   on is the newest year, because that is "you are here" exactly. */
.fk-bars {
  display: flex; align-items: flex-end; gap: .18rem;
  height: max(3.4rem, 48px);
}
.fk-bar { flex: 1 1 0; display: flex; align-items: flex-end; height: 100%; min-width: 0; }
.fk-bar i {
  display: block; width: 100%; border-radius: .12rem;
  background: rgba(255,255,255,.26);
}
.fk-bar.now i { background: #fff; }
.fk-ax {
  display: flex; justify-content: space-between;
  font-size: 0.6667rem; font-weight: 700; color: var(--dimmer);
  font-variant-numeric: tabular-nums;
}

.fk-counts {
  display: flex; flex-wrap: wrap; gap: .3rem 1rem;
  font-size: 0.7667rem; color: var(--dimmer);
}
.fk-counts b { color: var(--fg); font-weight: 800; font-variant-numeric: tabular-nums; }
.fk-src { margin: 0; }

/* The tens, as a way into the title rather than a decoration. It is the only
   thing on this card that does anything, and what it does is leave for the
   film - nothing here writes to Flick. */
.fk-top { display: flex; flex-wrap: wrap; gap: .35rem; }
.fk-t {
  padding: .3rem .6rem; border-radius: 99px; font-size: 0.7667rem; font-weight: 700;
  background: rgba(255,255,255,.08); color: var(--fg);
  box-shadow: inset 0 0 0 .06em var(--line);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fk-t:hover { background: rgba(255,255,255,.16); }
.fk-t:active { transform: scale(.96); }
.fk-t[disabled] { opacity: .5; }
/* The floor, the way .cardx pays it: the chip keeps the size it draws and the
   hit area grows past it, because a row of finger-height pills would be a wall
   rather than a footnote. */
@media (pointer: coarse) {
  .fk-t { position: relative; }
  .fk-t::after { content: ''; position: absolute; inset: -.6rem -.2rem; }
  .fk-bars { height: max(4rem, 56px); }
}

/* ---- the subtitle preview ----
   Size and style were three words each with no way to know what they meant
   without starting something and waiting for a line of dialogue. This draws
   the same text the player draws, from the same rules - ::cue cannot be
   borrowed outside a video, so the three styles are restated here and must be
   kept in step with the .player video::cue block. */
/* margin, not margin-top: a <figure> carries a UA margin on all four sides and
   nothing in this file resets it, so the box would indent 40px either side. */
.settings .set-prev { margin: .9rem 0 0; }
.settings .set-prevbox {
  display: grid; place-items: center; min-height: 6.5rem;
  padding: 1rem .9rem; border-radius: var(--r-s);
  background:
    linear-gradient(0deg, rgba(0,0,0,.55), rgba(0,0,0,.15)),
    repeating-linear-gradient(115deg, #23252b 0 14px, #1c1e23 14px 28px);
  box-shadow: inset 0 0 0 .06em var(--line);
  overflow: hidden;
}
.settings .set-cue {
  font-family: Inter, system-ui, sans-serif; line-height: 1.35; color: #fff;
  text-align: center; font-size: var(--pv-size, 1.33rem);
  text-shadow: 0 .08em .28em rgba(0,0,0,.95), 0 0 .06em rgba(0,0,0,.9);
}
.settings .set-prev[data-cue="box"] .set-cue {
  background: rgba(0,0,0,.72); text-shadow: none; padding: .05em .3em;
}
.settings .set-prev[data-cue="outline"] .set-cue {
  text-shadow:
    -.045em -.045em 0 #000,  .045em -.045em 0 #000,
    -.045em  .045em 0 #000,  .045em  .045em 0 #000,
     0 .06em .18em rgba(0,0,0,.8);
}
.settings .set-prevl {
  margin-top: .4rem; font-size: 0.6667rem; font-weight: 800; letter-spacing: .16em;
  text-transform: uppercase; color: var(--dimmer);
}

/* An orphan comment stood here describing a vw mechanism the rule above it
   stopped using: --pv-size is a rem handed over from SUB_SIZES_PUBLIC. The
   argument it made is already made properly beside that array. */

@media (max-width: 700px) {
  /* One column already, and no box to strip. What a phone needs instead is
     for the switch to stop trying to share a line with the sentence. */
  .settings .set-card { grid-template-columns: minmax(0, 1fr); }
  .settings .set-card > .set-group > .set-opts.sh-seg {
    grid-column: 1; grid-row: auto; justify-self: start; align-self: start; margin-top: .7rem;
  }
  .settings .set-cue { font-size: var(--pv-size, 1.33rem); }
  /* The card has given up its padding here, so without this a card is
     separated from the next by the grid's row-gap alone - clamp(1.2rem, 2vw,
     1.8rem) - while two groups INSIDE a card are separated by 2.2rem of their
     own padding. Cards would come apart less than their own contents do.
     Same specificity as the rule it undoes and later in source. */

}
/* .set-note and .set-sec stood here with zero occurrences anywhere in web/*.js
   - leftovers of the single-column page, and .set-sec was a third copy of the
   .rail h2 / .sec recipe in a file that has twice consolidated it. Their car
   overrides went with them. Nothing is going to want a band heading back
   either: a heading dropped into .set-grid would be spanned as a card, and
   splitting the grid to avoid that would split the packer's one pool. */

.settings .set-k { font-size: 0.9667rem; font-weight: 700; margin-bottom: .15rem; }
.settings .set-h { font-size: 0.8333rem; color: var(--dimmer); max-width: 44rem; line-height: 1.45; }
.settings .set-opts { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .7rem; }
.settings .set-opt {
  background: var(--elev); border: .06em solid var(--line-hi); border-radius: 99px;
  padding: .5rem 1.05rem; font-size: 0.8667rem; font-weight: 700; color: var(--dim);
  transition: background .2s, color .2s, border-color .2s, transform .15s var(--ease);
}
.settings .set-opt:hover { background: var(--elev2); color: var(--fg); }
.settings .set-opt:active { transform: scale(.96); }
/* The chosen one inverts rather than tints: a tint is a shade of grey away
   from the others and this palette has no colour to spend on it. */
.settings .set-opt.on { background: #fff; color: #000; border-color: #fff; }

/* ---------------------------------------------------- options that show ----
   Four shapes for a row of choices, for the settings whose answer can be
   drawn. Each is a layout over the SAME .set-opt button - same class, same
   states, same .on inversion, same 3.5rem car floor from the block below -
   so the white-means-chosen rule holds across all five shapes and nothing
   here is in the path of a save.

   Specificity, not order, is what keeps these above the car block: every
   selector here carries three classes against that block's two, so a rule
   there cannot flatten a tile back into a lozenge whatever the source order
   ends up being after an edit. */

/* A sample over its name. The sample is the setting at its real value - the
   four interface sizes in proportion, the three subtitle heights from the same
   figures the preview strip uses, the same box office in both currencies. */
.settings .set-opts.sh-scale { gap: .55rem; align-items: stretch; }
.settings .sh-scale .set-opt {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .4rem; border-radius: var(--r-s); padding: .7rem 1rem; min-width: 5.75rem;
}
/* A fixed strip whatever the sample's size, so the four tiles line their names
   up with each other and only the specimen moves. */
.settings .sh-scale .sh-a, .settings .sh-scale .sh-f {
  display: grid; place-items: center; width: 100%; min-height: 2.5rem;
  font-style: normal; font-weight: 800; line-height: 1;
}
/* flex and not the grid above it: the formatter hands back a currency glyph in
   its own span followed by a bare text node, which a single-column grid lays
   out as two ROWS - the riyal mark parked above its own figure. */
.settings .sh-scale .sh-f {
  display: flex; align-items: center; justify-content: center; gap: .12em;
  font-size: 1.05rem; white-space: nowrap;
}
.settings .sh-scale em {
  font-style: normal; font-size: 0.75rem; font-weight: 700;
}

/* The subtitle styles, in the styles. The sample keeps its dark striped strip
   even in the chosen tile: a drop shadow judged against white is not judged,
   and the strip is a stand-in for a picture rather than part of the control.
   These three rules are the preview's three rules restated for a one-word
   sample - .settings .set-prev[data-cue=...] and .player video::cue are the
   other two copies, and all three move together or none do. */
.settings .set-opts.sh-cue { gap: .55rem; align-items: stretch; }
.settings .sh-cue .set-opt {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .4rem; border-radius: var(--r-s); padding: .7rem .8rem; min-width: 6.5rem;
}
/* A BRIGHT strip, and deliberately not the preview's dark one. On a dark scene
   all three styles are white text on black and the samples came out as three
   copies of the same tile; the hint beside them says a box is easier on a
   bright scene, and this is that scene, which is the one place the three are
   actually told apart. The big preview above stays dark because it answers a
   different question - not "which of these three" but "what will this look
   like" - and most of what this app plays is dark. */
.settings .sh-cue .sh-c {
  display: grid; place-items: center; width: 100%; min-height: 2.5rem;
  border-radius: .3rem; font-style: normal; overflow: hidden;
  background:
    linear-gradient(160deg, rgba(255,255,255,.18), rgba(255,255,255,0)),
    repeating-linear-gradient(115deg, #7d838d 0 10px, #6a707a 10px 20px);
}
.settings .sh-cue .sh-c b {
  font-family: Inter, system-ui, sans-serif; font-size: 1.1rem; font-weight: 700;
  color: #fff; line-height: 1.2;
  text-shadow: 0 .08em .28em rgba(0,0,0,.95), 0 0 .06em rgba(0,0,0,.9);
}
.settings .sh-cue .sh-c[data-c="box"] b {
  background: rgba(0,0,0,.72); text-shadow: none; padding: .05em .3em;
}
.settings .sh-cue .sh-c[data-c="outline"] b {
  text-shadow:
    -.045em -.045em 0 #000,  .045em -.045em 0 #000,
    -.045em  .045em 0 #000,  .045em  .045em 0 #000,
     0 .06em .18em rgba(0,0,0,.8);
}
.settings .sh-cue em {
  font-style: normal; font-size: 0.75rem; font-weight: 700;
}

/* Two states as ONE control. Two separate lozenges say "here are two things
   you could have"; a track with the lit half inside it says "this is on", and
   a two-state setting has always been the second sentence. The track carries
   the border as an inset shadow so it does not add to the button's own box,
   and the halves shrink and wrap rather than overflowing a narrow column. */
.settings .set-opts.sh-seg {
  display: flex; flex-wrap: nowrap; gap: 0; width: max-content; max-width: 100%;
  padding: .2rem; border-radius: 99px; background: var(--elev);
  box-shadow: inset 0 0 0 .06em var(--line-hi);
}
.settings .sh-seg .set-opt {
  flex: 0 1 auto; min-width: 0; white-space: normal;
  background: none; border-color: transparent;
}
/* :not(.on), and that is the whole reason this rule needs writing out rather
   than being left to the base :hover. `background: none` on the shape above is
   (0,3,0) and sits later in the file than .settings .set-opt:hover, so it was
   winning the tie and the fill never painted - while `color` came through,
   leaving half a hover state. A plain .sh-seg .set-opt:hover would fix that and
   break the other end, because (0,3,1) beats the .on rule below and would tint
   the lit half grey. */
.settings .sh-seg .set-opt:not(.on):hover { background: var(--elev2); }
.settings .sh-seg .set-opt.on { background: #fff; color: #000; border-color: #fff; }

/* Six rungs are an ORDER, and a wrapped row of lozenges is the one shape that
   cannot say so. One row each, tallest ask at the top, and a meter that fills
   further the higher you go - so where you are on the ladder is the first
   thing seen rather than the result of reading six labels for the white one.
   currentColor, so the meter inverts with the row it is in. */
.settings .set-opts.sh-ladder { flex-direction: column; align-items: stretch; gap: .35rem; }
.settings .sh-ladder .set-opt {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  border-radius: var(--r-s); padding: .6rem .95rem; text-align: left;
}
.settings .sh-ladder em { font-style: normal; }
.settings .sh-ladder .sh-r {
  display: flex; align-items: flex-end; gap: .16rem; height: 1.15rem; flex: 0 0 auto;
}
.settings .sh-ladder .sh-r b {
  width: .2rem; height: 100%; border-radius: 99px;
  background: currentColor; opacity: .22;
}
.settings .sh-ladder .sh-r b.f { opacity: 1; }
.settings .sh-ladder .sh-r b:nth-child(1) { height: 30%; }
.settings .sh-ladder .sh-r b:nth-child(2) { height: 44%; }
.settings .sh-ladder .sh-r b:nth-child(3) { height: 58%; }
.settings .sh-ladder .sh-r b:nth-child(4) { height: 72%; }
.settings .sh-ladder .sh-r b:nth-child(5) { height: 86%; }
.settings .set-keyrow { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .8rem; }
.settings .set-key {
  flex: 1 1 18rem; min-width: 0;
  background: var(--elev); border: .06em solid var(--line-hi); border-radius: 99px;
  padding: .6rem 1.1rem; color: var(--fg); font: inherit; font-size: 0.9rem;
}
.settings .set-key::placeholder { color: var(--dimmer); }
.settings .set-key:focus { outline: none; border-color: #fff; }
.settings .set-keymsg { margin-top: .6rem; color: var(--fg); }
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  /* The floor this file states seventeen times and had never spent here.
     3.5rem is the ~52px this app uses for anything reached without looking,
     written as min-height so the number is the rule rather than a by-product
     of the line height - the same declaration .shead .kfilt, .shead .vtab,
     .player .mrow, .pm-all and .top .searchpill all carry. This block raised
     the type and the padding and never the box, which left every one of the
     forty-seven choices on the page at 41px, under the floor, on the page
     whose own Size hint says "read from a car seat, not a desk".
     inline-flex because a min-height on a button leaves the label at the top
     of the taller box, and justify-content because a flex container stops
     honouring the UA's text-align: center. Blockified to `flex` as a flex item
     of .set-opts either way; the declaration is what the header's Retry pill
     needs, which is a flex item of .browsehead. */
  .settings .set-opt {
    font-size: 1rem; padding: .65rem 1.3rem;
    min-height: 3.5rem; display: inline-flex;
    align-items: center; justify-content: center;
  }
  /* .btn is already inline-flex and centred; it only ever needed the height.
     This also reaches #/diag, which shares the .settings wrapper - it has no
     .btn today, and if it grows one it inherits a floor it did not ask for. */
  .settings .btn { min-height: 3.5rem; }
  /* The spine is a control too, and it was the only part of this page that
     did not get the floor: measured on the car at 1307x747, the section
     buttons came out 47px and the search field 49, both under the 52 every
     other target here clears. The nav is now the FIRST thing a hand reaches
     on this page, so it cannot be the one thing sized for a mouse. */
  .settings .set-navi { min-height: 3.5rem; font-size: 1rem; }
  .settings .set-q { min-height: 3.5rem; font-size: 1rem; }
  .settings .set-navn { font-size: 0.8667rem; }
  /* The one settings selector this block never covered, and the card title is
     the reason the store clause beside it can be sized in proportion at all:
     .rail h2 and .sec get exactly this treatment at the top of this section,
     and .set-ct is the same recipe wearing a different name. Twelve pixels of
     uppercase eyebrow on a dashboard was already too small before anything was
     hung off it. */
  .settings .set-ct { font-size: 0.9667rem; letter-spacing: .15em; }
  .settings .set-ct .note { font-size: 0.8667rem; }
  /* The three text rules this block was missing.

     Every other piece of text on this page has a companion here - .set-opt,
     .set-ct, its .note, .set-k, .set-h, .set-key - and these three were added
     later, outside it. The two `em` rules are where an option's NAME now
     lives: turning the lozenges into tiles moved the label out of the button's
     own text and into a child, so the button's 1rem stopped reaching it and
     "Compact", "Extra large", "Drop shadow" and "Saudi riyals" drew at 0.75rem
     - 11.25px, or 10.35px at the Compact interface size - beside neighbours at
     15px, on the page whose own hint says "read from a car seat, not a desk".
     Quality escaped it only because .sh-ladder em sets no size at all. */
  .settings .sh-scale em, .settings .sh-cue em { font-size: 0.9333rem; }
  .settings .set-k .note { font-size: 0.8667rem; }
  .settings .set-k { font-size: 1.1rem; }
  .settings .set-h { font-size: 0.9333rem; }
  .settings .set-key {
    font-size: 1.0333rem; padding: .75rem 1.3rem; min-height: 3.5rem;
  }
}

/* ------------------------------------------------------------ search pill --
   A field, not a button. Searching was: tap the magnifier, wait for a screen,
   find the box, then type. The box is the header now and the first keystroke
   is the search. It hands the term to the search screen on submit, which is
   what already knows how to look things up. */
/* Frosted, like the buttons - and like them it carries its own background in
   the same rule. A backdrop-filter with nothing behind it is the exact
   construction the car draws as flat black, so the blur is the bonus and the
   translucent fill is the thing that has to look right on its own. */
/* White at reduced alpha rather than the grey token. --dim is #909090, which
   is a colour chosen against black - over a bright frame it lands within a few
   points of the picture behind it and the glyph simply disappears. A white
   held back to two thirds reads as the same neutral grey on black and stays
   legible on anything else, because it is brightening away from the ground
   rather than sitting near the middle of the range. No new hue: this is the
   greyscale the palette already runs on. */
/* The least legible thing on the bar: --dimmer is that same mid grey at .62
   alpha, so over a bright still it was a word you could see the shape of and
   not read. */
/* The clear affordance browsers add to type=search is drawn as a native
   control and does not match anything else here. */

/* ---- shut, which is how it sits until it is used ----
   A magnifier and nothing else. The field was the widest thing on the bar and
   it was open on every screen, asking a question almost nobody was about to
   answer; see the note beside spOpen for why this is still the same one
   control rather than a second entry point.
   flex-basis is the animated property as well as width, because the three
   rules that size this pill do it through flex - so the collapse has to speak
   the same language they do or it only half happens. Layout rather than a
   composited transform, which this file otherwise avoids: it earns the
   exception the way .tabpill's width does, one small element once per
   interaction, and there is no transform that could do it because the field
   has to take its room FROM the bar rather than draw over it. */
/* Still focusable when shut - that is what makes tabbing to it OPEN the bar
   rather than move focus into something invisible. */

/* Sign out, armed. Same two-tap it had in the header, now that it lives on a
   page where a stray tap is likelier than it was on a small icon. */
.settings .set-opt.arm { background: #fff; color: #000; border-color: #fff; }

@media (max-width: 700px) {
  /* The field would crowd out the tabs. It keeps its place but gives up the
     width it does not need until it is being used. */
}
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  /* Out of the header's own padding, not out of --topH.
     .top is min-height: var(--topH) with .7rem top and bottom and border-box,
     so a 52.5px pill pushes the whole bar past the calc(--topH + .5rem) that
     .body-clear reserves and that .shead sticks to - which would bury the top
     of the Films|Shows switch under a fixed, gradient-filled header on the one
     device this screen was rewritten for. Raising --topH instead would move
     every screen in the app to buy one control 9px. */
  /* Tighter again in the car, where the pill is 3.5rem: 58.8px of pill needs
     the padding under about 5px a side to stay inside the same offset. */
  .top { padding-top: .28rem; padding-bottom: .28rem; }
}

/* ------------------------------------------------------------- episodes --
 * A contact sheet. One uniform grid of 16:9 stills, where how bright a
 * picture is IS the state of the episode: full brightness means held and
 * unwatched, dim means watched, near-black means missing. A season reads as
 * three bands of luminance before a single word of it is read, and exactly
 * one white ring says which episode to press.
 *
 * What this replaces gave a still to one episode at a time - whichever one it
 * had decided to expand - and drew every other episode as a line of text. On
 * a landscape screen that is a narrow column of words beside a lot of empty
 * space, with the most useful thing about an episode hidden behind a tap.
 *
 * Brightness rather than colour, and opacity rather than filter, because
 * filter does not paint on the car. A disc and a word rather than two shades
 * of grey, because two shades of grey through a glare-washed windscreen are
 * one shade of grey.
 */
.epsec { margin-top: .4rem; }
.epsec .epnote { font-size: 0.7333rem; color: var(--dimmer); font-weight: 600; letter-spacing: .04em; }

/* ---- seasons ----
   One bar for the season instead of the forty little cells this used to
   draw. The cells answered "which episodes do I have" - a question the sheet
   below now answers per episode, with a picture attached. */
/* The band: the edit tile, then the seasons, in one scroller. The tile is a
   child of it so it travels with them - pinned beside a rail that slides
   away, it read as two separate things. The seasons are wrapped in .sntabs,
   which is what carries role="tablist": the tile shares their scroller and
   is not one of them. */
.epsec .sntabs { display: flex; gap: .4rem; flex: none; }
.epsec .snrail {
  display: flex; gap: .4rem; overflow-x: auto; scroll-snap-type: x proximity;
  margin: .2rem 0 .95rem; padding-bottom: .2rem; scrollbar-width: none;
  /* a sideways flick here must not trigger the browser's back gesture */
  overscroll-behavior-x: contain;

  /* Full bleed, the same pull .detail .castrail takes and for the same
     reason: the row sat inside the sheet's gutter at BOTH ends, so there was
     dead space in front of the edit tile and the last season was cut off a
     gutter short of the screen edge, while every rail on home runs to the
     edge and lets its own padding inset the content instead.

     The arithmetic is the cast rail's, unchanged, and it holds here for the
     same two reasons. .epSlot is a sibling of .castSlot in .detail .sheet and
     is not in the 54rem max-width list, so the containing block is the same
     box. And --pad is clamp(0.95rem, 3.2vw, 3.4rem), VIEWPORT relative, so
     the gutter pulled out and the padding handed back are the same number of
     pixels on both boxes.

     margin-inline, not a `margin` shorthand: the shorthand above carries the
     .2rem top and .95rem bottom this band is spaced by, and a second
     shorthand would drop them silently. The cast rail's comment records that
     exact mistake being made on .trrail first.

     scroll-padding-left is NOT inert here, unlike on the cast rail where the
     note says it is written for a snap-align nobody has added yet: .epsec .sn
     is scroll-snap-align: start, so without this the first chip snaps to the
     scrollport edge and hides under the gutter. */
  padding-left: var(--pad); padding-right: var(--pad);
  margin-inline: calc(var(--pad) * -1);
  scroll-padding-left: var(--pad);
}
.epsec .snrail::-webkit-scrollbar { display: none; }
.epsec .sn {
  flex: none; scroll-snap-align: start;
  display: flex; flex-direction: column; align-items: flex-start; gap: .32rem;
  min-width: 5.2rem; padding: .45rem .7rem .5rem;
  background: var(--elev); border: .06em solid var(--line-hi);
  border-radius: var(--r-s); color: var(--dim);
  transition: transform .15s var(--ease), background .2s, color .2s, border-color .2s;
}
.epsec .sn-n { font-size: 1rem; font-weight: 800; letter-spacing: -.01em; }
.epsec .sn-c { font-size: 0.7333rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.epsec .sn-bar {
  width: 100%; height: .28rem; border-radius: 99px;
  background: rgba(255,255,255,.14); overflow: hidden;
}
.epsec .sn-bar i {
  display: block; height: 100%; border-radius: 99px; background: #fff;
  transform: scaleX(var(--p, 0)); transform-origin: left;
}
.epsec .sn.on { background: #fff; color: #000; border-color: #fff; }
.epsec .sn.on .sn-bar { background: rgba(0,0,0,.2); }
.epsec .sn.on .sn-bar i { background: #000; }
.epsec .sn:active { transform: scale(.97); }

/* The edit tile. It IS a .sn - same class, so the same box, the same
   min-width, the same padding and the same three rows in the same places -
   and it overrides nothing that affects size or layout. A band where one
   tile is a different height or a different width reads as a mistake before
   it reads as a distinction.
   What tells it apart is form only: a dashed edge instead of a solid one, and
   a glyph where a season puts its number. The count underneath is the SHOW,
   which is the one thing no season chip can say. */
.epsec .snedit {
  border-style: dashed;
  background: none; color: var(--dim);
}
/* Exactly one text line tall, like the "S1" it stands in for, so the bar and
   the count below it sit on the same rules as every other chip's.
   A BLOCK with an inline-block glyph, not a flex box: flex sizes the row to
   the svg and the row came out 9px short, which put the bar and the count
   above their neighbours'. Inline layout keeps the font's own strut, so the
   line is the height a line of 1rem text is, whatever the interface size
   makes that. */
.epsec .snedit .sn-n { font-size: 1rem; display: block; }
.epsec .snedit .sn-n svg {
  width: 1em; height: 1em; display: inline-block; vertical-align: -.125em;
}
.epsec .snedit .sn-bar { background: rgba(255,255,255,.10); }
.epsec .snedit:hover { color: var(--fg); background: var(--elev); }

/* ---- the sheet ----
   auto-fill against a rem minimum, so the interface-size setting makes the
   pictures bigger rather than fitting more of them in - which is what someone
   reaching for that setting actually wants. The max-width is the governor
   that stops a 2560px screen turning this into a wall of stamps. */
/* No `rise` of its own. The grid used to be inserted three seconds after the
   page and this was how it arrived; it is in the document from the first
   frame now, inside a .detail that is .screen's second child and is already
   running `rise .3s both` at a .06s delay. Two rises over one subtree do not
   blend - the opacities multiply and the translateY compounds to twenty-eight
   pixels - so the episode grid would enter from further away and dimmer than
   the sheet containing it, for the first 220ms of every series page. The
   section arrives with the page now, which is the only arrival it ever
   needed. */
.epsec .epgrid {
  display: grid; gap: 1.15rem .9rem;
  grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
}
.epsec .epgrid > .empty { grid-column: 1 / -1; }

.epsec .epc {
  display: flex; flex-direction: column; gap: .5rem;
  padding: 0; background: none; border: 0; text-align: left; color: var(--fg);
  transition: transform .15s var(--ease);
  /* For .epc-more, which hangs off the card rather than off the picture. */
  position: relative;
  /* The caption's height, as a variable, because two things now need it: the
     caption itself and .epc-more, which is positioned up from the bottom of
     the card and therefore has to know where the picture ends. It is
     overridden in two media blocks below; before this it was written out
     three times and .epc-more agreed with exactly one of them. */
  --epc-cap: 3.9rem;
}
.epsec .epc:active { transform: scale(.985); }
.epsec .epc-art {
  position: relative; width: 100%; aspect-ratio: 16 / 9;
  border-radius: var(--r-s); overflow: hidden; background: var(--bg2);
}
.epsec .epc-still { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* State, as exposure. Every one of these is qualified on .ld: the shared
   frame rules hold an image at opacity 0 until it has actually loaded, and an
   unqualified .epc-still rule outscores them - which would show a half-dim
   picture before it arrived and skip the fade entirely. */
/* Every episode is drawn at full strength.
 *
 * This used to be an exposure scale: bright meant held and unwatched, .40 meant
 * watched, .22 meant not in the library. It read as a contact sheet where most
 * of the frames had failed - and on a part-held season, most of them had,
 * because "not in the library" is the common case for a show you are partway
 * through and the grid was announcing it on every card.
 *
 * Watched is still said, by the tick in the corner, which is explicit and
 * survives glare in a way a shade of grey does not. Whether a copy is on the
 * disk is said nowhere, because it is not a fact about the episode.
 *
 * Unaired keeps its dimming: that one is not about this library, it is about
 * the episode not existing yet, and the card is disabled to match. */
.epsec .epc.soon .epc-still.ld { opacity: .3; }

/* No ring on the one to press.

   There was one, on the reasoning that a white outline survives being glanced
   at from a car seat. It went from the in-progress card first, where the
   progress bar and the Resume pill were already saying it, and now from the
   next-up card too - which leaves the emphasis entirely to words: the pill
   reads "Next up" or "Start" or "Resume", the card carries a line of synopsis
   its neighbours do not, and its title gets a full line to itself.

   That is a better set of signals than the ring was. A white rectangle around
   a picture says "this one" and nothing else; the pill says WHICH KIND of
   this-one it is, and you can read it without having first learned what an
   outline in this grid is supposed to mean. The ring was also the loudest
   thing on a page of photographs, which is a lot of shouting for what amounts
   to a bookmark. */

/* An episode with no picture is not waiting for one - it says its number and
   stops promising. Unaired episodes are the usual reason. */
.epsec .epc-ghost {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-size: 2.2rem; font-weight: 800; color: rgba(255,255,255,.14);
  font-variant-numeric: tabular-nums;
}
.epsec .epc-mark {
  position: absolute; top: .4rem; right: .4rem; z-index: 2;
  width: 1.25rem; height: 1.25rem; border-radius: 99px;
  display: grid; place-items: center; background: #fff; color: #000;
}
.epsec .epc-mark svg { width: .78rem; height: .78rem; }

/* ---- the dots ----
   Bottom-right of the picture, because .epc-mark already owns the top-right
   corner and .epc-pill the bottom-left. The remaining corner is the one
   nothing else wants.

   A child of .epc and NOT of .epc-art, which is the only surprising thing
   here. .epc-art is overflow: hidden so it can round the corners of the
   still, and that clip would cut off the invisible hit area below - the
   control would draw at 30px and be tappable at about 40. Hanging it off the
   card instead means the extension has room to exist.

   The offset is measured up from the bottom of the CARD, so it has to know
   the caption's height - which is three different values across three media
   blocks, and a hardcoded one agreed with exactly one of them. It reads
   --epc-cap instead, and that variable is now the only place any of the three
   is written. */
.epsec .epc-more {
  position: absolute; right: .4rem; z-index: 3;
  /* caption + the card's .5rem gap + the same .4rem inset the other overlays
     use, so this lands in the picture's bottom-right corner at every size. */
  bottom: calc(var(--epc-cap) + .5rem + .4rem);
  width: 1.75rem; height: 1.75rem; border-radius: 99px;
  display: grid; place-items: center; border: 0;
  background: rgba(0, 0, 0, .62); color: #fff;
  opacity: 0; transition: opacity .18s var(--ease), background .18s,
                          transform .15s var(--ease);
}
.epsec .epc-more svg { width: .95rem; height: .95rem; display: block; }
.epsec .epc:hover .epc-more,
.epsec .epc:focus-within .epc-more { opacity: 1; }
.epsec .epc-more:hover { background: rgba(0, 0, 0, .85); }
.epsec .epc-more:active { transform: scale(.9); }
/* Always there where there is no hover to reveal it with, and big enough to
   hit: 2rem is 30px at the Default text size and less at Compact, so the
   floor is in px and the invisible extension is too. */
@media (hover: none) {
  .epsec .epc-more { opacity: 1; background: rgba(0, 0, 0, .55); }
}
@media (pointer: coarse) {
  .epsec .epc-more {
    width: max(2rem, 34px); height: max(2rem, 34px);
    bottom: calc(var(--epc-cap) + .5rem + .3rem); right: .3rem;
  }
  .epsec .epc-more::after { content: ''; position: absolute; inset: -10px; }
}
.epsec .epc-pill {
  position: absolute; left: .4rem; bottom: .4rem; z-index: 2;
  padding: .16rem .5rem; border-radius: 99px; background: #fff; color: #000;
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
}
.epsec .epc.soon .epc-pill { background: rgba(255,255,255,.22); color: #fff; letter-spacing: .04em; }
/* On every card, not just the emphasised one: a deep link names its own
   episode, so the one you are part-way through can end up an ordinary card. */
/* How far into this episode you got, on the picture of it.

   The one progress bar in the app that lies on a PHOTOGRAPH rather than on a
   panel we chose the colour of, which is what the first version got wrong: a
   25%-white track under a white fill has nothing to hold it against a bright
   still, so on a snowy or daylit frame both the track and the fill washed out
   and the bar was only really visible on dark episodes. The track is black
   now - the still behind it is the variable, so the thing in front of it must
   not be.

   And it was .2rem, the thinnest of the three bars here (.sn-bar is .28,
   .pg-bar is .45) despite being the only one read at a glance from a car
   seat rather than up close. Now the same .28 as the season chip it sits
   under, which is a size this interface has already committed to.

   Still in rem, so the interface-size setting takes it with everything else.
   No rounding and full bleed to the card's edge deliberately: it reads as a
   measurement of the picture it is on, and an inset pill would read as a
   badge sitting on top of one. */
.epsec .epc-bar {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  height: .28rem; background: rgba(0, 0, 0, .55);
}
.epsec .epc-bar i {
  display: block; height: 100%; background: #fff;
  transform: scaleX(var(--p, 0)); transform-origin: left;
}

/* ---- caption ----
   Fixed height so every row of the sheet lines up whatever state its cards
   are in - the grid stays a grid. */
/* A fixed height, not a minimum: the emphasised card swaps a line of title
   for a line of synopsis, and without this it grows taller than its
   neighbours and the row it sits in stops being a row. */
.epsec .epc-cap { display: flex; flex-direction: column; gap: .12rem; height: var(--epc-cap); overflow: hidden; }
.epsec .epc-meta {
  display: flex; align-items: baseline; justify-content: space-between; gap: .5rem;
  font-size: 0.7333rem; font-weight: 700; color: var(--dimmer);
  font-variant-numeric: tabular-nums; letter-spacing: .03em;
}
/* Number and date are ONE side of the space-between, so the runtime stays
   pinned to the right edge where it was. min-width: 0 and the truncation
   because at --ui 1.26 on a two-column phone grid "12 . 24 Sep" plus "1h 4m"
   is wider than the card - and the thing to lose there is the date, which is
   also on the episode's own page, not the number, which is how you find it. */
.epsec .epc-n {
  display: flex; align-items: baseline; gap: .3rem; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.epsec .epc-tail { flex: none; }
/* Quieter than the number: the number is what you are looking for, the date
   is what you check once you have found it. */
.epsec .epc-air { color: var(--dimmer); opacity: .82; font-weight: 600; }
/* Every other separator in the app is a .dot at .35 - there is no base rule
   for the class, only scoped ones, so a new one has to say it too. */
.epsec .epc-n .dot { opacity: .35; }
.epsec .epc-t {
  font-size: 0.8667rem; font-weight: 700; line-height: 1.3; color: var(--fg);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* Titles at full strength too. A watched episode is marked, not demoted. */
.epsec .epc.here .epc-t { -webkit-line-clamp: 1; }
.epsec .epc-o {
  font-size: 0.7333rem; line-height: 1.4; color: var(--dim);
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden;
}
/* THE SHIMMER'S OWN TILE, restated. .skel paints #222226 - the ground the
   organic shimmer is designed against, whose blobs run at .11 to .14 alpha -
   but .epsec .epc-art is (0,2,0) against .skel's (0,1,0) and was quietly
   winning with var(--bg2). That is #0b0b0b, and it is why these holes were
   flat black while every other placeholder in the app had blobs in it.

   The two rules that used to live here are gone. They hushed everything past
   the third card and lifted its ground to .10 so a de-swept hole would not
   read as a black rectangle - both written when the only cap available was a
   fixed nth-child, which is three at the column count the car happens to give
   this grid and an orphan at every other width. Which cards shimmer is
   decided by the ROW now, in epcFill, and an unlit hole keeps this ground and
   reads on its own. */
.epsec .epcskel .epc-art { background: #222226; }
/* And an unlit hole is QUIET, not sweeping. .skel:not(:has(.t-shimmer)) keeps
   the plain old sweep on purpose - it is what a text bar or a heading falls
   back to - but in a grid of twenty-two, the holes past the lit rows would
   then be the OLD shimmer running beside the new one, which is the thing this
   change exists to end. They keep the tile and say nothing. */
.epsec .epcskel .epc-art:not(:has(.t-shimmer))::before { content: none; }

/* A thumb holds two columns comfortably and nothing else; auto-fill here
   would give one enormous column and a scroll the length of the season. */
@media (max-width: 640px) {
  .epsec .epgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .9rem .6rem; }
  .epsec .epc { --epc-cap: 3.4rem; }
  .epsec .epc-t { font-size: 0.8rem; }
  .epsec .epc-ghost { font-size: 1.6rem; }
}
/* At arm's length: fewer, larger cards, and chips a finger can hit. */
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .epsec .sn { min-width: 6.4rem; padding: .6rem .85rem .65rem; }
  .epsec .sn-n { font-size: 1.2rem; }
  .epsec .sn-c { font-size: 0.8667rem; }
  .epsec .sn-bar { height: .34rem; }
  .epsec .epgrid { grid-template-columns: repeat(auto-fill, minmax(19.5rem, 1fr)); gap: 1.25rem 1rem; }
  .epsec .epc { --epc-cap: 4.4rem; }
  .epsec .epc-meta { font-size: 0.8667rem; }
  .epsec .epc-t { font-size: 1.0667rem; }
  .epsec .epc-o { font-size: 0.9333rem; }
  .epsec .epc-pill { font-size: 0.8rem; padding: .2rem .62rem; }
  .epsec .epc-mark { width: 1.5rem; height: 1.5rem; }
  .epsec .epc-mark svg { width: .95rem; height: .95rem; }
}


/* ------------------------------------------------------------- timeline --
   A career with a spine. Years sit in the margin the way dates do on the
   history page; the line is the thing the eye follows back through a life.
   Every face and poster on it is a rectangle like everything else now. */
.ptl { max-width: 56rem; }
.tl-year {
  display: grid; grid-template-columns: 3.6rem minmax(0, 1fr);
  gap: 0 1.1rem; position: relative;
}
.tl-y {
  font-size: 1rem; font-weight: 800; color: var(--dim);
  font-variant-numeric: tabular-nums; text-align: right;
  padding-top: .55rem; position: sticky; top: var(--clear);
  align-self: start;
}
.tl-items {
  position: relative; padding: .3rem 0 .9rem 1.1rem;
}
/* The spine itself, with a node where each year begins. */
.tl-items::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0;
  width: 2px; background: var(--line);
}
.tl-year:first-child .tl-items::before { top: .8rem; }
.tl-year:last-child .tl-items::before { bottom: auto; height: 1.2rem; }
.tl-items::after {
  content: ''; position: absolute; left: -3px; top: .95rem;
  width: 8px; height: 8px; border-radius: 50%; background: #fff;
}
.tl-it {
  display: grid; grid-template-columns: 3rem minmax(0, 1fr) auto;
  gap: 0 .8rem; align-items: center; width: 100%; text-align: left;
  padding: .35rem .5rem; border-radius: var(--r-s); color: var(--fg);
}
.tl-it:hover { background: var(--elev); }
.tl-it:active { transform: translateY(1px); opacity: .8; }
.pface.tlface { width: 3rem; margin: 0; border-radius: calc(var(--r-s) * .8); }
.pface.tlface .pnoimg { font-size: 1rem; }
.tl-b { min-width: 0; display: flex; flex-direction: column; gap: .05rem; }
.tl-t {
  font-size: 0.9333rem; font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tl-r {
  font-size: 0.8rem; color: var(--dimmer);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tl-tail {
  font-size: 0.7333rem; font-weight: 700; color: var(--dimmer);
  letter-spacing: .04em; white-space: nowrap;
}
.tl-it.own .tl-tail { color: var(--fg); }

/* The bio unfolds in place. */
.personhead .pbio { cursor: pointer; }
.personhead .pbio.open { -webkit-line-clamp: unset; }

@media (max-width: 640px) {
  .tl-year { grid-template-columns: 2.6rem minmax(0, 1fr); gap: 0 .7rem; }
  .tl-y { font-size: 0.8667rem; }
  .tl-tail { display: none; }
}
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .tl-it { min-height: 3.4rem; }
  .tl-t { font-size: 1.0667rem; }
  .tl-r { font-size: 0.9rem; }
  .tl-tail { font-size: 0.8333rem; }
  .tl-y { font-size: 1.1333rem; }
  /* The two values this used to set are already set on every .chipbtn by the
     rule at the top of the OTHER car block - .chipbtn at app.css:4975, in the
     query opening at 4958, same three conditions. It was restating them and
     changing nothing. (This block holds the person page's timeline; the row
     landed here because it is the person page that also draws a .castchips.)
     What this row actually needs is the floor. These are the control that
     switches the cast list, they are pressed from a car seat, and measured at
     the car's own 1rem / .6rem 1.25rem a chip comes out 37px tall against a
     52.5px floor - 15.5px short. With this it measures 53px and the chip's
     WIDTH does not move at all, so the row wraps where it wrapped. The pill
     follows for free: slidePill sizes it from offsetHeight.
     NOT MEASURED on the car itself, and the thing to look at there is whether
     a row that does wrap - two lines at 53px instead of two at 37px - pushes
     the faces below it down far enough to be worth trading back. */
  .castchips .chipbtn { min-height: 3.5rem; }
}


/* ------------------------------------------------------------ hero trailer --
   The trailer sits between the backdrop and the scrim, so the text and
   buttons keep their contrast and the hero keeps saying what it said - it
   just moves. Opacity only; the car composites that for free. */
.hero .trvid {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; opacity: 0; transition: opacity .5s var(--ease);
  pointer-events: none;
  /* Overscan, per encode rather than per app.

     It still cannot be measured HERE - a cross-origin video taints any canvas
     it is drawn to, confirmed against a playing frame, and the usual escape is
     shut too: video.fandango.com sends no CORS headers, so crossOrigin
     "anonymous" does not make the pixels readable, it makes the video fail to
     load with MEDIA_ERR_SRC_NOT_SUPPORTED. The server has no such restriction
     and ffmpeg cropdetect answers in about 1.5s, so the number arrives with
     the url.

     1.13 was a hedge at both ends and it lost at both. Probed across the
     cached trailers: two clean 16:9 encodes needing 1.0, which this was
     cropping by 13% for nothing; one at 1.125; one at 1.239; two scope at
     1.343, which this left visibly letterboxed. One of six was served by it.
     It stays only as the value for a title nobody has probed yet.

     transform, so the car composites it for free. */
  transform: scale(var(--trcrop, 1.13));
}
.hero .trvid.live { opacity: 1; }
/* Once the video is opaque the Ken Burns underneath is pure compositing cost
   on a software rasteriser, painting a picture nobody can see. */
.hero .slide.trp .bg { visibility: hidden; }

/* ---------------------------------------------------- the trailer's sound --
   The disc was in the wrong FAMILY, not in no family. `.screen > .fab` -
   "Pull new content" - is a 3rem dark circle at rgba(0,0,0,.55) with a
   hairline border, fixed to the window's bottom right on EVERY screen. This
   was a 2.5rem dark circle at rgba(0,0,0,.62) with a hairline ring, at the top
   right. Ten pixels apart in diameter, at the two right-hand corners of one
   frame, and one of them reloads the library. Moving it was never going to fix
   that. It had to stop being a disc.

   Which also settles where it goes, and the corner it left is measurable.
   Evaluate `.hero.trailing .scrim::after`'s two gradients at the disc's own
   corner and BOTH are past their last stop: 0.000 black. It has been standing
   on raw video, which is why .62 was the whole contrast budget - and even then
   the note that defended .62 measured the GLYPH on the disc (3.30:1) and never
   the disc's own EDGE against the picture. That is 2.23:1 muted, and the loud
   state's dark rim on the white disc is 2.02:1: the state with the weakest
   boundary was the loud one, which is backwards. Both under the 3:1 floor.

   At the bottom left those same two gradients composite to 0.808 black at this
   glyph's lightest corner, and white ink on that - same veiling-glare model,
   recovered from the two figures above so the numbers stay comparable - is
   4.29:1 with NO GROUND AT ALL. The app already spends a gradient protecting
   that corner. The control was standing outside it, paying for its own.

   So there is no plate here. Every other thing in this gutter - the wordmark,
   h1, .meta, .plot - rides that scrim bare on a shadow. A lozenge would have
   been the only plated object among them, which is the same category of
   mistake as the circle: an object that does not match its neighbours.

   Muted it paints no ground, no ring and no box - a mark on the picture, in
   the wordmark's own gutter, on the stepper's own centre line. Sound on it
   inverts in place onto a white capsule with black ink, which is `.chipbtn.on`
   exactly (background #fff, color #000, and no contrasting rim), this app's
   one wordless on-state and the one thing about the old disc that was never
   foreign.

   Nothing travels and there is no track. This app has no switches: it has
   chips that invert where they stand, and a white pill that glides between
   options that have NAMES - .tabpill's own comment says the travel is what
   makes two options one choice. A thumb in a track with the names removed
   would have been a third family, and the Settings page already renders
   "Trailer sound / On / Off" as that widget. */
.hero .trmute {
  /* One number for the state change, because the glyph cut below is pinned to
     half of it. */
  --trm: 180ms;
  /* Bottom left, from tokens the hero already owns: left is --pad, which is
     `.hero .body`'s own left, and the vertical centre is --snd-cy, which is
     the stepper's. Neither is a number of its own and neither can drift.
     --clear is no longer read here, which also ends this control's dependency
     on the header's height - a dependency that made no sense on a phone, where
     the phone block redefines --clear to the notch inset and there is no
     header for the button to relate to.
     Structurally clear of the fab forever: that is right-anchored and fixed,
     this is left-anchored inside the hero, so no scroll offset and no width
     can bring them together. */
  position: absolute; left: var(--pad); z-index: 3;
  bottom: calc(var(--snd-cy) - var(--snd-h) / 2);
  width: var(--snd-w); height: var(--snd-h); border-radius: 99px;
  display: none; place-items: center;
  /* No background and no box-shadow, in either declaration. Both are gone,
     not moved: the ring existed because a dark disc floating on video has no
     boundary, and there is no disc. The scrim is the ground now and it was
     already paid for. */
  /* The slow half of the press. See :active for the fast half. */
  transition: transform .18s var(--ease);
}
/* `display` cannot be transitioned, so the frame where it begins to exist gets
   a fade of its own. Short for the same reason it always was: this fires on
   every trailer and the frequent event is the one that stays quiet.
   No delay, and none is needed. At the frame `.trailing` lands, the wordmark's
   bottom edge is at --dots-clear (53.30px on the car) and this box's top is at
   52.16 - the gap starts at 1.13px and only opens as the lift runs, so it is
   never negative, and this is at opacity 0 at that instant anyway. Sizing the
   control to the band BELOW the body is what removes the overlap that would
   otherwise have to be scheduled around. */
@keyframes trmIn { from { opacity: 0; } }
.hero.trailing .trmute { display: grid; animation: trmIn .16s var(--ease); }

/* How big it looks and how big it is stop being the same number: 49.14 x 35.91
   painted on the car, 52 x 52 touchable. `button` is reset to padding: 0 near
   the top of this file, so without this the box WAS the whole target.
   Both axes, because the capsule is under 52 in both - the disc's rule only
   needed the vertical one. The min() on each is load-bearing rather than
   decorative: at a large enough --ui the box passes 52 on its own and without
   the guard the inset would go positive and shrink the target to inside the
   paint. It paints nothing, so the rasteriser never sees it, and it is the
   reason this control needs no coarse-pointer block at all - the floor is
   derived at every --ui and every pointer type instead of being bumped by
   hand. That matters here specifically: the car reports `pointer: fine`, so
   the bump the old rule relied on has never fired there. */
.hero .trmute::after {
  content: ''; position: absolute;
  inset: min(0px, (var(--snd-h) - 52px) / 2) min(0px, (var(--snd-w) - 52px) / 2);
}

/* The on-state, and the hover hint, on one element and one property.
   A stacked layer rather than a background swap, for the reason this file
   already gives: background and color are not composited, so transitioning
   them would turn a one-frame snap into 180ms of continuous repaint over
   playing video, which on this rasteriser is worse than the snap it set out to
   fix. Opacity on a capsule buys the same inversion as a compositor job.
   No rim. The one this replaces was rgba(0,0,0,.3) inset and measured 1.78:1 -
   the weakest boundary in the control, on its loudest state. This capsule's
   own edge against the scrim is 4.26:1 on the car and 3.18:1 in the worst
   case, so the rim is deleted rather than ported. `.chipbtn.on` carries no
   contrasting rim either. */
.hero .trmute::before {
  content: ''; grid-area: 1 / 1;
  width: 100%; height: 100%; border-radius: inherit;
  background: #fff;
  opacity: 0;
  /* linear, and that is not a taste call: the cut below is pinned to half this
     duration on the assumption that half the time is half the fade, true only
     under linear. var(--ease) is 95% complete at the halfway mark. */
  transition: opacity var(--trm) linear;
}
.hero .trmute.unmuted::before { opacity: 1; }
/* The only affordance hint, and it is the same element as the on-state so the
   two cannot drift apart. Scoped to a real pointer - the car reports
   `pointer: fine` and `hover: hover`, so it is live there - and to the muted
   state only, so a touch screen's sticky hover cannot leave a ghost capsule on
   after a tap, and so it can never fight the state it is a preview of. */
@media (hover: hover) and (pointer: fine) {
  .hero .trmute:not(.unmuted):hover::before { opacity: .16; }
}

/* Both drawings live in the button at once, stacked in its single grid cell,
   and only their opacity changes.
   A cut, not a cross-fade: mid-fade a white cone at 50% over a black cone at
   50% is a grey cone on a capsule that is also passing through grey, and the
   icon dissolves into its own background. Both opacities flip in one frame, at
   half the capsule's fade, which under a linear fade is genuinely its middle.
   Each drawing owns its colour, so `color` is off the state axis entirely and
   never has to transition. */
.hero .trmute svg {
  grid-area: 1 / 1;
  width: 1.2rem; height: 1.2rem;
  transition: opacity 0s linear calc(var(--trm) / 2);
}
.hero .trmute .i-off { color: #fff; opacity: 1; }
.hero .trmute .i-on  { color: #000; opacity: 0; }
.hero .trmute.unmuted .i-off { opacity: 0; }
.hero .trmute.unmuted .i-on  { opacity: 1; }

/* The press, asymmetric on purpose: down fast under the finger, back slow as
   it lifts. One duration override, unambiguous only because the base
   transition list has exactly one entry for it to apply to. .96 is the figure
   .chipbtn already uses, and this is now .chipbtn's shape as well. */
.hero .trmute:active { transform: scale(.96); transition-duration: .08s; }

/* Kept, and still needed - unlike the ring, this one is not deleted by the new
   ground. The global ring is var(--ring-w) of var(--ring-focus), which is
   white, and the loud state is a white capsule: white on white with two pixels
   of raw video between. --accent-ink is the token for ink on that ground.
   The muted state needs no override, because there is nothing white under it -
   and there the ring is useful twice over, because it is the only thing that
   draws the control's footprint.
   The ring follows `border-radius: 99px` rather than the global rule's --r-s,
   because that rule is (0,1,0) and this selector is (0,2,0) - so it is a
   capsule in both states. */
.hero .trmute.unmuted:focus-visible {
  outline-color: var(--accent-ink); outline-offset: 1px;
}

@media (prefers-reduced-motion: reduce) {
  /* The same call `.top .navtab` makes: with the duration zeroed a press scale
     stops being motion and becomes a 1.97px jump, the harshest single thing
     this control can do, wearing the accessibility setting as a disguise. (It
     was 1.5px on the disc, which is the only reason the old rule read as
     small.)
     What is GONE from here is `.hero .trmute svg { transition-delay: 0s }` and
     the comment above it, which claimed the universal rule at the top of this
     file "never touches a delay". That stopped being true when the second
     universal block landed further down, which zeroes animation-delay AND
     transition-delay app-wide with !important. The rule was a no-op and the
     prose was a false statement about its own file, which is the worse of the
     two - the next reader would have believed it. */
  .hero .trmute:active { transform: none; }
}
/* The `@media (pointer: coarse)` block is DELETED, not replaced.
   --disc: 3rem existed to reach the touch floor by hand, and the floor is
   derived from --snd-h and --snd-w on both axes now, at every --ui and every
   pointer type - so there is nothing to bump and nothing to forget. It never
   fired on the car in any case: the car reports `pointer: fine`.
   `bottom: 3.4rem` went with it, and it is the one line here that would have
   turned a dead declaration into a bug. It was inert because the old base rule
   positioned from `top`, and an absolutely-positioned box given top, height
   and bottom is over-constrained with top winning. Against a rule that
   positions from `bottom` it becomes LIVE and lifts the control 34.3px off the
   stepper's line - the one alignment this design exists for - on tablets only,
   and silently. */


/* -------------------------------------------------------------- lists --
   A list is a shelf, so a tile shows what is on it. GET /lists answers a name
   and nothing else, which would have made this a column of words - the mosaic
   and the count are folded in server-side from the membership index PMDB
   forces us to build anyway.

   auto-fill against a rem minimum, so the interface-size setting makes the
   tiles bigger rather than fitting more in, which is what someone reaching
   for that setting wants. */
.listgrid {
  display: grid; gap: .5rem;
  /* The gutter, like every other block on a page. This grid had no horizontal
     padding at any width; a 92rem cap was standing in for one, so the cards
     looked inset while actually being flush, and the moment the cap went they
     ran to the bare edge of the window under a heading that did not. */
  padding: 0 var(--pad) 1rem;
  grid-template-columns: repeat(auto-fill, minmax(21rem, 1fr));
  animation: rise .22s var(--ease);
}
/* ---- a list, drawn as the things in it ----

   A row, not a poster tile. The covers fan out on the left like cards held in
   a hand and the words sit BESIDE them on the card's own surface, which is the
   whole point: a name and a count printed over three posters is unreadable
   against art this app does not choose, and no scrim fixes that without
   dimming the pictures into mud.

   It is also five times denser. The band-over-text tile ran 165px and fitted
   two lists on a phone; this is about 70 and fits seven, which is what a list
   OF lists is for.

   overflow is VISIBLE here, unlike the tile it replaces - the fan's outermost
   card leans past its own box by design and a hidden overflow would clip the
   corner off it. Nothing else in the row can spill: the text column clamps and
   the stack is a fixed size. */
.listcard {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: .85rem; padding: .6rem .75rem;
  background: var(--elev); border: .06em solid var(--line-hi);
  border-radius: var(--r-m); color: var(--fg);
  transition: transform .15s var(--ease), border-color .2s, background .2s;
}
.listcard:active { transform: scale(.985); }
.listcard:hover, .listcard:focus-visible {
  border-color: rgba(255,255,255,.34); background: var(--elev2);
}

/* The fan. Three cards, each leaning a degree further than the last, the
   newest on top - the shape a hand of cards makes, which is the one arrangement
   that reads as "several things" at 34px tall.

   Rotation and offset only, both transforms, because this has to open on a
   screen that composites nothing else cheaply. On a press the hand closes: the
   three straighten and gather, which is a squeeze rather than a spread and so
   cannot collide with the row beside it. */
.listcard .lc-stack {
  position: relative; flex: none; width: 4.35rem; height: 3.4rem;
}
.listcard .lc-stack img, .listcard .lc-mono {
  position: absolute; top: 0; left: 0;
  width: 2.27rem; height: 3.4rem; border-radius: .3rem;
  object-fit: cover; display: block; background: var(--bg2);
  box-shadow: 0 .12rem .5rem rgba(0,0,0,.6);
  border: .055em solid rgba(255,255,255,.16);
  transition: transform .22s var(--ease);
  transform-origin: 50% 90%;
}
.listcard .lc-stack img:nth-child(1) { transform: translateX(0) rotate(-9deg); z-index: 1; }
.listcard .lc-stack img:nth-child(2) { transform: translateX(.8rem) rotate(-1deg); z-index: 2; }
.listcard .lc-stack img:nth-child(3) { transform: translateX(1.6rem) rotate(7deg); z-index: 3; }
.listcard .lc-stack img:nth-child(n+4) { display: none; }
.listcard:active .lc-stack img:nth-child(1) { transform: translateX(.3rem) rotate(-3deg); }
.listcard:active .lc-stack img:nth-child(2) { transform: translateX(.72rem) rotate(0deg); }
.listcard:active .lc-stack img:nth-child(3) { transform: translateX(1.2rem) rotate(3deg); }

/* The first letter, for a list whose covers have not arrived or do not exist.
   A subject rather than a hole: centred on one card of the same shape, in the
   middle of the fan's footprint so the row does not lurch when art turns up. */
.listcard .lc-mono {
  left: .8rem; display: grid; place-items: center;
  font-size: 1.5rem; font-weight: 800; color: var(--dimmer);
  background: var(--elev2); letter-spacing: -.02em;
}

/* The affordance, and the only new element in the row. .back is a chevron
   pointing left, so it is turned around rather than given a second icon to
   keep in step. Quiet until the row is touched. */
.listcard .lc-go {
  flex: none; width: 1.05rem; height: 1.05rem; color: var(--dimmer);
  transform: rotate(180deg); opacity: .7;
  transition: opacity .2s, transform .2s var(--ease);
}
.listcard .lc-go svg { width: 100%; height: 100%; display: block; }
.listcard:hover .lc-go, .listcard:focus-visible .lc-go {
  opacity: 1; transform: rotate(180deg) translateX(-.15rem);
}
/* A hole is not a control. .skel sets no pointer-events, so a .listcard.skel
   computes auto and inherits the transition above - measured, pressing one made
   it flinch like a button that goes nowhere. .skelrow and .skelrail .track
   carry this guard already; the lists holes are the one skeleton that never got
   it. It belongs here rather than on .skel, which is also worn by .hero.skel
   and .card .art.skel where the pressable thing is an ancestor: a blanket rule
   would look like a guard while guarding nothing, and would have hidden the
   fact that this one was missing. */
.listcard.skel { pointer-events: none; }

.listcard .lc-body { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.listcard .lc-n {
  font-size: 1rem; font-weight: 800; letter-spacing: -.01em; line-height: 1.25;
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden;
}
.listcard .lc-m {
  display: flex; align-items: center; gap: .4rem; flex-wrap: wrap;
  font-size: 0.7333rem; font-weight: 600; color: var(--dimmer);
  font-variant-numeric: tabular-nums;
}
.listcard .lc-b {
  padding: .08rem .42rem; border-radius: 99px; font-weight: 800;
  font-size: 0.6667rem; letter-spacing: .06em; text-transform: uppercase;
  background: rgba(255,255,255,.14); color: #fff;
}
/* Public is a warning, not a badge. A private client that quietly made
   something public would be the worst outcome this screen can produce, so it
   is the one chip that does not look like decoration. */
.listcard .lc-b.warn { background: transparent; color: var(--warn, #f5c451);
  box-shadow: inset 0 0 0 .08em currentColor; }

/* ---- lists, by the record they live in ----
   Two sections, each a heading over its own grid. The heading is .sec, the
   same one every rail and sheet on this screen already uses, so a section
   reads as a section rather than as a new kind of band. */
.lsec + .lsec { margin-top: 1.1rem; }
.lsec > .sec { padding: 0 var(--pad); margin-bottom: .55rem; }
/* A heading that is a NAME keeps its capitals.
   .sec is uppercase with .18em tracking, which is right for "EPISODES" and
   wrong for a product: PublicMetaDB comes out PUBLICMETADB, and the camel case
   that identifies it - the one thing that makes it read as a name rather than
   a word - is exactly what the transform destroys. So a named heading opts
   out of the casing and keeps everything else, which is why it is a modifier
   on .sec rather than a heading of its own. */
.sec.named { text-transform: none; letter-spacing: .01em; font-size: .9rem; }
/* "Read-only", in the slot a Delete chip would occupy on a list you own.
   Deliberately not chip-shaped: it is a statement, and anything with a chip's
   ground beside real chips reads as a control that has failed to respond. */
.lh-ro {
  font-size: 0.7333rem; font-weight: 800; letter-spacing: .08em;
  text-transform: uppercase; color: var(--dimmer); align-self: center;
}

/* Back, on the pages reached THROUGH the Library rather than from the tab bar.
   A watchlist, a list and the lists index are all somewhere you arrived from
   somewhere, and until now the only way out was the tab you came in by - which
   returns you to the top of the shelf rather than to where you were.

   Inline in the head, not the fixed disc the detail pages float over their
   hero: there is no hero here, so a fixed button would sit on top of the
   heading it is meant to sit beside. It leads the row and the title follows,
   which is the order the eye reads them in anyway.

   Sized in max() because .iconbtn is 2.5rem, and a rem here is 15px at the
   Compact interface size - 37.5px against this app's 52px floor. The disc
   stays as drawn and the hit area grows past it on coarse pointers, the same
   trick .cardx uses for the same reason. */
.browsehead .navback { flex: none; align-self: center; margin-right: .1rem; }
@media (pointer: coarse) {
  .browsehead .navback { position: relative; }
  .browsehead .navback::after { content: ''; position: absolute; inset: -.55rem; }
}
/* ---- a record's own mark ----
   One rule for all six sites. The <i> is an inline-flex box of exactly 1em, so
   the mark scales with whatever names it - a settings heading, a tab, a
   section - and needs no per-site size. currentColor in the art means it also
   needs no per-site colour: dim beside a grey heading, black inside a lit tab.
   The .06em nudge is optical, not layout: these marks are drawn to a 24-grid
   with the trim already applied, and they sit a hair high against cap height. */
.bmark {
  display: inline-flex; flex: none; width: 1.2em; height: 1.2em;
  margin-right: .42em; vertical-align: -.06em;
}
.bmark svg { width: 100%; height: 100%; display: block; }
/* A heading is not a flex row here, so the mark rides the text baseline; a tab
   and a settings key are, and there the box does the aligning. */
.sec.named .bmark { vertical-align: -.12em; }
.ktab .bmark, .settings .set-k .bmark { vertical-align: -.14em; }
.sec.named .note { text-transform: none; letter-spacing: .01em; }

/* A list's own description, where it has one - Flick's carry them and this
   app's do not. One line, clamped: it is the only thing a list can say about
   itself that a name and a count cannot, and two of them turn a grid of tiles
   into a wall of paragraphs. */
.listcard .lc-d {
  font-size: 0.7667rem; line-height: 1.35; color: var(--dim);
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical;
  overflow: hidden;
}

/* The delete chip in a list's header. Outlined rather than filled: it sits
   next to navigation chips and must not read as the primary thing to press. */
.browsefilters .chipbtn.danger { color: var(--bad, #ff6b6b); }
.browsefilters .chipbtn.danger svg { width: .95rem; height: .95rem; }
.browsefilters .chipbtn svg { width: .95rem; height: .95rem; vertical-align: -.15em; }
.browsefilters .chipbtn { display: inline-flex; align-items: center; gap: .35rem; }

/* ---- new list ---- */
.nlform { display: flex; flex-direction: column; gap: .85rem; padding: .9rem 1rem 1.1rem; }
.nl-l { display: flex; flex-direction: column; gap: .35rem;
  font-size: 0.7333rem; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: var(--dimmer); }
.nl-opt { text-transform: none; letter-spacing: 0; font-weight: 600; opacity: .7; }
.nl-l input {
  background: var(--elev2); border: .06em solid var(--line-hi); color: var(--fg);
  border-radius: var(--r-s); padding: .6rem .7rem; font: inherit;
  font-size: 1rem; font-weight: 600; text-transform: none; letter-spacing: 0;
}
.nl-l input:focus { outline: none; border-color: #fff; }
/* The privacy switch. Stated rather than defaulted: PMDB creates a list PUBLIC
   when the field is omitted, so this is the one control on the sheet whose
   current value has to be readable without pressing it. */
.nl-pub {
  display: flex; align-items: center; gap: .6rem; text-align: left;
  background: var(--elev2); border: .06em solid var(--line-hi);
  border-radius: var(--r-s); padding: .55rem .7rem; color: var(--fg);
}
.nl-dot { width: 1.1rem; height: 1.1rem; border-radius: 99px; flex: none;
  background: rgba(255,255,255,.16); box-shadow: inset 0 0 0 .1em rgba(255,255,255,.35); }
.nl-pub.on .nl-dot { background: var(--warn, #f5c451); box-shadow: none; }
.nl-pt { display: flex; flex-direction: column; gap: .1rem; }
.nl-pt b { font-size: 0.9333rem; font-weight: 800; }
.nl-pt i { font-style: normal; font-size: 0.7333rem; color: var(--dimmer); font-weight: 600; }
.nl-row { display: flex; gap: .55rem; justify-content: flex-end; margin-top: .1rem; }
.nl-yes { display: inline-flex; align-items: center; gap: .45rem; }

/* ---- the undo toast ----
   Wider than a plain toast because it holds a sentence and a button, and the
   button is a real target rather than a link: this is the one toast anybody
   is meant to press. */
.toast.has-act {
  display: flex; align-items: center; gap: .9rem;
  padding-right: .45rem;
}
.toast .t-act {
  flex: none; padding: .35rem .8rem; border-radius: 99px;
  background: rgba(255,255,255,.16); color: #fff;
  font-weight: 800; font-size: 0.8667rem; letter-spacing: .02em;
}
.toast .t-act:active { transform: scale(.94); }

@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .listgrid { grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr)); gap: 1.1rem; }
  /* The row, at the car's reach. The fan grows with it because it is the
     thing being aimed at, and min-height states the 52px floor rather than
     leaving it to whatever the type happens to add up to. */
  .listcard { min-height: 3.5rem; padding: .75rem .9rem; gap: 1rem; }
  .listcard .lc-stack { width: 5.2rem; height: 4rem; }
  .listcard .lc-stack img, .listcard .lc-mono { width: 2.67rem; height: 4rem; }
  .listcard .lc-stack img:nth-child(2) { transform: translateX(.94rem) rotate(-1deg); }
  .listcard .lc-stack img:nth-child(3) { transform: translateX(1.88rem) rotate(7deg); }
  .listcard .lc-mono { left: .94rem; font-size: 1.7rem; }
  .listcard .lc-n { font-size: 1.1333rem; }
  .listcard .lc-m { font-size: 0.8667rem; }
  /* A toast's action is the shortest-lived control in the app - six seconds by
     toastAction's default - which is an argument for making it easier to hit,
     not harder. It had the type and the padding and not the box. */
  .toast .t-act { padding: .5rem 1rem; font-size: 1rem; min-height: 3.5rem; }
  .nl-l input { font-size: 1.0667rem; padding: .7rem .8rem; }
}
@media (max-width: 640px) {
  .listgrid { grid-template-columns: 1fr; }
  }

/* A control that does not yet know its own state. Dimmed and unpressable
   rather than guessing: list membership takes a beat to answer, and a pill
   that reads "Watchlist" unfilled while the answer is in flight is claiming
   it is not saved. */
.btn.pending { opacity: .45; pointer-events: none; }

/* ---- the picker rows ---- */
.lrow {
  display: flex; align-items: center; gap: .7rem; width: 100%;
  padding: .7rem .95rem; background: none; color: var(--fg); text-align: left;
  border-bottom: .06em solid var(--line);
}
.lrow:last-child { border-bottom: 0; }
.lrow .lr-tick {
  width: 1.5rem; height: 1.5rem; flex: none; border-radius: 99px;
  display: grid; place-items: center; color: transparent;
  background: rgba(255,255,255,.08); box-shadow: inset 0 0 0 .09em var(--line-hi);
  transition: background .18s, color .18s, box-shadow .18s;
}
.lrow .lr-tick svg { width: .9rem; height: .9rem; }
.lrow.on .lr-tick { background: #fff; color: #000; box-shadow: none; }
.lrow .lr-t { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.lrow .lr-t b { font-size: 0.9333rem; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lrow .lr-t i { font-style: normal; font-size: 0.7333rem; color: var(--dimmer); font-weight: 600; }
.lrow:active { background: rgba(255,255,255,.05); }
/* Busy is on the row, not a spinner: the row is the unit of work and the tick
   is where the eye already is. */
.lrow.busy { opacity: .5; pointer-events: none; }
.lrow.lnewrow { color: var(--dim); }
.lrow.lnewrow svg { width: 1.5rem; height: 1.5rem; flex: none; }
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .lrow { padding: .95rem 1.1rem; }
  .lrow .lr-t b { font-size: 1.0667rem; }
  .lrow .lr-t i { font-size: 0.8667rem; }
  .lrow .lr-tick { width: 1.8rem; height: 1.8rem; }
  .lrow .lr-tick svg { width: 1.1rem; height: 1.1rem; }
}

/* ----------------------------------------------------------- activity --
   Two kinds of event on one axis, told apart by a filled marker against a
   hollow one and by the fact that only one of them carries a delete. NOT by
   two shades of grey: through a windscreen in daylight those are one shade.

   No drawn spine. A rule with nodes on it looks like a timeline in a mockup
   and costs a rasteriser a long thin box plus a circle per row for something
   the dates already say; the markers alone carry the axis, and a 26-year gap
   between two of a film's releases needs no more vertical space than a
   two-week one - the dates are the measure, not the distance. */
.actsec { margin-top: .2rem; }
/* The heading and the section's one action on a single row.

   "Watched again" used to be the last thing in the section, which put a grey
   pill directly on top of the white cast-filter chip that starts the NEXT
   section - two identically shaped controls, touching, belonging to different
   things. Up here it is unmistakably Activity's, it reads as the action for
   the list it heads, and the collision is gone rather than papered over.
   .sec is already a flex row, so it needs no persuading; the wrapper exists
   only to carry the margins the heading used to own. */
.acthead { display: flex; align-items: center; gap: .6rem; margin: 1.6rem 0 .6rem; }
.acthead .sec { margin: 0; min-width: 0; }
.actlist { list-style: none; margin: .2rem 0 0; padding: 0; }
.actrow {
  display: flex; align-items: center; gap: .7rem;
  padding: .55rem 0; border-bottom: .06em solid var(--line);
}
.actrow:last-child { border-bottom: 0; }
.act-dot {
  width: .62rem; height: .62rem; border-radius: 99px; flex: none;
  box-shadow: inset 0 0 0 .11em var(--dimmer);
}
/* Yours is solid white; the world's is an outline. One is a thing you did. */
.actrow.mine .act-dot { background: #fff; box-shadow: none; }
.act-b { display: flex; flex-direction: column; gap: .1rem; min-width: 0; flex: 1 1 auto; }
.act-l { font-size: 0.9333rem; font-weight: 700; display: flex; align-items: center; gap: .45rem; }
.actrow.mine .act-l { color: #fff; }
.actrow:not(.mine) .act-l { color: var(--dim); }
.act-again {
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .06em;
  text-transform: uppercase; color: var(--dimmer);
  box-shadow: inset 0 0 0 .08em var(--line-hi);
  padding: .05rem .38rem; border-radius: 99px;
}
.act-d {
  font-size: 0.7333rem; font-weight: 600; color: var(--dimmer);
  font-variant-numeric: tabular-nums; display: flex; gap: .45rem;
}
.act-ago::before { content: '·'; margin-right: .45rem; opacity: .5; }
/* Which record this line came from, on the line.
   Same separator and the same weight as the relative date beside it, because
   it is the same kind of fact - a quiet qualifier on the date, not a label
   competing with the word above it. No hue: the accent system spends white on
   "you are here" and cyan and amber on the query, and "which database said so"
   is none of those.
   The row wraps because three items - 12 Mar 2026, 5 months ago, and
   PublicMetaDB · Flick - do not fit a 375px line, and a source silently
   clipped is worse than a source on its own line. */
.act-d { flex-wrap: wrap; }
.act-src::before { content: '·'; margin-right: .45rem; opacity: .5; }
/* A play only Flick has is still a thing you did, so it takes the same solid
   dot and the same white label as one PublicMetaDB recorded. The difference
   between them is WHERE it is written down, and that is what the name at the
   end of the line is for - it does not also need the row to look dimmer.
   After the :not(.mine) rule above on purpose: both are (0,3,0), so source
   order is the only thing deciding this. */
.actrow.fonly .act-dot { background: #fff; box-shadow: none; }
.actrow.fonly .act-l { color: #fff; }
/* A real target, not a hairline x. It is the only destructive thing on the
   page that is not behind a button already, so it gets a finger's worth of
   room and a confirmation behind it. */
.act-x {
  width: 2.1rem; height: 2.1rem; flex: none; border-radius: 99px;
  display: grid; place-items: center; color: var(--dimmer);
  background: rgba(255,255,255,.06);
}
.act-x svg { width: .85rem; height: .85rem; }
.act-x:hover, .act-x:focus-visible { color: #fff; background: rgba(255,255,255,.14); }
.act-x:active { transform: scale(.9); }
.actempty {
  font-size: 0.8rem; color: var(--dimmer); line-height: 1.45;
  padding: .5rem 0 .1rem; max-width: 34rem;
}
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .actrow { padding: .75rem 0; gap: .9rem; }
  .act-l { font-size: 1.0667rem; }
  .act-d { font-size: 0.8667rem; }
  .act-dot { width: .72rem; height: .72rem; }
  .act-x { width: 2.6rem; height: 2.6rem; }
  .act-x svg { width: 1.05rem; height: 1.05rem; }
  .actempty { font-size: 0.9333rem; }
}

/* The one control in this section. Quiet, because it is not the headline
   action on the page - the watched button above is - but quiet is not the
   same as indistinguishable, which is what it had become.

   ALIGNED WITH THE LABELS, not the section edge. The timeline is a dot rail
   at the margin and a text column 1.32rem in - .act-dot's .62rem plus
   .actrow's .7rem gap, written that way here so the two cannot drift apart -
   and this used to start at 0, so its box lined up with the dots while its
   own text lined up with nothing. Two ragged edges in a section whose whole
   shape is one straight rail.

   AND SIZED LIKE A BUTTON. It was .5rem/.8667rem, giving 39px against the
   44px of every other button in the app; on the car, where this section is
   read at arm's length, it was the smallest target on the page.

   AND IT READS AS A CONTROL NOW. It was var(--dim) at weight 700, which is
   character for character the same colour and weight as the row labels
   beside it - a button rendered exactly as the text it sits under, told
   apart only by a 7%-white background. Full-strength text, weight 800 like
   the app's other buttons, and a hairline so it has an edge. Still a quiet
   background rather than the white fill of a primary action, which is the
   register it should be in: obviously pressable, obviously secondary. */
.actagain {
  display: inline-flex; align-items: center; gap: .4rem;
  /* Pushed to the end of the heading row. It sits beside the word Activity
     now rather than under the last event, which is why it is also quieter
     than it was: at the foot of a list it was the only control in sight and
     could afford to look like a button, but next to a heading it is a section
     action and must not outweigh the heading it shares a line with. */
  margin: 0 0 0 auto; flex: none; white-space: nowrap;
  padding: .42rem .85rem; border-radius: 99px;
  color: var(--dim); background: none;
  box-shadow: inset 0 0 0 .06em var(--line-hi);
  font-weight: 700; font-size: 0.8rem;
}
.actagain svg { width: .85rem; height: .85rem; }
.actagain:hover, .actagain:focus-visible {
  color: var(--fg); background: rgba(255,255,255,.1);
  box-shadow: inset 0 0 0 .06em var(--line-hi);
}
.actagain:active { transform: scale(.96); }
.actagain.busy { opacity: .5; pointer-events: none; }
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  /* 52px, which is the size this app settled on for a target you reach for
     from a car seat. Kept, even though the pill is quieter in the heading row
     than it was at the foot of the list - a control gets smaller to the eye,
     never to the thumb. min-height rather than padding, so the box grows to
     the target without the label drifting away from the heading beside it. */
  .actagain { min-height: 3.47rem; padding: 0 1.4rem; font-size: 0.9333rem; }
  .actagain svg { width: 1.05rem; height: 1.05rem; }
}

/* ------------------------------------------------------- image viewer --
   Solid black, not frosted glass. A backdrop-filter here would paint flat
   black on the car anyway, and flat black is exactly what a picture wants
   behind it - so it is asked for rather than arrived at by accident, and it
   costs a fill instead of a blur.

   Only opacity and transform move. */
/* The ground is a layer, not the wrapper.
 *
 * It used to be `background: #000` on .shotwrap with the whole wrapper fading
 * on the way out - and the wrapper CONTAINS the picture, so closing faded the
 * picture too. It went transparent inside about a hundred milliseconds, long
 * before it had flown home, which is why closing looked like it had no
 * animation: there was one, you just could not see it happen.
 *
 * So the black gets its own layer and fades on its own. The image keeps full
 * opacity all the way into the thumbnail, which is the entire point - it is
 * meant to read as the same object going back where it came from, and an
 * object that dissolves on the way is not that. */
.shotwrap {
  position: fixed; inset: 0; z-index: var(--z-full); background: transparent;
  display: grid; place-items: center;
  touch-action: none;                 /* a swipe pages, it does not scroll */
}
.shotwrap::before {
  content: ''; position: absolute; inset: 0; background: #000;
  animation: shotIn .18s var(--ease);
}
@keyframes shotIn { from { opacity: 0; } to { opacity: 1; } }
/* .26s against the image's .24s, so the ground is still there as the picture
   lands. Black clearing first would leave it finishing its journey over the
   page it came from, which reads as two animations rather than one. */
.shotwrap.leaving::before { opacity: 0; transition: opacity .26s var(--ease); }
/* The dissolve keeps that .26s - it takes the wrapper away at 190ms and a
   420ms fade would still be half black when it vanished. Only the flight
   home gets the longer one, and it gets 20ms more than the flight so the
   black is still there as the picture lands. */
.shotwrap.leaving.homing::before {
  transition-duration: calc(var(--opentilt-close-dur, 420ms) + .02s);
}
/* On the way out nothing is pressable. A swipe landing during the dive used
   to run a page turn on a viewer already being removed - the picture snapped
   back to full screen and then everything vanished - and the close is now
   420ms rather than 260, which nearly doubles the window for it. */
.shotwrap.leaving { pointer-events: none; }
/* The furniture leaves with the ground, not with the picture. */
.shotwrap.leaving .shot-x, .shotwrap.leaving .shot-bar { opacity: 0; }
.shot-stage {
  position: absolute; inset: 0; display: grid; place-items: center;
  padding: 3.4rem 1rem 4.2rem;
}

/* ---- a still opens with the same tilt as a portrait ----

   transitions.dev's image-open-tilt again, on the other picture in this app
   worth opening properly. Same recipe, same clock, same angles. Three things
   differ, and all three are the same fact: a still fills the screen and a
   portrait does not.

   THE PERSPECTIVE IS A TRANSFORM FUNCTION, NOT A PROPERTY ON A PARENT. The
   portrait hangs its perspective on a stage that shrink-wraps the card, so
   the vanishing point IS the card. The only parent here is .shot-stage,
   which is inset: 0 - full bleed - so a perspective on it resolves its
   origin to the middle of the screen, and the picture is nowhere near the
   middle of the screen for most of its flight. Measured at 1400x900, sitting
   on a thumbnail at -480/+280 and scaled to .35: the property version came
   out 486px tall against 268 unpitched - 1.81x, sheared to nearly square,
   and worst exactly where it is furthest from the vanishing point. The
   function version came out 1.21x, which is its own at-rest ratio, because
   perspective() puts the vanishing point on the element's own
   transform-origin and it rides along. The picture keeps one shape the whole
   way home. It is also why this costs one element and no perspective parent.

   AND THE LENS IS PROPORTIONAL. perspective is an absolute length, so the
   900px that suits a 26rem card mangles a 1883px picture - at the car's
   1921x1141 it pitched a still to 2874px wide inside a 1921px screen. The JS
   writes --shot-persp as three times the picture's measured width, so the
   lean reads the same on a phone, a desk and the car.

   THE LIFT IS A PULL-BACK. The recipe's translateZ brings the card forward,
   which a picture already filling the screen has no room to do - measured at
   car size, the lift alone threw it off the side. A scale() does the job the
   lift was doing, the picture receding as it leans, and it is symmetric
   about the centre so nothing is thrown anywhere. It sits immediately after
   perspective() so it shrinks the projected result rather than the local
   space before the rotation. It only has to be a few percent because the two
   peaks do not coincide: the flight is a heavy ease-out and is only about
   three quarters home at the 40% mark where the lean peaks.

   THE LEAN IS HERE AND PAGING IS ON THE PICTURE INSIDE. A filling CSS
   animation outranks inline style, so a keyframe on .shot-img's transform
   would silently discard every transform go() writes - measured, an inline
   translateX(-56px) computing to identity. It cannot reach across elements,
   and it cannot reach translate or scale, which is where the flight rides.

   Licensed from transitions.dev (Pro) on 2026-09-07. */
/* Nothing sizes this: as a grid item of the centring stage it shrink-wraps
   the picture on its own. The origin is the default and is stated anyway,
   because the flight's scale and the lean's rotations both hang off it and a
   later rule moving it would quietly ruin both. */
.shot-tilt { transform-origin: center; }
.shot-tilt.is-open    { animation: t-shot-tilt-open  var(--opentilt-open-dur)  both; }
.shot-tilt.is-closing { animation: t-shot-tilt-close var(--opentilt-close-dur) both; }

@keyframes t-shot-tilt-open {
  0% {
    transform: perspective(var(--shot-persp, 2600px)) scale(1)
               rotateX(0deg) rotateY(0deg);
    animation-timing-function: cubic-bezier(0.3, 0.7, 0.4, 1);
  }
  40% {
    transform: perspective(var(--shot-persp, 2600px)) scale(var(--shot-pull, .94))
               rotateX(var(--opentilt-tilt-x)) rotateY(var(--opentilt-tilt-y));
    animation-timing-function: cubic-bezier(0.45, 0, 0.3, 1);
  }
  100% {
    transform: perspective(var(--shot-persp, 2600px)) scale(1)
               rotateX(0deg) rotateY(0deg);
  }
}
@keyframes t-shot-tilt-close {
  0% {
    transform: perspective(var(--shot-persp, 2600px)) scale(1)
               rotateX(0deg) rotateY(0deg);
    animation-timing-function: cubic-bezier(0.4, 0.4, 0.5, 1);
  }
  45% {
    transform: perspective(var(--shot-persp, 2600px))
               scale(calc(1 - (1 - var(--shot-pull, .94)) * 0.4))
               rotateX(calc(var(--opentilt-tilt-x) * -0.45))
               rotateY(calc(var(--opentilt-tilt-y) * -0.45));
    animation-timing-function: cubic-bezier(0.45, 0, 0.3, 1);
  }
  100% {
    transform: perspective(var(--shot-persp, 2600px)) scale(1)
               rotateX(0deg) rotateY(0deg);
  }
}
/* contain, so a still is never cropped and never upscaled past its own size -
   the whole reason for serving `original` was to stop showing less than there
   is. */
/* contain, so a still is never cropped and never upscaled past its own size.
   No entry animation of its own any more: paging drives transform and opacity
   from script, and a keyframe on the same element would be fighting it for
   the same two properties. */
.shot-img {
  max-width: 100%; max-height: 100%; object-fit: contain; display: block;
  will-change: transform, opacity;
}
/* The loader waits for the flight. paint() shows it the moment the small
   copy goes up and only hides it when the full one lands, so without this a
   full-size dot loader sits on top of a picture that is still the size of
   its thumbnail - for 260ms before this change, and for 620ms after it. */
.shot-spin {
  position: absolute; inset: 0; display: grid; place-items: center;
  pointer-events: none; transition: opacity .2s var(--ease);
}
.shot-tilt.is-open ~ .shot-spin { opacity: 0; }
/* And through a page turn, for the same reason: paint() unhides it on every
   turn, so a full-size dot loader used to appear over a picture that is
   sliding out and back in. */
.shotwrap.turning .shot-spin { opacity: 0; }
.shot-spin[hidden] { display: none; }

.shot-x {
  position: absolute; top: 1rem; right: 1rem; z-index: 2;
  width: 2.8rem; height: 2.8rem; border-radius: 99px;
  display: grid; place-items: center; color: #fff;
  background: rgba(255,255,255,.12);
}
.shot-x svg { width: 1.1rem; height: 1.1rem; }
.shot-x:active { transform: scale(.92); }
.shot-x:hover { background: rgba(255,255,255,.22); }

/* Furniture off. Opacity only, so the picture behind it never reflows and the
   car never has to do anything but composite - and pointer-events go with it,
   or an invisible close button still catches the tap meant for the image. */
.shotwrap .shot-x, .shotwrap .shot-bar {
  transition: opacity .2s var(--ease);
}
.shotwrap.bare .shot-x, .shotwrap.bare .shot-bar {
  opacity: 0; pointer-events: none;
}

/* The only two things TMDB actually knows about a still: where it sits in the
   set, and how big it is. There is no caption to print. */
.shot-bar {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  display: flex; align-items: center; justify-content: center; gap: 1rem;
  padding: 1rem 1rem calc(1.1rem + env(safe-area-inset-bottom, 0px));
  font-size: 0.8667rem; font-weight: 700; color: var(--dim);
  font-variant-numeric: tabular-nums; letter-spacing: .04em;
  pointer-events: none;
}
.shot-d { color: var(--dimmer); }

@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .shot-x { width: 3.4rem; height: 3.4rem; }
  .shot-bar { font-size: 1rem; }
}
@media (max-width: 700px) {
  .shot-stage { padding: 3rem .5rem 3.6rem; }
}
@media (prefers-reduced-motion: reduce) {
  .shotwrap { animation: none; }
  /* Not belt and braces. The two global blocks set animation-duration to
     .01ms and touch neither animation-name nor animation-fill-mode - so a
     `both`-filled lean would finish in a hundredth of a millisecond and then
     HOLD its last keyframe for as long as the viewer is open. That frame is
     identity, so it would look like nothing at all while keeping the picture
     on the perspective draw path forever. The JS is what stops the class
     being added; this is what stops it mattering if that ever changes. */
  .shot-tilt, .shot-tilt.is-open, .shot-tilt.is-closing { animation: none; }
  .shotwrap .shot-x, .shotwrap .shot-bar { transition: none; }
}

/* A subtitle is a download, and one over the same link the video is using.
   The button says so while it happens - before this there was no signal at
   all, which is indistinguishable from the thing being broken, and for a
   while it genuinely was. */
/* The subtitle button, while its file is coming down.
   Pulsing rather than spinning: the icon is a captions box, and rotating a
   rectangle reads as something broken rather than something working - a
   spinner has to be round to mean spinner. Opacity also happens to be one of
   the two properties the car's browser composites cheaply.
   Unclickable while it loads, so a second tap cannot queue a third state on
   top of the two already in flight. */
.player .iconbtn.subs.busy { pointer-events: none; animation: subpulse 1.1s ease-in-out infinite; }
@keyframes subpulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) {
  .player .iconbtn.subs.busy { animation: none; opacity: .55; }
}


/* =========================================================== home shapes ==
 * A row about a decade should not look like a row about a studio, and until
 * now every row on this screen was the same horizontal rail of posters - so
 * the shape carried no information at all. The heading said "the 1980s" and
 * the picture said "here are some films", and the picture is what gets read.
 *
 * ONE PRINCIPLE, which is what stops five shapes being five styles: the
 * rule's own value is the largest thing in the block and the posters are
 * demoted to evidence under it. Only the value and the position of the
 * evidence change between shapes.
 *
 * There is exactly one colour in this app and it is white, so the only
 * instruments here are size, position and space. Nothing below uses a filter,
 * a blur or a shadow that carries meaning - none of the three paint on the
 * car - and nothing animates anything but transform and opacity.
 */
/* ---- two hues, and each means exactly one thing ----
   The rule, in one sentence:
     grey  = neutral, this control is contributing nothing
     white = where you are (Films/Shows, the current view) - unchanged
     cyan  = you have narrowed the question that leaves this machine
     amber = you are hiding part of the answer that came back

   Three tiers per hue is what keeps the chrome off the artwork's turf. The
   saturated value is spent only on 1-2px rings and small marks; a lighter
   tint carries text; and the fills are near-black with a cast, so no large
   saturated area exists anywhere in the band. Posters are big photographic
   rectangles - the chrome stays a flat dark strip with quietly tinted pills,
   separated from the art by field and by area rather than by avoiding colour.

   Cyan and amber are also chosen for being cold and warm respectively: cold
   is rare as a poster's dominant, and warm is the right register for "there
   is something you are not being shown". */
:root {
  --ask: #4CC9E8;                        /* 10.9:1 on black */
  --ask-tint: #7FDCF2;                   /* label text when set */
  --ask-fill: #0E2C34;                   /* ground when set - 1.4:1, present, never loud */
  --ask-ring: rgba(76, 201, 232, .55);

  --hide: #F2B33D;                       /* 11.3:1 on black */
  --hide-tint: #FFD37A;
  --hide-fill: #2E2205;
  --hide-ring: rgba(242, 179, 61, .55);
}
:root {
  /* Evidence is two thirds of a rail card. That ratio is the sentence "this
     row is about the rule, not about these particular films", said before a
     word is read. */
  --ev-card: 8.4rem;
  --rule: rgba(255, 255, 255, .25);
  --rule-w: 2px;
}
/* The same gap as .rail. These were 1.9 against the rail's 1.7 with nothing
   explaining the difference, so a page that alternates rails and hs sections
   had two rhythms three pixels apart - far too close to read as deliberate
   and far enough to read as sloppy. */
.hs { margin: 0 0 var(--row-gap); }
.hs-h { padding: 0 var(--pad); }
/* Evidence carries no caption: the block is about the rule, and a caption
   under every poster would add sixty pixels of text to a shape whose whole
   argument is that it is cheaper than the rail it replaces. */
.ev { width: var(--ev-card); }
.ev .t, .ev .s { display: none; }

/* ---- a decade, as a run of years ----
   Ten fixed columns with posters stacked on a ruler was the first attempt, and
   the note it carried argued that an empty column was content. Against the
   real row - 20 films spread 1,1,0,1,5,2,0,3,4,3 - it capped the stack at two
   and threw seven of them away, drew two holes, and read as a ragged scatter.
   A chart wants dense even data; a decade of somebody's taste is lumpy.

   So the films touch, in year order, and a marker sits under the first of each
   year, and nothing is dropped to fit a row height. The source was corrected
   in the same change - it asks year by year now rather than ranking the whole
   decade - so the runs are even and the axis is telling the truth. A year that
   ever answers with less simply draws a shorter run. */
.pl-scroll { overflow-x: auto; scrollbar-width: none; padding: 0 var(--pad) .2rem; }
.pl-scroll::-webkit-scrollbar { display: none; }
.pl-run { display: flex; align-items: flex-end; gap: 0; width: max-content; }
/* The run of one year: its films, then its marker. A column so the marker
   tracks the films above it however many there are. */
.pl-yr { display: flex; flex-direction: column; min-width: 0; }
.pl-films { display: flex; gap: .3rem; padding-right: .3rem; }
.pl-films .ev { width: var(--ev-card); flex: none; }

/* The marker, and the rule it sits on. The rule is drawn per year rather than
   once across the row, so it starts and stops with the films it measures -
   which is what makes a wide year look like a wide year. */
.pl-tick {
  display: flex; align-items: center; gap: .38rem;
  margin: .45rem .3rem 0 0; padding-top: .4rem;
  border-top: var(--rule-w) solid var(--rule);
  font-size: 0.7333rem; font-weight: 800; letter-spacing: .06em;
  color: var(--dimmer); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.pl-tick i {
  display: block; width: var(--rule-w); height: .42rem; flex: none;
  background: currentColor; margin-top: -.4rem;
}
/* The decade's start and middle anchor the rest, so they are the two the eye
   lands on first. Everything else is a two-digit year and stays quiet. */
.pl-yr[data-full] .pl-tick { color: var(--fg); }

/* ---- a genre, as the left edge of its own row ----
   The one rule kind where the artwork actively lies: twelve horror posters
   look like twelve posters, and the word is the only thing saying horror.
   vertical-rl is the book-spine direction and needs no transform, so there is
   no rotated-raster cost on a software rasteriser. */
.spine {
  display: flex; align-items: stretch; gap: .9rem;
  padding-left: var(--pad); margin: 0 0 var(--row-gap);
}
.sp-word {
  flex: 0 0 auto; align-self: center;
  writing-mode: vertical-rl; text-orientation: mixed;
  /* Weight, not size. A rotated stem loses subpixel positioning along its own
     axis, so a vertical word needs mass where a horizontal one needs scale:
     800 at 60px reads at arm's length and 200 at 60px is mush. --k is written
     by JS so DOCUMENTARY shrinks to fit instead of running past the posters. */
  font-size: clamp(1.7rem, calc(16rem * var(--k, .22)), 4rem);
  font-weight: 800; letter-spacing: .04em; text-transform: uppercase;
  line-height: 1; color: var(--fg); user-select: none;
}
.sp-body { flex: 1 1 auto; min-width: 0; }
.spine .sec { margin: 0 0 .5rem; padding-right: var(--pad); }
/* A shorthand here replaces the base padding-top, so the room is folded in.
   The negative margin is inherited from .track and is still correct. */
.sp-track { padding: calc(.2rem + var(--lift-room)) var(--pad) .5rem 0; }

/* ---- a rank, down a column, all of it on screen ----
   A ranking whose eighth item is behind the right bezel is not a ranking. A
   rail shows four cards on the car and hides the rest, so "the ten highest
   rated" renders identically to "ten films" - the shape and the heading
   disagree, and the shape wins because nobody reads the heading. */
.lg-list {
  list-style: none; margin: 0; padding: 0 var(--pad);
  display: grid; grid-auto-flow: column; column-gap: 1.6rem;
  /* 1fr, not a fixed 34rem: the column count is chosen in JS from the measured
     width, so the columns that DO fit should share the row rather than leaving
     the remainder as a gutter on the right. LG_COL_REM in app.js is the
     narrowest a column may be and must stay in step with the 22rem floor. */
  grid-auto-columns: minmax(22rem, 1fr); justify-content: stretch;
}
.lg-r {
  display: grid; grid-template-columns: 2.2rem 2.4rem minmax(0, 1fr) auto;
  align-items: center; gap: 0 .8rem;
  min-height: 3.6rem; padding: .25rem .3rem; border-radius: var(--r-s);
  cursor: pointer; transition: background .18s;
  width: 100%; text-align: left; background: none; border: 0; color: inherit;
}
.lg-r:hover { background: var(--elev2); }
.lg-r:active { transform: scale(.99); }
.lg-n {
  font-size: 1.25rem; font-weight: 800; color: var(--dimmer);
  font-variant-numeric: tabular-nums; text-align: right; line-height: 1;
}
/* The shimmer is sized for a rail card, and this is a twentieth of one.
   --shimmer-scale multiplies both the band's blur and the wrapper's overhang,
   and .7 was tuned against a card's ~159px-wide art where the 3.5px blur is
   2.2% of the tile. This poster measures 45px wide on a desk and 36 on a
   phone, where the same blur is 7.8% and 9.7% - the band stops being a band
   and becomes a haze over the whole thing. .2 puts the proportion back where
   the card's is. */
.lg-p { --shimmer-scale: .2; }
.lg-p { display: block; width: 2.4rem; aspect-ratio: 2 / 3; border-radius: 3px; overflow: hidden; }
.lg-p img { display: block; width: 100%; height: 100%; object-fit: cover; }
.lg-t {
  display: block; font-size: 0.9rem; font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lg-v { font-size: 0.8333rem; font-weight: 800; color: var(--dim); font-variant-numeric: tabular-nums; }

/* ---- a name that owns a body of work ----
   A studio, a network, a country. The name is the claim and the films are the
   proof of it - the opposite of a genre row, where the films are the point
   and the word is a label - so the name gets a column rather than a heading. */
.deck { display: flex; align-items: center; gap: 1.2rem; padding-left: var(--pad); }
.dk-head { flex: 0 0 11rem; min-width: 0; }
.dk-name {
  font-size: clamp(1.5rem, 2.6vw, 2.2rem); font-weight: 800;
  letter-spacing: -.02em; line-height: 1.05; color: var(--fg);
}
/* The mark is sized by HEIGHT and left to find its own width, which is the
   only way to lay out logos whose aspect ratios run from a wide wordmark to a
   tall roundel. max-width stops the widest of them eating the hand beside it. */
.dk-logo {
  display: block; height: 2.6rem; width: auto;
  max-width: 100%; object-fit: contain; object-position: left center;
}

/* ---- a genre, under its own mark ----
   A masthead rather than a left column: a studio MADE these films and a genre
   merely describes them, so the mark sits over the row as a title rather than
   beside it as an owner. */
.mast { margin: 0 0 var(--row-gap); }
.mt-head { display: flex; align-items: center; gap: .8rem; padding: 0 var(--pad) .1rem; }
.mt-logo { display: block; height: 2.2rem; width: auto; max-width: 42%; object-fit: contain; }
.mt-h { padding: 0; margin: 0; }
.mt-track {
  padding: calc(.3rem + var(--lift-room)) var(--pad) .5rem;
  margin-top: calc(var(--lift-room) * -1);
  /* Same fix, same reason as .rail .track above: without it the first card
     snaps to the scrollport edge and sits under the padding, so the row
     starts flush left while every rail starts at the gutter. */
  scroll-padding-left: var(--pad);
}
/* Small, and beside the words rather than over them - the ledger's heading is
   already doing the work here and the mark is a note on it. */
.lg-mark { height: 1.1rem; width: auto; vertical-align: -.18rem; margin-right: .5rem; }
.dk-kick {
  margin-top: .3rem; font-size: 0.7333rem; font-weight: 800;
  letter-spacing: .14em; text-transform: uppercase; color: var(--dimmer);
}
.dk-hand {
  flex: 1 1 auto; min-width: 0;
  padding: calc(.2rem + var(--lift-room)) var(--pad) .5rem 0;
  margin-top: calc(var(--lift-room) * -1);
}

/* ---- the index: a way in, rather than a list of titles at all ----
   The cheapest shape here per unit of information and the only one carrying
   no posters, which is exactly why it earns a place on a screen that already
   scrolls too far - the page finishes in a door rather than in more of the
   same. Every cell is a real query against the browse tab.

   Four bands under one masthead, not four sections. A page that ends four
   times has not ended. */
.index { margin: 2.6rem 0 0; padding: 0 0 1.4rem; }
.ix-top {
  padding: 1.5rem var(--pad) .2rem;
  border-top: var(--rule-w) solid var(--rule);
}
/* Sentence case and tight, against every other heading on the page being a
   shouted label - the index is the one thing here that speaks rather than
   files. */
.ix-title {
  margin: 0; font-size: clamp(1.6rem, 3vw, 2.3rem); font-weight: 800;
  letter-spacing: -.03em; line-height: 1.05; color: var(--fg);
}
.ix-sub { margin: .28rem 0 0; font-size: .9333rem; color: var(--dim); }

.ix-band { padding: 1.35rem 0 0; }
.ix-h {
  margin: 0 0 .55rem; padding: 0 var(--pad);
  font-size: .7333rem; font-weight: 800; letter-spacing: .14em;
  text-transform: uppercase; color: var(--dim);
}
.ix-h .note {
  margin-left: .6rem; letter-spacing: .1em; color: var(--dimmer);
  font-weight: 700;
}
.ix-grid {
  display: grid; gap: .55rem; padding: 0 var(--pad);
  grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
}

/* One cell, one door. The plate is the shared ground; what sits on it is what
   the band is made of. */
.ix-cell {
  min-height: 3.6rem; display: flex; align-items: center; justify-content: flex-start;
  padding: 0 1rem; border-radius: var(--r-s);
  /* background-COLOR, not the shorthand: .ix-door paints a wash into
     background-image and a shorthand here - or on the hover below - would
     reset it every time the pointer crossed a card. */
  background-color: var(--elev); border: var(--rule-w) solid transparent;
  color: var(--fg); font-size: 1rem; font-weight: 800; letter-spacing: -.01em;
  transition: background .18s, border-color .18s, transform .12s var(--ease);
}
.ix-cell:hover, .ix-cell:focus-visible { background-color: var(--elev2); border-color: var(--rule); }
.ix-cell:active { transform: scale(.97); }
.ix-word { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Films or television, at the head of the band. Two small tabs beside the
   heading rather than a second button inside all twenty-three cells: the
   per-cell version put a TV square next to every genre that had one, and a
   grid of "Action" beside "TV" beside "Adventure" has to be decoded before it
   can be used. Selected is the house's own: white ground, black ink. */
.ix-tabs { display: inline-flex; gap: .3rem; margin-left: .7rem; vertical-align: -.15rem; }
.ix-tab {
  min-height: 1.65rem; padding: 0 .62rem; border-radius: 999px;
  background: var(--elev); border: var(--rule-w) solid transparent;
  color: var(--dim); font: inherit; font-size: .7333rem; font-weight: 800;
  letter-spacing: .08em; text-transform: uppercase;
  transition: background .18s, color .18s, border-color .18s;
}
.ix-tab:hover, .ix-tab:focus-visible { background: var(--elev2); color: var(--fg); }
.ix-tab.on { background: #fff; color: #000; border-color: #fff; }

/* ---- a door: its mark, three films from behind it, and how many there are --

   What this replaces put ONE backdrop under the mark at opacity .42, and the
   rule's own comment said what that was for: dimmed until it is atmosphere
   rather than content. It was a real film, the door's own top result, drawn
   at a strength chosen by measuring how far a photograph can be dimmed before
   a wordmark starts fighting a face - which is to say, chosen so it could not
   be identified. An image tuned until it cannot be read, of something the
   page could have shown honestly, is the one thing in this section that was
   pretending. Three posters at 34px are three specific films. They also cost
   LESS: 46 backdrops at w=480 against 138 posters at w=120.

   Dropping the backdrop is also what makes the hand possible at all, and that
   is not a coincidence: overflow: hidden existed to clip the picture to the
   corner radius, and a fan has to lean past its own box to read as a fan. */
.ix-door {
  justify-content: flex-start; text-align: left;
  padding: .75rem; gap: .65rem; overflow: visible;
}

/* ---- the card, lit by the films in it ----
   Two colours taken off the hand this card is holding - not off the door, so a
   door that deals a different three next visit is washed differently. They are
   read out of the posters when the weekly gather runs, not in the browser: 138
   canvas reads on every home render is the kind of work this app pushes to the
   relay, and the answer is good for a week.

   Where the light comes FROM is the whole idea. The first wash is centred on
   the hand at 12% and falls away to the right, so the card reads as lit by the
   pictures lying on it; the second comes in low from the far corner, in the
   second poster's colour, which is what stops a one-colour card looking like a
   tint someone applied. A radial rather than a linear because a linear band has
   an edge and a wash should not.

   Not a filter and nothing animated: the car paints neither, and a gradient is
   one of the things its rasteriser does without complaint.

   rgba over the cell's own translucent ground, never an opaque fill - an opaque
   surface here would punch a flat hole in body::before's wash and body::after's
   grain, on 46 cards at once.

   .30 and .17 are what the CONTRAST allows, measured rather than picked. The
   count is the dimmest thing on the card at rgba(144,144,144,.62), and it sits
   at 68% across and 76% down - past where the first wash has faded to a fifth
   of its strength and short of where the second arrives. Both together put
   0.108 of tint under it, and the worst of the 46 cards - Crime - holds
   5.13:1 against a 6.05 baseline and a 4.5 floor. Measured at the full .30
   under the text it would have been 3.08, which is the number to remember if
   these ever move: it is the falloff doing the work, not the alpha. */
.ix-door {
  background-image:
    radial-gradient(120% 150% at 12% 50%, rgba(var(--c1), .30), transparent 60%),
    radial-gradient(95% 130% at 100% 100%, rgba(var(--c2, var(--c1)), .17), transparent 58%);
}

/* The hand. Three cards, each leaning a degree further than the last, newest
   on top - the .listcard's shape one screen over, at the .listcard's own
   numbers, because a fan that disagreed with it by a degree would be a second
   idiom rather than the same one.

   z-index 0 rather than nothing, and it is load-bearing. The three cards
   carry z 1/2/3 and a position: relative parent with z-index auto opens no
   stacking context, so they would sit in the cell's own context alongside the
   mark. A zero contains them at REST - the only state that was ever safe
   without it is :active, whose transform opens a context of its own.

   One number and not six. The offsets are fractions of --fan rather than six
   literals, because the car and the phone each want a different stack width
   and the .listcard shows the cost of writing them out: three translations
   restated per breakpoint, nine figures in three places to hold in agreement
   for ever. Here a size change is one declaration. */
.ix-hand {
  --fan: .8rem;
  position: relative; z-index: 0; flex: none;
  width: 3.9rem; height: 3.4rem;
}
/* Two fixed lengths and not aspect-ratio, so a poster that never arrives
   cannot collapse its own slot. Every dimension is on the frame: the hand is
   3.9 x 3.4rem whatever loads into it.
   --elev2 and not the .listcard's --bg2. #0b0b0b is opaque, and an opaque
   surface punches a flat hole in body::before's wash and body::after's grain.
   Eight of them on the lists screen does not read; 138 across the index
   would. --elev2 is the same rgba the cell's own hover ground uses. */
.ix-hand img {
  position: absolute; top: 0; left: 0;
  width: 2.27rem; height: 3.4rem; border-radius: .28rem;
  object-fit: cover; display: block; background: var(--elev2);
  box-shadow: 0 .1rem .4rem rgba(0, 0, 0, .6);
  border: .055em solid rgba(255, 255, 255, .16);
  transition: transform .22s var(--ease); transform-origin: 50% 90%;
}
.ix-hand .ix-p1 { transform: rotate(-9deg); z-index: 1; }
.ix-hand .ix-p2 { transform: translateX(var(--fan)) rotate(-1deg); z-index: 2; }
.ix-hand .ix-p3 { transform: translateX(calc(var(--fan) * 2)) rotate(7deg); z-index: 3; }
/* On a press the hand CLOSES - the three straighten and gather. A squeeze and
   never a spread, and that is stricter here than it was in a single column of
   lists: on a grid nine wide a spread puts a pressed card's corner inside its
   neighbour. Transform only, which is one of the two things the car's
   rasteriser composites without repainting. */
/* Keyed off the BUTTON, not off either card's own class. These were
   .ix-door:active, which was right while the index was the only thing holding
   a hand and silently wrong the moment the pick cards started holding one -
   they got the fan and not the squeeze, and a hand that does not close on a
   press reads as a picture rather than as a control. A hand only ever lives
   inside a button in this app, and .ix-p1 only ever lives inside a hand, so
   the button is the honest place to hang this: the next card to hold one gets
   the press without having to remember to ask for it. */
button:active .ix-p1 { transform: translateX(calc(var(--fan) * .375)) rotate(-3deg); }
button:active .ix-p2 { transform: translateX(calc(var(--fan) * .9)) rotate(0deg); }
button:active .ix-p3 { transform: translateX(calc(var(--fan) * 1.5)) rotate(3deg); }

/* A FIXED height, and it is what keeps a band rectangular. Every door is one
   column wide and one row tall, and ixFill centres a short last row on those
   same tracks - so a height derived from the contents, a wrapped count, a tall
   mark, an absent hand, would put one card out of line with the row above it
   at particular widths and particular interface sizes. That is a bug nobody
   reproduces on the machine they found it on. */
.ix-body {
  height: 3.7rem; flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column; justify-content: center; gap: .2rem;
}
/* The mark's own box. Everything about the mark's SIZE is derived from this
   height, never from the column's width - which is what lets one rule hold at
   every interface size, at every column count, and inside a cell ixFill has
   stretched, with nothing to recompute. */
.ix-well {
  position: relative; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: var(--well-h);
}
/* ---- every mark, at the same amount of ink ----

   These 46 marks run from 0.64 - Sony, taller than it is wide - to 11.32, a
   DreamWorks Pictures hairline. Given a width cap and a height cap, a mark of
   aspect r renders at W x W/r when it is wide and H*r x H when it is tall, so
   the drawn area peaks at r = W/H and falls away on both sides. Measured on
   the real 46 at the caps this replaces: 4.7x between the most and the least
   ink, with a wordmark filling the column edge to edge and Sony filling 26% of
   it. Twelve other cap pairs were evaluated against the same 46; the best of
   them managed 4.3x, and every pair that shrank the caps put MORE marks under
   60% of the column rather than fewer. Two caps cannot fix this - it is the
   shape of the function, not a number that was chosen badly.

   So the size comes from the mark's own aspect ratio instead. For a target
   area A, a mark of aspect r wants sqrt(A*r) by sqrt(A/r) - and expressed in
   units of the WELL HEIGHT, which is a rem, both of those are pure numbers
   that ixDoor can write once on load. Measured after: 1.65x, and 44 of the 46
   land on exactly the same ink. The two that cannot are Sony, which is so tall
   that the well's own height caps it, and the 11.32 hairline, which runs out
   of column - both of them the honest limit rather than a miss.

   CENTRED, and that is the other half of it. Equal ink makes a roundel and a
   wordmark the same WEIGHT; it does not make them the same WIDTH, and it
   cannot - a 1:1 mark at the same area as a 5:1 one is less than half as wide.
   Left-aligned, that difference all collects on the right-hand side of the
   card as a void, on the fourteen marks narrow enough to show it. Centred, the
   same space sits either side of every mark and reads as a margin.

   The count centres with it. A centred mark over a left-aligned number is two
   alignments in a box 100px wide, which is worse than either. */
.ix-door .ix-well { --well-h: 2.6rem; }
.ix-door .ix-mark {
  width: calc(var(--mw, 2) * var(--well-h));
  height: calc(var(--mh, .5) * var(--well-h));
  max-width: 100%; max-height: 100%;
}
/* The word is the mark's fallback, and it takes the column rather than an ink
   budget - a name is read, not weighed. */
.ix-door .ix-word { max-width: 100%; text-align: center; }

/* The number behind the door - the same number the browse screen prints back
   at the top of the page this cell opens, because both sides send the same
   filters, the same sort and the same vote floor.

   "films" and "shows", never "titles". The lists tile's "12 titles" means
   twelve things you own; this means every film TMDB holds behind this door
   above its floor. Two different facts must not wear the same word on two
   components that look this much alike - that would be a new deception
   introduced by the change that removed one.

   tabular-nums because it lines up in a column nine wide and proportional
   digits make that column wobble. One line, ellipsed: a wrapped count would
   break the fixed body height above. */
.ix-count {
  font-size: .6667rem; font-weight: 700; letter-spacing: .05em;
  line-height: 1.3; color: var(--dimmer); text-align: center;
  font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.ix-mark {
  /* contain, and it is load-bearing rather than tidy: the box is set from the
     mark's aspect and then clamped by max-width and max-height, so on the two
     marks that hit a clamp the box is no longer the mark's own shape. contain
     letterboxes inside it instead of stretching. */
  object-fit: contain;
  /* Full white is a glare source on a night dashboard, and these marks are
     already white on transparency - so this is the only dimming available
     without a filter, which never paints on the car at all. */
  opacity: .92;
}
/* ONE track for all three bands, where there were three.
   Genres and Studios each held eighteen cells at 10.5rem and Networks ten at
   12.5rem, so the three grids started at the same left edge and agreed about
   nothing after it - three widths, three cell heights, three column counts,
   under one masthead. A shared track lines all three up column for column,
   which is most of what "organised" means on a wall of doors.

   Two classes, not one. The phone block re-declares .ix-grid at (0,1,0) and
   LATER in the file, so a single-class rule here is overridden on a phone
   without saying so. .ix-doors is also what keeps the decades out of it: that
   band is a flex scroller and shares only .ix-grid.

   Sized against the mark rather than the poster. At --ui 1 and 1999px this
   lands nine columns of 203px, which gives the mark 111px against the 118 it
   had on a genre plate - so the ink a house is drawn at survives the change
   that puts three films beside it. Deliberately no px cap: capping the track
   makes the mark NARROWER at the largest interface size than at the default,
   which is the one thing an interface scale must never do. */
.ix-grid.ix-doors { grid-template-columns: repeat(auto-fill, minmax(12.5rem, 1fr)); }
/* Counted rather than auto-fitted, because eight is the one count here that
   auto-fill gets wrong: at this width it lands seven across and leaves a
   single cell alone on a second row, which reads as a mistake rather than as
   a grid. Four divides eight; the others happen to divide cleanly already
   (eighteen studios across six, ten networks across five) and twenty-three
   genres cannot divide by anything, so a ragged last row there is simply what
   an index of twenty-three looks like. */
/* ---- the decades, as one line of time ----
   Eight doors in a four-by-two grid of wide plates, each holding a short word
   hard against its left edge and a great deal of nothing after it. The words
   are a sequence and the grid was drawing them as a set - two rows, wrapping
   from 1990s to 1980s at the line break, which is the one place the sequence
   is hardest to follow.

   One row on a rule instead. The rule is the cells' own bottom border, so it
   is continuous by construction rather than by a background positioned behind
   them, and each door brightens its own segment of it - which is the whole
   affordance: you are pointing at a stretch of time.

   Oldest on the left, because that is the direction a timeline runs and the
   old order ran backwards down it. */
.ix-dec {
  grid-template-columns: none;
  display: flex; gap: 0; flex-wrap: nowrap;
  overflow-x: auto; scrollbar-width: none; overscroll-behavior-x: contain;
}
.ix-dec::-webkit-scrollbar { display: none; }
.ix-dec .ix-cell {
  flex: 1 0 auto; min-width: 0;
  background: none; border-radius: 0;
  border: 0; border-bottom: var(--rule-w) solid var(--rule);
  align-items: flex-end; justify-content: center;
  /* The 52px floor stated in the form that survives the interface scale.
     3.6rem is 49.7px at Compact, and this rule carries two classes against
     the car block's one, so the car's own .ix-cell { min-height: 3.9rem }
     never reached it - on the one band of this section that is a row of
     touch targets and has no artwork to make the cell taller than its own
     minimum. */
  min-height: max(3.6rem, 52px); padding: 0 1.1rem .7rem;
  position: relative; color: var(--dim);
  font-size: 0.9333rem; letter-spacing: 0;
  transition: color .18s, border-color .18s, transform .12s var(--ease);
}
/* The tick sits ON the rule, so the label, the tick and the line are one
   object rather than three that happen to be near each other. */
.ix-dec .ix-cell::after {
  content: ''; position: absolute; left: 50%; bottom: 0;
  width: var(--rule-w); height: .38rem; background: var(--rule);
  transform: translateX(-50%);
  transition: height .18s var(--ease), background .18s;
}
.ix-dec .ix-cell:hover, .ix-dec .ix-cell:focus-visible {
  background: none; color: #fff; border-bottom-color: #fff;
}
.ix-dec .ix-cell:hover::after, .ix-dec .ix-cell:focus-visible::after {
  height: .68rem; background: #fff;
}
.ix-dec .ix-cell:active { transform: scale(.97); }
@media (prefers-reduced-motion: reduce) {
  .ix-dec .ix-cell, .ix-dec .ix-cell::after { transition: none; }
}

/* The only centred full-width element on the whole page, which is why the
   page now reads as finished rather than as stopped. A page that states its
   own extent has ended; one that simply runs out has not. */
.ix-colophon {
  margin: 2rem 0 0; padding: 1.1rem var(--pad) 0;
  border-top: var(--rule-w) solid var(--line);
  text-align: center; font-size: .8rem; font-weight: 700;
  letter-spacing: .04em; color: var(--dimmer);
}

@media (max-width: 700px) {
  .lg-list { grid-auto-flow: row; }
  .deck { flex-direction: column; align-items: stretch; gap: .5rem; }
  .mt-logo { height: 1.8rem; max-width: 50%; }
  .dk-logo { height: 2.1rem; }
  .dk-head { flex: none; padding-right: var(--pad); }
  /* The hero body is anchored to the BOTTOM and grows upward, so on the
     smallest screen it had climbed to within 7px of the fixed header - six
     things stacked in 520px of picture. The cast row goes: it is the only one
     repeated verbatim on the title page a tap away, and a hero that reaches
     the header reads as broken rather than as full. */
  .hero .castSlotHero { display: none; }
  .ix-grid { grid-template-columns: repeat(auto-fill, minmax(7.6rem, 1fr)); }
  /* One column, and a smaller still - two 16/9 pictures side by side at 375px
     are two postage stamps. */
  /* The head was 59px of title over a 38px rail over four lines of control.
     Each number here drops one notch, which together move the first poster
     from 42% of the screen to about a quarter of it. */
  /* The word "Discover" is already on screen, lit, in the tab bar at the
     bottom - a phone does not have the room to say it twice. Dropping it here
     lets the kind switch, the count and the source rail share one line
     instead of three, which is the single biggest saving available on this
     screen and costs nothing anyone was reading. The heading stays in the
     document for screen readers. */
  .bh-word { position: absolute; width: 1px; height: 1px; overflow: hidden;
             clip-path: inset(50%); white-space: nowrap; }
  .browsehead { padding: .5rem var(--pad) .3rem; gap: .4rem .6rem; align-items: center; }
  /* The switch leads and the figure follows it. With the word gone the count
     was the first thing on the line, a bare "36,252" against the left edge
     answering a question nothing had asked yet. */
  /* No `order` here any more. Those three rules were written for the old
     head, where the h1 carried the count and the source rail sat beside it;
     against the new markup they left the count and the lens row (both
     unordered, so 0) sitting in front of the Films|Shows switch (order 1) -
     the switch appeared at the END of its own row. The DOM order is now the
     reading order, which is what it should have been. */
  .browsehead h1 { font-size: 1rem; margin: 0; }
  .browsecount {
    font-size: .7333rem; color: var(--dim);
    font-variant-numeric: tabular-nums;
  }
  /* margin-left: 0 belongs to the count that is a FLEX ITEM of the head, where
     the row's own gap already separates it. It used to be unscoped, and every
     nested library screen puts its count INSIDE the h1 instead - where that
     margin is the only thing between the word and the number. On a phone they
     ran together: "History3,841 plays". */
  .browsehead > .browsecount { margin-left: 0; }
  /* Same line as the switch and the count, scrolling within whatever is
     left. max-width:100% made it claim the whole line and wrap itself onto a
     second one; flex-basis 0 with min-width 0 lets it take the remainder and
     put its own overflow behind its own scroll, which is what it was built to
     do everywhere else. */
  .srcrail { flex: 1 1 0; min-width: 0; max-width: none; margin-left: .2rem; }
  /* Films|Shows and the count hold their place; everything to the right of
     them scrolls, so row one is one line however many controls it carries. */
  .qhead .lensrow {
    flex: 1 1 0; min-width: 0; overflow-x: auto;
    scrollbar-width: none; overscroll-behavior-x: contain;
  }
  .qhead .lensrow::-webkit-scrollbar { display: none; }
  .qhead .lensrow > * { flex: none; }
  .qh-gap { display: none; }
  .qhead { gap: .4rem .5rem; padding: .5rem var(--pad) .3rem; flex-wrap: nowrap; }
  .qtok { min-height: 2.2rem; font-size: .7667rem; }
  /* The two controls took 344 of the 351 usable pixels between them and left
     Filters stranded on a line of its own. Capped, they make room for it.

     This named `.browsefilters select` and `.fsortd` until the selects became
     tokens - and `.fsortd` matched nothing in the markup even then. */
  .fsort, .fgenre {
    min-height: 2.3rem; font-size: .8rem; padding: 0 .7rem;
    flex: 1 1 7.5rem; max-width: 8.7rem; min-width: 0;
  }
  .lensrow .chipbtn { min-height: 2.2rem; padding: 0 .7rem; }
  .ktab { min-height: 1.8rem; padding: 0 .5rem; font-size: .6333rem; letter-spacing: .05em; }
  .viewsw::before { margin-right: .25rem; }
  .srcrail { gap: .4rem; }
  .rows { grid-template-columns: 1fr; }
  .drow { grid-template-columns: 6rem minmax(0, 1fr) auto; }
  /* Two lines rather than an ellipsis. At 375px the title shares its line
     with the library pill and roughly 230px is left, which "The Shawshank
     Redemption" does not fit in - and a listing whose first job is naming the
     thing should not be the element that gives up its words first. */
  .dr-t { white-space: normal; display: -webkit-box;
          -webkit-line-clamp: 2; -webkit-box-orient: vertical; }

  /* ---- the shelf's head: one line, and every control whole ----
     Two things were wrong here and they are the same fix.

     `.qhead .lensrow` is `flex: 1 1 0`, which is right on Discover where that
     box holds a rail that must scroll. Here it holds two icon tabs and a
     divider, and a zero-basis grower is handed whatever the line has left
     after everything else. Measured at 375px: 18px of an 83px switch at
     Default and 46px of 76px at Compact, the remainder hidden inside the
     lensrow's own scrollbar-less scroller - the density switch sliced off at
     the right edge, which is what the screenshot showed. `flex: none` makes
     the box ask for its real width, and answers both sizes at once.

     The head still wraps at the largest interface size, and that is a fair
     trade there - but it is NOT this rule's doing, and the obvious reading of
     the block is wrong: `.qhead { flex-wrap: nowrap }` does not decide it.
     `.browsehead { flex-wrap: wrap }` in the SECOND max-width:700px block is
     later at equal specificity and wins. Anything written here on the
     assumption that this head cannot wrap is false.

     Asking for its real width costs the head about ten pixels it has not got,
     so the episode figure gives up the line. Hidden the way this file hides
     every line it wants read but not seen - `.bh-word` above, `.shead .srstat`
     below - so "4,941 episodes" is still in the document and still announced.
     It is the one fact no chip carries and also the only number on this screen
     you cannot act on: the three chips say 439, 265 and 174 an inch away, and
     nothing on the shelf ever turns this figure amber.

     Measured at 375px, head height: Compact 35.8 unchanged, Default 39
     unchanged, Large 82.3 -> 43.7, Largest 92.6 unchanged. Nothing overflows
     the 375 at any of the four. */
  .shelfhead .browsecount {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }
  /* flex: none only. The obvious companion change is `overflow: visible`, on
     the reasoning that a box asking for its real width has nothing left to
     scroll - and it is wrong, measured: at the Compact interface size the
     switch's contents run about four pixels past the box, and `visible` spills
     those past the screen edge instead of containing them, which gave the
     whole PAGE a sideways scroll at that one setting and no other. The
     inherited overflow-x: auto stays, and with flex: none it never actually
     engages at Default or above. It is a floor, not a feature. */
  .qhead.shelfhead .lensrow { flex: none; margin-left: auto; }
  .shelfhead .kindsw { gap: .25rem; }
  .shelfhead .ktab { padding: 0 .4rem; }
  .shelfhead .kfilt b { margin-left: .3rem; }
  /* The last 1.3px, and it is genuinely the last: with the chip trims above
     the line measured 344.4 into 343.08 at the Large interface size, so it
     wrapped for want of one and a third pixels. Taken from the space BETWEEN
     the two switches rather than from the chips, because the chips are tap
     targets and this is not - measured both ways, and trimming the chip
     padding a further .05rem does the same job by making every filter
     fractionally harder to hit.
     Neither half works alone: undo the trims above and this gap change is
     inert at every size, which is worth knowing before anyone tidies one of
     them away. */
  .qhead.shelfhead { column-gap: .3rem; }

  /* ---- a Library row at 375px ----
     `.dr-body` was 177px while `.dr-m` carries 180 to 450px of tracked
     uppercase, so nine rows in ten ended mid-word: "SHOW · ANIMATION · COMEDY
     · SC...". Sixty-four pixels come back off the furniture and the line is
     then allowed to wrap instead of ellipsing. Measured at Large: the body
     goes 176.8 -> 240.6, and the longest meta on this screen fits whole.

     The score is what moves. You own all 439 of these, so what TMDB thinks of
     a film already sitting on your disk is the one fact here that cannot
     change a decision - and it is not lost: the title page prints it one tap
     in, and the Sort token still offers Top rated, so the number can still
     ORDER the whole wall without being read a hundred times to do it. Hiding
     the column alone would leave its 14px gutter behind, so the template drops
     to two tracks - which is exact rather than lucky: shRow puts `.dr-at` and
     `.dr-held` INSIDE `.dr-body`, so `.dr-v` is the only third child a row
     ever has, and skelRow builds two children already.

     Scoped by the head that owns the screen. Discover draws the same `.drow`
     from the same `discRow`, and search's `sPaintGrid` puts class `rows` on
     its own container - on neither do you own the result, and there the score
     is the point. `.shelfhead` exists once in app.js, on this screen's head,
     so the sibling combinator cannot reach either of them.

     The last three declarations pay for the second line, and not evenly.
     Measured at Large: a film row 81.2 -> 76.1, a show row 114.7 -> 95.1, but
     a row whose TITLE also wraps goes 94.3 -> 100.8 - the long-title case is
     the one that spends six pixels to get its genre line whole. 1.35 is the
     line-height `.dr-p` already uses. */
  .shelfhead ~ .results .dr-v { display: none; }
  .shelfhead ~ .results .drow {
    grid-template-columns: 5.4rem minmax(0, 1fr);
    gap: .7rem; padding: .55rem .6rem;
  }
  .shelfhead ~ .results .dr-body { gap: .1rem; }
  .shelfhead ~ .results .dr-m {
    white-space: normal; line-height: 1.35;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  }

  .grid.wideg { grid-template-columns: 1fr; }
  /* TWO columns, counted rather than auto-filled, and that is the whole
     argument on this screen. One column is the shape the .listcard was drawn
     for and it looks better per card - 361px of row, a 267px mark - but it is
     46 rows, and measured it takes the index from 2314px to 4349. Nearly
     doubling the last section of the longest screen in the app, on the device
     held closest, is not a thing a prettier card can buy.
     Counted, because auto-fill at any track that gives two columns at --ui 1
     gives ONE at 1.26 - which is the bug the three tracks this replaces
     already had: .ix-net's 9.6rem is 181px at the largest interface size and
     will not fit twice in 361, so Networks silently collapsed to a single
     column while the two bands above it stayed at two.
     Measured at 390, --ui 1: 2273px against today's 2314. Forty pixels
     shorter, with three posters and a count added to all 46 doors. */
  .ix-grid.ix-doors { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The hand keeps the size the reader was shown on the lists screen - a
     2.27 x 3.4rem card - because that is the one place in this app where a
     poster this small is already known to read. What gives instead is the
     cell's own padding and the gap, which are air rather than information.

     And it stops growing at the interface sizes above Default, which is the
     one place this card is weaker than the plate it replaces. Two columns of
     a 390px screen is a fixed width; the hand beside the mark is not, so
     every rem the hand gains at Large and Larger comes out of the mark. Left
     alone the mark runs 97, 89, 76, 62px across the four sizes - it gets
     SMALLER as the reader asks for bigger, which is the one thing an
     interface scale must never do. Capped against the viewport it runs 97,
     89, 83, 77: still not flat, and the rest is the padding and the gap,
     which are not worth pinning to a viewport unit to recover.
     15vw is 58.5px at 390, which is what 3.87rem already measures at Default
     - so nothing moves at or below the size actually in use. */
  .ix-door { padding: .6rem; gap: .5rem; }
  .ix-hand {
    --fan: min(.8rem, 3.1vw);
    width: min(3.87rem, 15vw); height: min(3.4rem, 13.2vw);
  }
  .ix-hand img { width: min(2.27rem, 8.8vw); height: min(3.4rem, 13.2vw); }
  .ix-body { height: 3.6rem; }
  .ix-door .ix-well { --well-h: 2.5rem; }
  /* Still one line, still in order - it scrolls rather than wrapping, because
     a timeline that wraps stops being one. */
  .ix-dec { grid-template-columns: none; }
}
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .lg-r { min-height: 3.9rem; }
  .lg-t { font-size: 1rem; }
  /* 52px is the floor under a thumb in a car; these clear it and the marks
     grow with the plates rather than rattling around inside them. */
  .ix-cell { min-height: 3.9rem; font-size: 1.0667rem; }
  /* 52px under a thumb; the switch is now the only way between films and
     shows, so it is the last control on the page that may be fiddly. */
  .ktab { min-height: 3.5rem; padding: 0 1.2rem; font-size: .8rem; }
  /* The filter tokens were the only controls on this screen with no car
     size at all, sitting at the 2.35rem desktop height. */
  .qtok { min-height: max(3.5rem, 52px); padding: 0 1.1rem; }
  .drow { grid-template-columns: 9rem minmax(0, 1fr) auto; padding: .85rem .6rem; }
  .dr-t { font-size: 1.0667rem; }
  .dr-p { font-size: .8667rem; }
  /* The whole card grows, not only its mark. The three per-band wells this
     replaces existed because there were three bands with three plate sizes;
     there is one card now, and what it needs here is the same step up
     everything else on this screen gets - read from a seat, at arm's length,
     with a thumb. The count goes up a step too: .6667rem is 10px, which is a
     figure rather than a fact from a driving position. */
  .ix-body { height: 4.15rem; }
  .ix-door .ix-well { --well-h: 2.9rem; }
  .ix-hand { --fan: .9rem; width: 4.4rem; height: 3.85rem; }
  .ix-hand img { width: 2.57rem; height: 3.85rem; }
  .ix-count { font-size: .7333rem; }
  .ix-tab { min-height: 3.5rem; padding: 0 1.1rem; font-size: .8rem; }
  .ix-title { font-size: 2.1rem; }
  .dk-logo { height: 3rem; }
  .mt-logo { height: 2.5rem; }
  .sp-word { font-size: clamp(1.9rem, calc(18rem * var(--k, .22)), 4.4rem); }
}
/* The home page kept its own copy of the measure here, in two values that
   did not agree - 96rem for the lists and the index, calc(96rem + var(--pad))
   for the spine and the deck - so adjacent blocks in one column started at
   different places. The page body owns the measure. */

/* --------------------------------------------------------- person stats --
   The life as numbers, then as a shape. The figures reuse the history page's
   vocabulary - a big numeral over a small shouted label - because they are
   the same kind of fact. */
.personhead .pstats .figures { padding: .5rem 0 .2rem; gap: clamp(1.2rem, 3vw, 2.6rem); }
.personhead .pstats .fig-n { font-size: clamp(1.5rem, 2.6vw, 2.2rem); }

/* One bar per year of the career, taller where they made more. Static paint,
   no animation - it is read, not watched. The empty years matter as much as
   the full ones: a gap in the strip is a gap in the career. */
.pcareer {
  display: flex; align-items: flex-end; gap: .6rem;
  margin: .35rem 0 .55rem; max-width: 34rem;
}
.pcareer .pc-y {
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .08em;
  color: var(--dimmer); font-variant-numeric: tabular-nums;
  padding-bottom: .05rem;
}
.pcareer .pc-bars {
  flex: 1 1 auto; min-width: 0; height: 2rem;
  display: flex; align-items: flex-end; gap: 1px;
}
.pcareer .pc-bars i {
  flex: 1 1 0; min-width: 1px; border-radius: 1px 1px 0 0;
  background: rgba(255, 255, 255, .78); cursor: pointer;
}
.pcareer .pc-bars i.nil { background: rgba(255, 255, 255, .13); cursor: default; }
.pcareer .pc-bars i:hover { background: #fff; }

/* What they tend to make, sitting at the end of the facts line. */
.pfacts .pgenres { display: inline-flex; gap: .35rem; margin-left: .6rem; vertical-align: .08em; }

@media (max-width: 640px) {
  .personhead .pstats .figures { gap: 1.1rem; }
  .pcareer { max-width: 100%; }
}
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .pcareer { height: auto; }
  .pcareer .pc-bars { height: 2.5rem; }
  .pcareer .pc-y { font-size: 0.8rem; }
}

/* ------------------------------------------------------------ box office --
   What it made against what it cost. The interesting fact is the ratio, and
   a ratio is a length - two numbers side by side make you do the division. */
/* Padded by its container, not by itself.

   This said padding: .2rem var(--pad), and so did .kwrow below - but both of
   them only ever live inside .sheet, which has already applied var(--pad).
   So they were indented TWICE: the fact labels above them sat at x=41 and
   these sat at x=81, a 40px step in the middle of a column that is otherwise
   one straight edge. Measured on both the film page and the episode page,
   which have different containers and the same bug.

   The vertical padding stays and grew a little at the top: with the Box
   office fact row gone this block now follows the facts grid directly, and
   .9rem on top of the grid's .2rem margin gives it the same 1.1rem the grid
   uses between its own rows. */
.boxo { padding: .9rem 0 1.2rem; max-width: 34rem; }
.bo-k, .kw-k {
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .2em;
  text-transform: uppercase; color: var(--dimmer);
}
.bo-bar {
  margin: .45rem 0 .35rem; height: .5rem; border-radius: 99px;
  background: rgba(255, 255, 255, .1); overflow: hidden;
}
.bo-bar i { display: block; height: 100%; border-radius: 99px; background: #fff; }
.bo-l {
  display: flex; justify-content: space-between; gap: 1rem;
  font-size: 0.8333rem; font-weight: 600; color: var(--dim);
}
.bo-x { color: var(--fg); font-weight: 800; }

/* ---------------------------------------------------------------- money --
   A figure you can press to see it in riyals.

   It has to look pressable without looking like a button: these sit inside a
   sentence - "$858m on $175m" - and a pill or a border there would break the
   line into three objects. A dotted underline is the convention for "there is
   more to this word", costs no layout, and stays legible on a software
   rasteriser where a 1px solid rule can disappear.

   The padding is negative-margined back out for the same reason. A number in
   a sentence is a poor target for a finger in a moving car, so it gets a real
   one, and the negative margin means claiming that space does not shift the
   text around it by a pixel.

   inherit on font and colour deliberately: it appears in a fact grid at one
   size and under the box office bar at another, and it should look like the
   text it is part of in both. Only the underline says it is different. */
.money {
  /* Explicit, not inherited from the UA: a transform needs a box, and an
     inline one does not have the kind that translates. */
  display: inline-block;
  font: inherit; color: inherit; background: none; border: 0;
  padding: .15rem .3rem; margin: -.15rem -.3rem; border-radius: 6px;
  text-decoration: underline dotted;
  text-underline-offset: .22em;
  text-decoration-color: var(--dimmer);
  transition: background .15s, text-decoration-color .15s;
}
.money:hover, .money:focus-visible {
  background: var(--elev); text-decoration-color: var(--fg);
}
.money:active { transform: scale(.96); }
/* ---- the reel ----
   Each digit is a window one line tall onto a strip of ten, and changing the
   figure translates the strip. The digit travels through every value between
   the old one and the new one, which is the whole point: $175m and SAR 656m
   share no digits, and the swap this replaced had to be trusted rather than
   read. You watch the 1 become a 6 instead.

   Only transform moves. Nothing here changes width, height or position, so
   there is nothing for a rasteriser with no GPU to repaint.

   tabular-nums so every column is the same width as the 1ch window that holds
   it - proportional digits would make the columns jitter as they rolled, a 1
   being narrower than an 8. */
.money { font-variant-numeric: tabular-nums; }
.reel { display: inline-flex; align-items: flex-start; }
/* Symbols are inline-blocks of exactly one line too, aligned by their top.
   A clipped inline-block sits on its BOTTOM edge for baseline purposes, so
   leaving the currency mark as ordinary inline text is what makes this effect
   render with the digits a few pixels low. */
.reel-t, .reel-d { display: inline-block; height: 1em; line-height: 1em; }
/* The symbol moves with the number.
   A digit rolls because it has ten values to roll through; "$" becoming "SAR"
   has nothing to roll, so it arrives instead - lifting into place on the same
   curve, on the same stagger, as the column beside it. Without this the one
   element that changes IDENTITY rather than value was the only thing on the
   line holding still, which is what made the swap look half-animated.

   backwards, so it is held out of sight through its delay rather than
   flashing at full strength and then starting. Runs that did not change get
   animation: none from JS and never move at all. */
/* reelText is also the player's time readout, which changes identity the
   same way a currency mark does - see .player .pctrls .time.tswap. It reuses
   the keyframe and not this class, because this one carries the reel's box as
   well as its animation. */
.reel-t { animation: reelText .34s var(--ease) backwards; }
@keyframes reelText {
  from { opacity: 0; transform: translateY(.34em); }
}

/* ---- the currency mark ----
   Drawn larger than the number it belongs to. Currency signs are cut to sit
   beside running text at text weight, so at the size these figures are set
   they read as punctuation rather than as the unit - and the riyal glyph is
   the worst of them, a fine-stroked mark where "$" is at least a bold
   vertical. So the glyph gets more than the dollar does.

   A word does not: "SAR" is three legible letters already, and enlarging
   letters looks like a mistake where enlarging a symbol looks deliberate.
   moneyMark decides which it is by looking for a letter. */
/* The mark, heavier and larger than the figure it sits on.

   Currency signs are drawn to sit beside text at text weight, so at the size
   these figures are set they read as punctuation rather than as the unit - and
   the riyal is the worst of them, a fine-stroked mark where "$" is at least a
   bold vertical. Size alone was not enough for it. 700 is the top of what the
   face carries, so this is a real bold and not a smear. */
.mny-c { font-size: 1.12em; font-weight: 700; }
.mny-c.g { font-size: 1.5em; }
/* Inside a reel every child is a box exactly one line tall, and the row's
   height comes from that. A larger font-size would make this child's own 1em
   taller and push the row open, so the height is given back in the child's
   OWN em: 1.12 x .893 = 1, and 1.34 x .746 = 1. The glyph overflows its box,
   which is what we want and why nothing here clips. */
.reel .mny-c { height: .893em; line-height: .893em; }
.reel .mny-c.g { height: .746em; line-height: .746em; }
.reel-d { overflow: hidden; width: 1ch; text-align: center; }
/* The keyframe gives only a `from`; the implicit `to` is the element's own
   declared transform, which JS has already set to the final digit. So the
   column is correct the instant it exists and the roll is decoration over it -
   see the note in app.js for why that ordering is load-bearing. */
@keyframes reelRoll { from { transform: translateY(var(--from, 0%)); } }
.reel-d > b {
  display: block; font-weight: inherit;
  animation: reelRoll .52s var(--ease) backwards;
  animation-delay: var(--rd, 0s);
}
.reel-d i { display: block; height: 1em; line-height: 1em; font-style: normal; }
@media (prefers-reduced-motion: reduce) {
  /* The click handler does not build a reel at all under reduced motion. This
     is for one already on screen when the setting changes. `animation: none`
     rather than a zero duration, because it drops the DELAY too - the global
     rule zeroes durations only and would leave the stagger behind as a stall,
     with a column sitting on the old digit for a quarter second. */
  .reel-d > b, .reel-t { animation: none; }
}
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  /* Bigger where it is pressed with a thumb rather than clicked. */
  .money { padding: .3rem .5rem; margin: -.3rem -.5rem; }
}

/* ------------------------------------------------------------- keywords --
   What it is about, as against what genre it files under. */
.kwrow {
  display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
  padding: 0 0 1.3rem;
  /* The measure is on .detail .kwrow, which is where this is actually used.
     A max-width here as well was dead: (0,2,0) beats (0,1,0) at any source
     position, so the number on this rule had never once applied. */
}
/* A heading over the chips rather than the first thing in the row.

   It was inline, which meant the first row of chips started after the word
   ABOUT and every row below it started at the margin - so a set of chips that
   wrapped, which is most of them, had a ragged step in its top-left corner.
   flex-basis 100% gives the label the whole first line and lets every chip
   below it share one left edge. It also matches the labels on either side:
   BOX OFFICE sits above its bar, and now ABOUT sits above its chips. */
.kwrow .kw-k { flex: 0 0 100%; margin: 0 0 .15rem; }
.kw {
  background: var(--elev); border: .06em solid var(--line-hi); border-radius: 99px;
  padding: .28rem .7rem; font-size: 0.8rem; font-weight: 600; color: var(--dim);
  transition: background .2s, color .2s, border-color .2s;
}
.kw:hover { background: var(--elev2); color: var(--fg); border-color: var(--accent); }
.kw:active { transform: scale(.96); }

@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .kw { font-size: 0.9333rem; padding: .45rem .95rem; }
  /* Browse, at the size it is actually used. Everything here is a finger
     target on a screen read at arm's length: 3.47rem is the ~52px this app
     settled on for anything reached without looking. */
  .srcrail .chipbtn, .browsefilters .chipbtn,
  .fsec-b .chipbtn, .fsec-b select {
    min-height: 3.47rem; display: inline-flex; align-items: center;
    padding: 0 1.4rem; font-size: 1.0667rem;
  }
  .fsort, .fgenre {
    min-height: 3.47rem; font-size: 1.0667rem; padding: 0 1.1rem;
  }
  .browsecount { font-size: 1rem; color: var(--dim); }
  .sd-ft .chipbtn, .sd-ft .morebtn { min-height: 3.47rem; font-size: 1.0667rem; }
  .fsec-h { font-size: 0.8rem; }
  /* The facts cell grows with everything else here, so its fixed box has to
     grow with it: 1.0333rem at 1.4 is two lines of 2.89rem. */
  .facts-grid dd { font-size: 1.0333rem; }
  .facts-grid dd { height: 2.89rem; }
  .facts-grid .ffig { font-size: 1.75rem; }
  .facts-grid .fq { font-size: 0.8rem; }
  .facts-grid .fchip { font-size: 0.8667rem; padding: .28rem .75rem; }
  .facts-grid .fmore { font-size: 0.8rem; }
  .facts-grid .mark-i { height: 1.35rem; }
  /* The chip grows with the cell, and the hit area grows past it: the cell is
     2.89rem here and the chip about two thirds of that, so the pseudo takes
     the rest of the row. It cannot reach 52px without making the whole facts
     grid taller, which is the one thing the fixed cell exists to prevent -
     this is the largest target available inside that rule. */
  .facts-grid .mark-t { font-size: 0.8rem; }
  .facts-grid .mark { padding: .26rem .6rem; }
  .facts-grid .mark-art { padding: .24rem .5rem; }
  .facts-grid .mark-b::after { inset: -.85rem -.25rem; }
  .bo-l { font-size: 0.9667rem; }
  .bo-k, .kw-k { font-size: 0.8rem; }
}

/* =========================================================== the phone ==
 * The car is the screen this app is designed around, but it is not the only
 * one it runs on, and the header made that obvious: five tabs, a search
 * field and two icon buttons need 572px of content width and a phone gives
 * 364px. Because the header is position:fixed the overflow never produced a
 * scrollbar - the Full screen and Refresh buttons were simply painted past
 * the right edge on every screen, and the search field was crushed to a
 * 27px stub with a zero-width input. It looked fine and half of it was gone.
 *
 * No amount of tightening fixes that: five tabs plus back, search and two
 * actions is 396px of forty-four-pixel targets before a single gap. So on a
 * phone the tabs leave the header entirely and become a bottom bar, which is
 * where a thumb is anyway. The .tabs wrapper is display:contents everywhere
 * else, so the car and the desktop keep exactly the header they had.
 */
/* A real box now, and position: relative, because that is what the shared pill
   measures against - slidePill writes translate(offsetLeft, offsetTop) and
   those are relative to the nearest positioned ancestor. As display: contents
   the nav generated no box at all, so the pill would have measured against
   .top and travelled to the wrong place.
   The gap is small on purpose: each tab carries its own .6rem of padding, and
   Apple's row reads as one object with divisions rather than as five separate
   controls. */
.top .tabs {
  display: inline-flex; align-items: center; gap: .1rem;
  position: relative; flex: none;
}
/* Every tab keeps its word, not only the selected one.
   This is what makes the slide possible rather than merely prettier. While the
   label belonged to .on alone, the row's geometry was a function of the
   SELECTION - press a tab and the old one shrank by a word while the new one
   grew by one - so the pill's destination moved while the pill was travelling
   to it. Labelled throughout, the row is a fixed object and the pill crosses a
   stationary distance.
   Measured on the car at 1307px before committing to it: the five labelled
   tabs are 529px at --ui 1, 593 at 1.12 and 667 at 1.26, against 1213px of
   available bar - so the widest case still leaves 459px for the search field.
   The phone is untouched: there the nav is moved out to .screen and this
   selector stops matching. */
.top .navtab .nt-l { display: inline; }
/* Above the pill, which is the ground now. */
.top .haspill .navtab { z-index: 1; }
/* Search keeps its word for the phone's bottom bar and gives it up in the
   header. The word exists because down there every other tab has one and an
   icon alone sat six pixels low against them - the labelled tabs stack icon
   over word and a lone icon centres itself. Up here the magnifier says it
   without help, and five words plus a sixth is a row that stops fitting the
   car at the larger interface sizes. */
.top .ntsearch .nt-l { display: none; }
/* And no lozenge at all on the screens that are not a section - see markTab.
   Opacity rather than display, so leaving a tab fades the pill out where it
   stood instead of making it vanish mid-slide. */
.tabs.nopill .tabpill { opacity: 0; }

@media (max-width: 700px) {
  /* Nothing is left in it. The tabs are a bar at the foot of the screen, the
     search field is a tab, refresh is a corner button and so is back - so on a
     phone the header capsule held nothing but its own ground, and drew as a
     small dark blob in the middle of the artwork. It is not hidden because it
     is unwanted; it is hidden because it is empty. */
  .screen > .top { display: none; }
  /* And nothing has to clear it either. --clear reserves the height of the
     header plus a gap, which on a phone was 70px of black held open above the
     first row for a bar that is not drawn - most of a thumb's worth of screen,
     on the device with the least of it. Measured on a 390x844 phone: the
     library's filter pills started 70.4px down a screen whose top was empty,
     and start at 8.3px now. */
  :root { --clear: calc(.6rem + env(safe-area-inset-top, 0px)); }
  /* Except where the corner back button is, which is the one thing still
     floating up there. Selected off the button itself rather than off a class
     on the page, because chrome() puts it in the DOM ahead of the body on
     exactly the screens that have one. Measured: the button ends at 49px and
     the content starts at 71px. */
  .screen > .topback ~ .body-clear {
    padding-top: calc(4.15rem + env(safe-area-inset-top, 0px));
  }
  /* ---- the header keeps only what a phone needs up top ---- */
  .top { gap: .45rem; padding-top: calc(.7rem + env(safe-area-inset-top, 0px)); }
  /* Full screen is a Tesla-only trick - the car will not honour
     requestFullscreen, so that button leaves through YouTube and comes back.
     On a phone the player's own control does it properly, and this would
     just bounce you to youtube.com. */
  .top [data-go="bigscreen"] { display: none; }

  /* ---- tabs, at the bottom, where the thumb is ---- */
  /* How much of the bottom edge the tab bar owns. Anything else that fixes
     itself to that edge - the corrections commit bar is the only one today -
     reads this instead of guessing, so the two cannot end up stacked. It is
     0 everywhere except here, which is the only place the tab bar exists.
     Measured at 61px on a 390x844 phone; stated in rem so it tracks --ui. */
  :root { --tabsH: calc(3.65rem + env(safe-area-inset-bottom, 0px)); }
  /* A bar across the bottom, not a capsule.
     The capsule was tried and taken back out: at the top it floats on the
     artwork and the picture runs past it on both sides, which is the whole
     effect - down here it is a thumb rest, it is pressed without looking, and
     a centred shape with 51px of live picture either side of it turns a
     full-width target into six small ones with gaps that do nothing.
     What it keeps from the capsule is the GROUND. .92 flat black was chrome
     bolted to the screen; this is the same glass as the header, so the two
     read as one system and the picture is still visible through the bar. */
  .screen > .tabs {
    display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-tabs);
    background: rgba(0, 0, 0, .62);
    backdrop-filter: var(--glass);
    -webkit-backdrop-filter: var(--glass);
    /* A hairline of light rather than a line of dark. Over a bright frame a
       dark border disappears into the bar and the bar's top edge disappears
       into the picture; white at .1 separates in both directions. */
    border-top: 1px solid rgba(255, 255, 255, .1);
    padding: .3rem .2rem calc(.3rem + env(safe-area-inset-bottom, 0px));
  }
  /* Scoped to the moved nav, not to .top - once it is out of the header the
     header's own tab rules stop matching it, so this styles it whole. */
  .screen > .tabs .navtab {
    /* Equal shares, which is what a full-width bar wants: six targets of the
       same size, each reaching the edge of its neighbour, so there is no gap
       between them that does nothing when pressed. */
    flex: 1 1 0; min-width: 0; margin: 0; padding: .4rem .2rem .35rem;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: .2rem; border-radius: var(--r-s); border: 0;
    min-height: 2.9rem; background: none; color: var(--dimmer);
    letter-spacing: normal; text-transform: none;
  }
  .screen > .tabs .nt-i { display: grid; place-items: center; }
  .screen > .tabs .nt-i svg { width: 1.45rem; height: 1.45rem; display: block; }
  /* Every label shows here. The header hid all but the active one because it
     was short of room; a bar has room, and an unlabelled row of five glyphs
     is a guessing game. */
  .screen > .tabs .nt-l,
  .screen > .tabs .navtab.on .nt-l {
    display: block; font-size: 0.6rem; font-weight: 700; letter-spacing: .02em;
    line-height: 1; text-transform: none; animation: none;
  }
  .screen > .tabs .navtab.on { color: #fff; background: none; }
  .screen > .tabs .navtab.on .nt-i { animation: none; }
  /* Where you are, said twice. The bar marked it by turning one of six glyphs
     from grey to white, which is a difference you have to look for on a phone
     held at arm's length in daylight - and the top capsule gets a whole white
     lozenge for the same job. This is the same idea at the size a bar allows:
     a soft disc behind the icon alone, so the current section has a shape and
     not only a brightness.
     Behind the ICON rather than the whole tab, because a tab-sized ground on
     an equal sixth of the screen reads as a pressed button rather than as a
     place. */
  .screen > .tabs .nt-i {
    border-radius: 99px; padding: .3rem .7rem;
    transition: background .2s var(--ease);
  }
  .screen > .tabs .navtab.on .nt-i { background: rgba(255, 255, 255, .14); }
  /* The moving pill does not belong on a bar this size - it would be a white
     slab under a thumb. The active tab is simply the bright one. */
  .screen > .tabs .navtab.on::before { display: none; }
  /* And no travelling lozenge down here either. The bottom bar marks the
     current section by lighting the icon white, which is the platform idiom a
     thumb expects, and a pill sliding along the foot of the screen would be a
     second answer to a question already answered. */
  .screen > .tabs .tabpill { display: none; }
  .screen > .tabs .navtab:active { transform: scale(.92); }

  /* This row was laying out 428px wide on a 375px screen and taking the whole
     page with it - on the library screen it is five chips, on browse it is
     two filter tokens, and neither wrapped. */
  .browsehead { flex-wrap: wrap; row-gap: .5rem; }
  .browsefilters { margin-left: 0; width: 100%; flex-wrap: wrap; row-gap: .45rem; }

  /* Room for the bar, so the last row of anything is not under it. */
  .screen { padding-bottom: calc(3.9rem + env(safe-area-inset-bottom, 0px)); }
  /* The player covers the bar completely, so it must clear it too. */
  .player .pbottom { padding-bottom: calc(.9rem + env(safe-area-inset-bottom, 0px)); }

  /* ---- the player transport ----
     Sized once for a dashboard and never revisited: at 390px the 3.25rem
     circles were shrinking to 24px-wide ellipses and the time readout was
     wrapping onto three lines, which pushed the control bar to 154px and
     put the menu behind it. */
  .player .pctrls { gap: .3rem; }
  .player .pctrls .iconbtn { width: 2.7rem; height: 2.7rem; flex: none; }
  .player .pctrls .iconbtn.big { width: 3.4rem; height: 3.4rem; }
  .player .pctrls .iconbtn svg { width: 1.25rem; height: 1.25rem; }
  .player .ptop { padding-top: calc(.85rem + env(safe-area-inset-top, 0px)); }
  /* A finger needs more than a six-pixel bar to catch. */
  /* A finger needs more than a six-pixel bar to catch. */
  .player .scrubwrap { padding: 2.05rem 0 1.5rem; }
}

/* ---- fixes that are not phone-only ----
   Each of these is wrong everywhere; the phone is just where it shows. */

/* The readout wrapped because the string has spaces around its separator and
   nothing forbade a break. Three lines of clock inflated the whole bar. */
.player .pctrls .time { white-space: nowrap; }

/* ---- the time readout is a control ----
   A number set in a line of text is a poor target for a finger in a moving
   car, and this number has the +10s button on one side of it, where a miss
   jumps the film. The padding claims a real hit box and the negative margin
   hands the space straight back to the flex line, so the row height, --pbH
   and every button beside it stay where they were - measured identical at all
   four interface sizes.

   The floor is a pixel value on purpose. .8rem of padding reads 52.9px at an
   interface size of 1.12 and 47.3px at the default 1.0, which is exactly the
   by-product the .player .mrow rule warns about: the number has to be the
   rule, not whatever the font happened to report. max() lets the padding grow
   with the text above a 20px root and stops it shrinking under the target
   below one, and because the negative margin is the same expression the row
   never notices either way. min-height would have been the house idiom and
   still missed, at 48px, on the smallest interface size.

   Not align-self: stretch, which would give the full row height for nothing:
   display flex or grid on this element turns its text runs into anonymous
   items and eats the spaces around <em>, so "0:36 / 20:28" sets as
   "0:36/20:28". Padding leaves inline layout alone.

   No reset here - the button rule near the top of this file already strips
   font, colour, border, background and padding, and touch-action:
   manipulation arrives from .player, which the <span> this replaced never
   got. The dotted underline is the only thing saying it is pressable, and it
   is the one .money uses on the title page, so it is a sign the reader has
   already been taught. Deliberately not .money's hover background: a filled
   box over video reads as chrome, and there is no hover in the car.

   No min-width, and none is needed. Measured at 1200: .pgrow sits between
   this and the right-hand cluster and absorbs the whole delta, so .fs and
   .speed land on the same pixel in all four strings. On the phone .pgrow is
   already zero and the row already overflows its box by a hundred pixels
   before anything is pressed, so the mode change slides .speed about eleven
   pixels; "left" is the narrower of the two and makes that overflow smaller,
   not larger. A min-width would buy nothing and push this element's own text
   off its own left edge. */
.player .pctrls .time {
  --tpad: max(.8rem, 16px);
  /* Vertical only. The height is what was missing - the string is already
     about eighty pixels wide, so horizontal padding buys a target almost
     nothing and spends the one thing worth keeping: a quarter-rem each side
     ate the gap to the +10s button from 5.03px down to 0.84px, and that
     button is the reason the comment above says a miss is expensive.

     Longhands, and margin-left is deliberately not among them. There are two
     of those in force above - .3rem at 2296 and .6rem at 2307 - and the
     shorthand cannot leave them alone: `margin: X 0` writes zero into both
     sides and drags the text left by a third of a rem on the phone. Naming a
     value here instead is no better, because one value cannot be both. Left
     untouched, both stand and the glyphs do not move at all: measured d 0.00
     on the separator's left edge, and d 0.00 on row height, .speed and .fs. */
  padding: var(--tpad) 0;
  margin-top: calc(var(--tpad) * -1);
  margin-bottom: calc(var(--tpad) * -1);
  border-radius: 6px;
  text-decoration: underline dotted;
  text-underline-offset: .22em;
  text-decoration-color: var(--dimmer);
}
.player .pctrls .time:active { transform: scale(.96); }

/* The currency reel's keyframe, on a class of this element's own.
   NOT .reel-t. That class carries the reel's geometry as well as its
   animation - inline-block, height 1em, line-height 1em - which is right for
   a one-line digit window and ruinous here: with border-box sizing a 1em
   height under this padding clamps the content box to nothing. Measured, the
   hit box halves from 52.9px to 26.9px and the glyphs end up hanging four and
   a half pixels below the button they are inside, permanently, from the first
   press. The animation is worth sharing; the box is not.

   The keyframe supplies only `from`, so its implicit `to` is this element's
   own declared state and the new text is correct the instant it exists,
   whether or not anything animates. That is the property being reused: the
   money reel needed a frame handed back through requestAnimationFrame and sat
   on the old digit whenever the tab was backgrounded, and the player is the
   tab a car backgrounds when navigation takes the screen. */
.player .pctrls .time.tswap { animation: reelText .34s var(--ease) backwards; }
@media (prefers-reduced-motion: reduce) {
  /* The global rules already zero this animation's duration and, further down
     this file, every delay - so there is no stagger left to strand. This is
     for the one frame `backwards` would still hold at opacity 0 before a
     0.01ms run: the readout should change, not blink. */
  .player .pctrls .time.tswap { animation: none; }
}

/* A long title pushed the top bar to two and three lines. */
.player .ptop .pt { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Every other overlay reads the control bar's measured height; this one
   carried a constant, so the subtitle and quality menu sat behind the bar
   the moment the bar was taller than 4.3rem. */
.player .pmenu { bottom: calc(var(--pbH, 7.5rem) + .6rem); }
/* Double-tap to seek is also double-tap to zoom on iOS unless this says
   otherwise. Pinch zoom elsewhere in the app is untouched. */
.player { touch-action: manipulation; }
/* The season rail already contains its overscroll so a sideways flick cannot
   fire the browser's back gesture. Every other rail wants the same. */
.track, .rail .track, .castrail, .castchips { overscroll-behavior-x: contain; }
/* iOS zooms the page when a focused input's text is under 16px, and then
   leaves it zoomed. This has to be a literal pixel value: the interface-size
   setting can put a rem below 16px on its own. */
/* Sixteen is the number iOS decides not to zoom for. It has to be a literal
   pixel value - the interface-size setting can put a rem under 16 on its own
   - and it has to out-rank the fields' own type rules, which are set by
   descendant selectors and would otherwise win. Zoom is already off two
   other ways; this removes the reason it would ever be attempted. */
input, select, textarea,
.settings .set-key { font-size: max(16px, 1em); }

/* Phone landscape - about 852x393 - matches neither the car block (which
   asks for 600px of height) nor the phone width blocks, so it was falling
   through to desktop sizing on a screen 393px tall. The hero alone filled
   100.1% of it, which put everything else below the fold. */
@media (max-height: 500px) {
  .heroSlot .hero { height: clamp(14rem, 72vh, 22rem); }
  .strmdlg { max-height: 90vh; }
  .player .ptop { padding-top: calc(.5rem + env(safe-area-inset-top, 0px)); padding-bottom: 1.2rem; }
  .player .pbottom { padding-top: 1.2rem; }
  .player .pctrls .iconbtn { width: 2.5rem; height: 2.5rem; }
  .player .pctrls .iconbtn.big { width: 3rem; height: 3rem; }
}

/* ---- the Continue watching dismiss ----
   Small, dark, top-right, and only ever on a resume card. It has to be
   reachable with a thumb without being the thing a thumb lands on by
   accident, so it sits in the corner the card's own tap target does not
   need and gets a wider hit area than it draws.

   That last clause was aspirational until now. The rule below is 1.6rem
   square with place-items: center, no padding, no negative margin and no
   pseudo-element - so the drawn disc WAS the entire hit area, and it
   measured 31.9px on a touch screen and 37.0px in the car against this
   app's 52px floor. The extension is real now and lives with the coarse
   pointers below. */
.card .cardx {
  position: absolute; top: var(--mark-inset, .4rem); right: var(--mark-inset, .4rem); z-index: 3;
  width: 1.6rem; height: 1.6rem; border-radius: 99px;
  display: grid; place-items: center;
  background: rgba(0,0,0,.62); color: #fff; border: 0;
  opacity: 0; transition: opacity .18s var(--ease), background .18s, transform .15s var(--ease);
}
.card .cardx svg { width: .85rem; height: .85rem; display: block; }
/* Three dots need more room than a cross to read as three dots rather than a
   smudge, so the glyph goes up a size inside the same disc - .95rem, which is
   what the episode grid's ••• already draws. The disc itself does not change,
   because the hit area above was measured against it. */
.card .cardmore svg { width: .95rem; height: .95rem; }
.card:hover .cardx, .card:focus-within .cardx { opacity: 1; }
.card .cardx:hover { background: rgba(0,0,0,.85); }
.card .cardx:active { transform: scale(.9); }
.card .cardx:disabled { opacity: .5; }
/* Nothing hovers on a touch screen, so there it is simply always visible. */
/* The hit area, finally. Transparent, centred on the disc, and .6rem out on
   every side: 1.9rem + 1.2rem = 52.1px where the phone draws it, 2.2 + 1.2 =
   57.1px where the car does. Both clear the floor without either disc
   growing, which is the point - a bigger black circle on the corner of every
   resume card is exactly the "thing a thumb lands on by accident" the note
   above is trying to avoid.

   Coarse pointers only, deliberately. With a mouse the button is opacity 0
   until the card is hovered, and an invisible element still takes clicks - so
   extending it there would put a dead zone on the corner of every resume card
   for someone who has not hovered yet. Where the pointer is coarse the phone
   rule below has already set opacity 1, so the thing being enlarged is
   something the reader can see. */
@media (pointer: coarse) {
  .card .cardx::after { content: ''; position: absolute; inset: -.6rem; }
}

@media (hover: none) {
  .card .cardx { opacity: 1; background: rgba(0,0,0,.55); }
  .card .cardx { width: 1.9rem; height: 1.9rem; }
}
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .card .cardx { width: 2.2rem; height: 2.2rem; top: .4rem; right: .4rem; }
  .card .cardx svg { width: 1.05rem; height: 1.05rem; }
  .card .cardmore svg { width: 1.2rem; height: 1.2rem; }
}

/* ---- where you got to, on the episode page ----
   A bar you can read the length of, the numbers spelled out beside it, and
   the way out. It sits under the buttons rather than among them because it
   is a statement with an action attached, not a fourth thing to press. */
.detail .progrow {
  display: flex; flex-direction: column; gap: .4rem;
  margin: .8rem 0 .2rem; max-width: 26rem;
}
/* wrap, and a text that can shrink. Both children were flex: none, so neither
   could give way and the pair simply pushed the page wider than the screen -
   measured at 375px, the document scrolled to 429. The sentence is the half
   that can afford to lose a few characters, so it shrinks and ellipses; the
   button keeps its size and drops to its own line if it has to. */
.detail .pg-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: .5rem .8rem; flex-wrap: wrap;
}
/* The bar gets its own line and the full measure. Sharing a row with the
   numbers and the button left it a sliver, which is not a bar - the length
   of the thing IS the information here. */
.detail .pg-bar {
  width: 100%; height: .45rem; border-radius: 99px;
  background: rgba(255,255,255,.16); overflow: hidden;
  box-shadow: inset 0 0 0 .05rem rgba(255,255,255,.08);
}
.detail .pg-bar i {
  display: block; height: 100%; border-radius: 99px; background: #fff;
  transform: scaleX(var(--p, 0)); transform-origin: left;
}
.detail .pg-t {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 0.8rem; font-weight: 700; color: var(--dim);
  font-variant-numeric: tabular-nums;
}
.detail .pg-x {
  flex: none; border: 0; border-radius: 99px; cursor: pointer;
  padding: .35rem .8rem; font-size: 0.7333rem; font-weight: 700;
  background: var(--elev); color: var(--dim);
  transition: background .2s, color .2s, transform .15s var(--ease);
}
.detail .pg-x:hover { background: var(--elev2); color: var(--fg); }
.detail .pg-x:active { transform: scale(.96); }
.detail .pg-x:disabled { opacity: .5; }
@media (max-width: 700px) {
  .detail .progrow { max-width: 100%; }
  .detail .pg-t { font-size: 0.8667rem; }
  .detail .pg-x { padding: .45rem .9rem; font-size: 0.8rem; }
}
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .detail .pg-bar { height: .55rem; }
  .detail .pg-t { font-size: 0.9333rem; }
  .detail .pg-x { padding: .5rem 1rem; font-size: 0.8667rem; }
}

/* ---- the episode sheet ----
   An episode page used to end at its buttons. Everything here was already in
   the payload it fetches; it was just never drawn. */
/* The text blocks of the episode sheet. .epExtra is deliberately NOT here: it
   holds horizontally scrolling rails - the stills, the clips - and capping
   them made the same rail 1008px on an episode page and 1338px on a film page
   at the same 1440 window. The rule at the top of this file already draws that
   line for the title sheet; this is the episode sheet obeying it too.
   .epCast USED to be exempt for the same reason and no longer is: it holds a
   grid now rather than a rail, and without the cap that grid stretched to the
   full window.
   .epRev stays because .revBlock caps itself at 62ch inside it either way. */
.detail .epFacts, .detail .epNav, .detail .epRev, .detail .epCast { max-width: 60rem; }

.detail .epFacts .facts-grid { max-width: 54rem; }
.detail .kwrow .kwlink { text-decoration: none; }
.detail .kwrow .kwlink:hover { border-color: var(--accent); color: var(--fg); }

.detail .epseason {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: .75rem 0 .2rem;
}
.detail .es-k {
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .2em;
  text-transform: uppercase; color: var(--dimmer);
}
.detail .es-v { font-size: 0.8667rem; font-weight: 700; color: var(--fg); }
/* The one fact you actually want at the end of an episode. */
.detail .es-next {
  padding: .2rem .6rem; border-radius: 99px; background: var(--elev2);
  font-size: 0.7333rem; font-weight: 800; letter-spacing: .04em; color: var(--fg);
}
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .detail .es-v { font-size: 1.0667rem; }
  .detail .es-k, .detail .es-next { font-size: 0.8667rem; }
}

/* ---- position, and the episodes either side ---- */
.detail .epwhere {
  display: flex; align-items: center; gap: .6rem;
  font-size: 0.8667rem; font-weight: 700; color: var(--dim); padding-top: .6rem;
}
.detail .epsides {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: .8rem; margin: .7rem 0 .2rem; max-width: 40rem;
}
.detail .epside {
  display: flex; flex-direction: column; gap: .35rem; text-align: left;
  background: none; border: 0; padding: 0; color: var(--fg);
  transition: transform .15s var(--ease);
}
.detail .epside:active { transform: scale(.985); }
.detail .epside .epc-art { width: 100%; aspect-ratio: 16/9; border-radius: var(--r-s); background: var(--bg2); }
.detail .epside .epc-still { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.detail .es-lab {
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .2em;
  text-transform: uppercase; color: var(--dimmer);
}
.detail .es-t {
  font-size: 0.8rem; font-weight: 700; line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* ---- the score, and the season's shape around it ----
   The sparkline is the densest thing on the page: one bar per episode,
   height by score, this one lit. Static paint - it is read, not watched. */
.detail .eprate { margin: 1rem 0 .3rem; max-width: 34rem; }
.detail .er-head { display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap; }
.detail .er-n { font-size: 1.6rem; font-weight: 800; letter-spacing: -.02em; }
.detail .er-s { font-size: 0.8rem; font-weight: 600; color: var(--dim); }
.detail .er-spark {
  display: flex; align-items: flex-end; gap: 2px; height: 2.2rem; margin-top: .5rem;
}
.detail .er-spark i {
  flex: 1 1 0; min-width: 2px; border-radius: 1px 1px 0 0;
  background: rgba(255,255,255,.24);
}
.detail .er-spark i.here { background: #fff; }

/* ---- what the runtime is actually made of ---- */
/* ---- the hour, drawn to scale ----
   Where the titles are, where the credits start, and how much of the hour is
   the episode. Its own names: .epmark and .em-* are the provider-scores strip
   further down this page and restyling them would have moved that too.
   No hue. The bar says skippable against story by HEIGHT and by weight, which
   is the same answer this palette gives everywhere else - white is the
   accent and there is no second one to reach for. */
.detail .ephour { margin: 1.4rem 0 .2rem; max-width: 44rem; }
.detail .eh-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  padding-bottom: 1.2rem;
}
.detail .eh-lab {
  font-size: .7333rem; font-weight: 800; letter-spacing: .18em;
  text-transform: uppercase; color: var(--dim);
}
.detail .eh-sum { font-size: .8rem; color: var(--dimmer); font-variant-numeric: tabular-nums; }
/* The whole runtime is the hairline; everything else is placed along it as a
   percentage, so the bar is the episode and not an arbitrary width. */
.detail .eh-line { position: relative; height: 2.9rem; }
.detail .eh-line::before {
  content: ''; position: absolute; left: 0; right: 0; top: 1.35rem;
  height: 2px; background: var(--line);
}
.detail .eh-story {
  position: absolute; left: 0; top: 1.35rem; height: 2px; background: var(--fg);
}
/* Taller than the line and solid, because a block sitting ON the bar is the
   shape of a stretch you can skip. */
.detail .eh-band {
  position: absolute; top: .95rem; height: .8rem; background: var(--fg);
}
.detail .eh-recap { background: var(--dim); }
.detail .eh-mark {
  position: absolute; top: .75rem; width: 1px; height: 1.2rem; background: var(--dim);
}
.detail .eh-t {
  position: absolute; top: 2rem; display: flex; align-items: baseline; gap: .4rem;
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
.detail .eh-t.eh-end { transform: translateX(-100%); }
.detail .eh-t b {
  font-size: .6667rem; font-weight: 700; letter-spacing: .16em;
  text-transform: uppercase; color: var(--dim);
}
.detail .eh-t em { font-style: normal; font-size: .7333rem; color: var(--dimmer); }
/* A phone has no room for two captions under one bar, so the second one goes
   and the bar still says where things are. */
@media (max-width: 700px) {
  .detail .eh-t em { display: none; }
}
.detail .epmark {
  display: flex; flex-wrap: wrap; gap: .5rem .9rem;
  margin: .9rem 0 .3rem; padding-top: .8rem; border-top: 1px solid var(--line);
  max-width: 40rem;
}
.detail .em-i { display: flex; flex-direction: column; gap: .1rem; }
.detail .em-k {
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .16em;
  text-transform: uppercase; color: var(--dimmer);
}
.detail .em-v { font-size: 0.8667rem; font-weight: 700; font-variant-numeric: tabular-nums; }
/* ---- the provider strip ----
   .epmark's shape, spread wider: the episode page shows four marks and this
   shows up to nine, so it gets the full measure and a little more air between
   them. Everything else - the label, the figure, the rule above - is the same
   component, on purpose. */
.detail .scores { max-width: none; gap: .6rem 1.6rem; }
/* The denominator is context, not a reading. Held well back so the eye lands
   on the 8.8 and picks up the /10 only if it wants it - which is also what
   keeps IMDb's 8.8 and Metacritic's 67 from looking like the same kind of
   number. */
.detail .sc-of { font-weight: 600; color: var(--dimmer); margin-left: .1em; }
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .detail .er-n { font-size: 2rem; }
  .detail .er-s, .detail .em-v, .detail .es-t { font-size: 1rem; }
  .detail .er-spark { height: 2.8rem; }
}

/* ---- clips ----
   The stills strip that stood beside this is gone: the hero's pile is the same
   set at a size worth looking at, so the row below was the same pictures said
   twice. Its .esrow and .eshot rules went with it. */
.detail .epclips { margin: 1rem 0 .2rem; }
/* Clips are cards, not chips, so this is a scrolling band rather than a
   wrapping bag of pills.
   Full bleed, the same pull the cast and similar rails take - the arithmetic,
   the three measured gutters, and why --pad being viewport-relative is what
   makes it exact, are written out once on .detail .castrail. Longhands here
   because this rule already writes its padding as longhands, and dropping a
   shorthand in would reset the two it does write.
   The "Clips" label is a sibling in .epclips, which carries margin and no
   padding, so it stays at the sheet's gutter where it belongs.
   No scroll-padding: this row does not declare scroll-snap-type and .clipc
   carries no scroll-snap-align, so there is no snap position that could hide
   under the gutter. */
.detail .ecrow {
  display: flex; gap: .6rem; overflow-x: auto; scrollbar-width: none;
  overscroll-behavior-x: contain; padding-top: .45rem; padding-bottom: .3rem;
  padding-left: var(--pad); padding-right: var(--pad);
  margin-inline: calc(var(--pad) * -1);
}
.detail .ecrow::-webkit-scrollbar { display: none; }
.clipc {
  flex: none; width: 18rem; display: flex; flex-direction: column; gap: .4rem;
  text-decoration: none; color: var(--fg);
  transition: transform .15s var(--ease);
}
.clipc:active { transform: scale(.985); }
/* It was an <a> and is a <button> now, because it opens a sheet rather than
   leaving. A button brings a UA background, border, font and centred text
   that an anchor did not. */
.clipc { background: none; border: 0; padding: 0; font: inherit; text-align: left; }

/* ---- the clip sheet ----
   The app's own player for a fifteen-second thing. Built on .strmwrap so it
   inherits the flat backdrop, the sheetup/sheetdown motion, Escape and the
   two height clamps - no filter anywhere, transform and opacity only. */
.clipdlg { width: min(58rem, 100%); }
.clipbox {
  position: relative; width: 100%; aspect-ratio: 16 / 9;
  background: #000; border-radius: var(--r-s); overflow: hidden;
}
.clipv { width: 100%; height: 100%; display: block; background: #000; }
.clipwait {
  position: absolute; inset: 0; display: grid; place-items: center;
  gap: .6rem; grid-auto-flow: row; color: var(--dim); font-size: .82rem;
  background: rgba(0, 0, 0, .45);
}
.clipfoot {
  display: flex; align-items: center; gap: .7rem;
  padding: .7rem var(--pad) calc(.7rem + env(safe-area-inset-bottom, 0px));
}
.clipnote { flex: 1 1 auto; min-width: 0; font-size: .8rem; color: var(--dim); }
/* The way out, and it is always there rather than appearing on a failure: the
   resolver depends on yt-dlp keeping up with YouTube, and a door you can see
   before you need it is worth more than one that arrives with an apology. */
.clipfoot .clipout { flex: none; min-height: max(3.2rem, 52px); }
.clipc-art {
  position: relative; width: 100%; aspect-ratio: 16 / 9;
  border-radius: var(--r-s); overflow: hidden; background: var(--bg2);
}
.clipc-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* It is a video, and a still of a video looks exactly like a still. The badge
   is the only thing separating this row from the one above it at a glance. */
.clipc-play {
  position: absolute; left: .45rem; bottom: .45rem;
  width: 1.8rem; height: 1.8rem; border-radius: 99px;
  display: grid; place-items: center;
  background: rgba(0,0,0,.62); color: #fff;
  box-shadow: inset 0 0 0 .07em rgba(255,255,255,.35);
}
.clipc-play svg { width: .85rem; height: .85rem; margin-left: .08rem; }
.clipc-t {
  font-size: 0.8rem; font-weight: 700; line-height: 1.3; color: var(--dim);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.clipc:hover .clipc-t, .clipc:focus-visible .clipc-t { color: var(--fg); }
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .clipc { width: 21rem; }
  .clipc-t { font-size: 0.9333rem; }
  .clipc-play { width: 2.2rem; height: 2.2rem; }
  .clipc-play svg { width: 1.05rem; height: 1.05rem; }
}
@media (max-width: 640px) { .clipc { width: 14rem; } }
.detail .es-seen {
  padding: .2rem .6rem; border-radius: 99px; background: var(--elev);
  font-size: 0.7333rem; font-weight: 700; color: var(--dim);
}
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .detail .es-seen { font-size: 0.8667rem; }
}

/* ---- confirm, and the work it is doing ----
   Built on the stream dialog's overlay so it arrives the same way everything
   else in this app does. */
.cfmwrap { display: grid; place-items: center; padding: var(--pad); }
.cfmdlg {
  width: min(26rem, 100%); background: var(--panel); color: var(--fg);
  border: 1px solid var(--line-hi); border-radius: var(--r-l);
  padding: 1.15rem 1.15rem 1rem; box-shadow: var(--shadow-lg);
  animation: rise .2s var(--ease);
}
.cfm-t { font-size: 1.0667rem; font-weight: 800; letter-spacing: -.01em; }
.cfm-b { margin-top: .4rem; font-size: 0.8667rem; line-height: 1.5; color: var(--dim); }
.cfm-err {
  margin-top: .6rem; font-size: 0.8rem; font-weight: 600; color: var(--fg);
  background: var(--elev2); border-radius: var(--r-s); padding: .5rem .65rem;
}
/* Wraps, because the third button carries a sentence rather than a word -
   "Mark 6 before it too" beside Cancel and Mark watched does not fit a phone
   on one line, and a row that overflows would put the primary off-screen. */
.cfm-row {
  display: flex; justify-content: flex-end; flex-wrap: wrap;
  gap: .5rem; margin-top: 1rem;
}
.cfm-row .btn { min-height: 2.6rem; }
/* The alternative is a real choice, not a lesser one - same height and shape
   as the other two. It sits between Cancel and the primary so the order reads
   least to most: nothing, some, all. */
.cfm-alt { flex: 0 1 auto; min-width: 0; }

/* Three answers do not lay out like two.
   Two buttons sit right-aligned at their natural widths and look deliberate.
   Three, at three different natural widths, wrap - and a wrapped row is two
   ragged lines with three different right edges and nothing lining up with
   anything. On the car it wraps sooner, because that block makes every button
   taller and wider.
   So a three-answer row shares itself out equally instead: one line, three
   equal columns, and the primary reads as one of three choices rather than as
   an offcut. Below 700px equal thirds would be about 110px each, which cannot
   hold "Resume from 1:10:00", so there they stack full width - which is the
   same idea in the other axis and puts the answer wanted most under the
   thumb. */
.cfm-row.three { flex-wrap: nowrap; }
.cfm-row.three .btn { flex: 1 1 0; min-width: 0; justify-content: center; }
@media (max-width: 700px) {
  .cfm-row.three { flex-direction: column; flex-wrap: nowrap; }
  .cfm-row.three .btn { flex: none; width: 100%; }
}

/* The season action sits under the season rail rather than in it. */
/* No gutter of its own. .epsec sits inside .detail .sheet, which already
   applies var(--pad) - the first version added a second one and stood the
   button about fifty pixels right of the season chips it belongs under, which
   is what looked wrong about it.

   The row collapses entirely when the button is hidden, rather than leaving an
   empty flex line holding the margin open. */
/* The rail's own bottom margin is the gap before the grid. With a button in
   between, that gap now belongs to the button, so the rail gives it up rather
   than the two stacking into a band of empty space. */

/* ---- a bulk write, said out loud ----
   Counts rather than a percentage of an estimate: the total is known before
   the first call, so "7 of 24" is a fact. The bar is the same number drawn a
   second way, for the glance rather than the read.

   width on a 3px strip, not transform: this is one element that moves a
   handful of times over several seconds, not a thing animating per frame, and
   a scaled bar would carry its own rounded ends out of shape. */
.cfm-prog { margin-top: .75rem; }
.cfm-bar {
  height: 3px; border-radius: 99px; overflow: hidden;
  background: rgba(255, 255, 255, .16);
}
.cfm-bar i {
  display: block; height: 100%; width: 0; border-radius: 99px;
  background: #fff; transition: width .25s var(--ease);
}
.cfm-count {
  margin-top: .4rem; font-size: 0.7667rem; font-weight: 700;
  color: var(--dim); font-variant-numeric: tabular-nums;
}
@media (prefers-reduced-motion: reduce) {
  .cfm-bar i { transition: none; }
}
.cfm-no:disabled { opacity: .45; }
/* The confirm button carries the whole state machine: idle, working, done. */
.cfm-yes { position: relative; gap: .5rem; }
.cfm-spin {
  width: 0; height: .95rem; opacity: 0; flex: none;
  border-radius: 50%; border: .14rem solid rgba(0,0,0,.25); border-top-color: #000;
  transition: width .18s var(--ease), opacity .18s var(--ease);
}
.cfm-yes.busy .cfm-spin { width: .95rem; opacity: 1; animation: spin .7s linear infinite; }
.cfm-yes.busy { pointer-events: none; }
.cfm-yes.ok { background: var(--good-deep); }
.cfm-yes.danger { background: #fff; }
@media (prefers-reduced-motion: reduce) {
  .cfmdlg { animation: none; }
  .cfm-yes.busy .cfm-spin { animation: none; }
}

/* A pulse on the button that changed, so the result registers as an event
   rather than as something that was always that way. */
@keyframes btnFlash {
  0%   { transform: scale(1); }
  30%  { transform: scale(1.06); }
  100% { transform: scale(1); }
}
.btn.iconlabel.flash { animation: btnFlash .32s var(--ease); }
@media (prefers-reduced-motion: reduce) { .btn.iconlabel.flash { animation: none; } }

@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .cfmdlg { width: min(32rem, 100%); padding: 1.4rem; }
  .cfm-t { font-size: 1.3333rem; }
  .cfm-b { font-size: 1rem; }
  /* 3.5rem, not 3.1: this is the app's confirmation dialog and its buttons were
     the one class of control still short of the floor everything around them
     keeps. The settings page now sends its only destructive action here. */
  /* px under the rem, because this dialog is where the irreversible writes
     are confirmed and 3.5rem is 48.3px at the Compact interface size - under
     the floor, on the one button whose mis-tap cannot be undone. */
  .cfm-row .btn { min-height: max(3.5rem, 52px); font-size: 1.0667rem; }
}


/* ================================================================ search ==
   The head is sticky, and that is the decision everything else rests on.

   The bug was that the second list became unreachable at depth. A switch that
   scrolls away repairs the pagination and leaves the reader 3,000px into Films
   with no way to Shows but to scroll all the way back - the same complaint
   restated one level down. Sticky is what makes "the other kind" one tap from
   anywhere, which is the actual repair.

   Paid for honestly: 3.5rem plus padding is about 68px, 9.1% of the car's
   747px, permanently. In exchange it is the cheapest sticky this stylesheet
   could draw - one flat opaque fill and a hairline. No blur, no
   backdrop-filter, no gradient, no shadow: the car rasterises in software and
   paints neither filter, and an opaque solid is the only kind of sticky bar it
   can composite instead of repaint. */
/* Not sticky, and no rule under it.
   It was sticky to keep "the other kind" one tap away at any depth, which was
   a real need when the two kinds were two lists you could get lost between.
   With one list and a filter there is nothing to get back to - and a bar
   pinned across the top of a screen whose whole job is showing results is
   paying 68px of every viewport, permanently, for a control used once per
   search. */
.shead {
  display: flex; align-items: center; gap: .5rem;
  padding: .1rem var(--pad) .7rem;
}
.sh-gap { flex: 1; }

/* Chip-sized, because they are chips. These were .ktab raised to 3.5rem - a
   52px control repeated three times across the top of a screen whose job is to
   show results, which read as a toolbar rather than as a filter. The 52px
   floor is about a thumb in a moving car, so it is spent where that is true
   and nowhere else. */
/* The search head's kind switch is a .chipbtn and the density switch beside it
   is a .ktab, and the two disagreed about how tall a pill is: measured on the
   running app at 1500px, 34.9px against 28.5px in the same flex row - 22%
   apart, on one line. .chipbtn carries no min-height at all, so its own
   .45rem block padding set the height and nothing could bring it down.
   The Library head has the identical pair of switches and reads correctly,
   because there BOTH are .ktab - which is what makes Search look wrong rather
   than merely different.
   Only the BLOCK padding is zeroed. Taking `padding: 0 1rem` instead would
   also override the car block's wider .chipbtn sides, and the car is the one
   place those 1.25rem are doing a job. */
.kfilt { display: inline-flex; align-items: center; }
.shead .kfilt { min-height: 1.9rem; padding-top: 0; padding-bottom: 0; }
.kfilt b {
  margin-left: .4rem; font-weight: 800; opacity: .55;
  font-variant-numeric: tabular-nums;
}
/* A kind with nothing behind it keeps its chip and its zero. "There are no
   shows called that" is an answer worth reading without tapping for it, and a
   control that appears and disappears between keystrokes is worse than a quiet
   one. */
.kfilt[disabled] { opacity: .4; }
.kfilt[disabled]:hover { background: var(--elev); border-color: var(--line-hi); }
.kfilt[disabled]:active { transform: none; }

/* The density switch while the head is still waiting for its counts.

   On the GROUP, not on .vtab, and that is the whole rule. Once slidePill has
   marked .haspill, .haspill .ktab.on hands its ground to .tabpill - and the
   pill is a SIBLING of the buttons, pinned to opacity 1 by .haspill .tabpill.
   Dimming the tabs therefore leaves the brightest object on a screen otherwise
   showing eighteen grey holes, in the app's own "you are here" white, in the
   one row whose entire job is to read as not-yet. Group opacity makes a
   stacking context and the pill renders inside it. Measured: head height
   unchanged at 46.88, and the switch lands on the same .4 the chips beside it
   already get from .kfilt[disabled]. */
.shead.sh-wait .viewsw, .shelfhead.sh-wait .viewsw { opacity: .4; }

/* The only thing this screen ever announced was the pager's closing sentence.
   .results holds hundreds of cards and must never be a live region; this says
   the shape of the answer in one sentence instead. */
.shead .srstat {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  border: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* Both lists are always in the document; [hidden] is forced to display:none at
   the top of this file, so the inactive one occupies exactly zero height and
   the single autoPage foot is genuinely the last laid-out element in the
   scroller - the only configuration autoPage works in. Switching is an
   attribute flip, not two hundred rebuilt cards. */
/* One list, so one shape. .dr-shot is a 16/9 box and a search result is a 2:3
   poster: rows that had a backdrop injected filled the frame while every other
   row letterboxed inside it, which is two artwork shapes in one column. The
   injection is gone and the shot is poster-shaped, so Details and Posters are
   two readings of the same wall rather than two different walls. */
.sres .drow { grid-template-columns: 4.6rem minmax(0, 1fr) auto; }
.sres .dr-shot { aspect-ratio: 2/3; }

/* Which kind a row is, said in the one place a mixed list needs it. Grey,
   because it is a neutral fact about the item and not a state of yours - the
   white pill next to it is already spoken for by watched state. */
.kmark {
  position: absolute; top: var(--mark-inset, .4rem); right: var(--mark-inset, .4rem); z-index: 2;
  padding: .1rem .32rem; border-radius: var(--r-s);
  background: rgba(0, 0, 0, .66); color: var(--dim);
  font-size: .5rem; font-weight: 800; letter-spacing: .1em;
  text-transform: uppercase; pointer-events: none; white-space: nowrap;
}

/* Deliberately NO staggered entry on these cards. @keyframes rise is applied
   with `both` fill and per-child delays, and the reduced-motion block zeroes
   durations but not delays - so a nineteen-card stagger becomes nineteen holds
   at opacity 0. It is also nineteen animated elements on a software
   rasteriser. The screen wrapper's own rise is enough. */

/* ---- between one term and the next ----
   Dimmed, not deleted. Emptying the container collapsed a ~6000px document to
   about 60px, the browser clamped scrollY to 0, and the reader was thrown to
   the top by a layout collapse rather than by a decision - on every debounced
   keystroke. The previous answer is still valid for the previous prefix.

   120ms because this is the highest-frequency transition on the screen, once
   per keystroke past two characters, and frequency of use sets duration.
   Opacity because it is one of the two properties this car composites. */
.sres { transition: opacity .12s var(--ease); }
.sres.busy { opacity: .45; }

/* ---- what you already have ----
   Ownership is the outline .card .artbox::after already paints, turned white.
   That outline's only other job is :hover, which never fires on a touchscreen,
   so nothing is lost - and no element, no pseudo-slot (both of .artbox's are
   long since spoken for) and no hue is added. White is already this app's word
   for asserted.

   Never the only statement: the sub-line says "In library" in words and the
   accessible name repeats it, because colour alone would make the difference
   invisible to anyone who cannot see it. */
.card.own .artbox::after { box-shadow: inset 0 0 0 .06em #fff; }

/* ---- Details, on results with no backdrop ----
   .dr-shot is 16/9 with object-fit: cover, so a 2:3 poster falling through
   `x.backdrop || x.poster` is drawn as its own middle band with the printed
   title sliced off top and bottom. Contained on black instead: a letterbox is
   honest, a crop through the title is not. */
/* Two standing lines the poster grid has nowhere to put. White is where you
   are; grey is a neutral fact about the shelf. */
.dr-at, .dr-held {
  font-size: .6667rem; font-weight: 800; letter-spacing: .1em;
  text-transform: uppercase;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dr-at { color: var(--fg); }
.dr-held { color: var(--dim); }

/* Amber is already this app's word for results being hidden, and the foot is
   the one place a list can own up to it: the server drops every result with no
   artwork while `total` is passed through unfiltered, so "Ted" is headed 475
   films and can serve 367. */
.ap-end.cut { color: var(--hide-tint); }
/* A foot with nothing left to say is still 3.4rem of min-height, so a
   single-page answer used to end in 51px of nothing. */
.apage.mute { min-height: 0; padding: 0; }

/* ---- the empty state, which now offers something ----
   Typing is the expensive act here - "Ted Lasso" is nine taps on a car
   keyboard - and a search refined away was unrecoverable: the first search
   pushes a history entry and every refinement replaceStates over it, so Back
   left search rather than returning to the word before. */
/* ---- one Clear, three times ----
 * Three controls on this screen cleared something, and each was drawn by
 * whatever happened to be nearest. Recent's was a .chipbtn in among the term
 * chips - identical ground, border, radius and hover to the eight
 * navigational things beside it, told apart by one shade of grey - and as the
 * last child of a wrapping row it stood somewhere different every time the
 * terms rewrapped. Recently opened's was a lone chip below twelve cards, and
 * it never matched the .sr-row rule above, so it was the only control on this
 * screen that had never been given the 52px floor on any device: 33px in the
 * car. The field's is a paragraph of its own, at .sfield below.
 *
 * One shape now, and PLACEMENT says which is which. A Clear sits at the
 * right-hand end of the row belonging to the thing it clears. The two words
 * "Clear" are never ambiguous because they are never on the same row, so
 * neither label has to grow; the distinction a screen reader needs is on
 * aria-label, which is where this file keeps those.
 *
 * --sc-f is the ONE number. It is .sec's own font-size, so the control's line
 * box IS the heading's line box and the row cannot get taller; everything the
 * thumb needs beyond that is taken by ::after, which paints nothing and
 * occupies no layout. .sec's size changes in the car block, and --sc-f is
 * redeclared THERE rather than here, so an edit to that line cannot leave
 * this behind. */
.sidle {
  padding: .4rem 0 2rem;
  --sc-f: 0.8rem;
  /* How far the invisible half reaches past the drawn half, per side. 52px is
     the house floor and 1.55 the root line-height. Floored at zero so that a
     heading which ever grew past 52px could not invert the reach and shrink
     the target to inside the ink. */
  --sc-reach: max(0px, (52px - var(--sc-f) * 1.55) / 2);
}
.srecent { padding: 0 var(--pad); margin-top: .2rem; }
/* The heading and the section's one action on a single row. align-items is
   center rather than the baseline .sec uses inside itself, because the control
   is a box and the heading is a line - .sec keeps its own baseline row, so the
   "12 titles" note still sits on the heading's baseline.
   The bottom margin is the reach plus a quarter rem, so the invisible half of
   the button stops short of the first chip and the first poster. That is the
   answer to the objection this placement used to be rejected on. */
.srhead, .sohead { display: flex; align-items: center; gap: .6rem; }
.srhead > .sec, .sohead > .sec { margin: 0; min-width: 0; flex: 1 1 auto; }
.srhead { margin: 0 0 max(.6rem, calc(var(--sc-reach) + .25rem)); }
.sohead { margin: 0 var(--pad) max(.7rem, calc(var(--sc-reach) + .25rem)); }

.sr-row { display: flex; flex-wrap: wrap; gap: .5rem; }
/* Every chip on this screen, and .sask-b is the one that never joined the
   list. .chipbtn on its own comes to about 34px at --ui 1 and about 43px in
   the car - both derived from its own declarations, both under the floor -
   so the one control here offering to spend money was the smallest target on
   the screen. */
.sr-row .chipbtn, .sfail .chipbtn,
.sask-row .chipbtn, .ask-back .chipbtn, .ask-turn .chipbtn {
  min-height: 3.5rem; padding: 0 1.1rem;
  display: inline-flex; align-items: center; max-width: 100%;
}
.sr-term { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sf-retry { gap: .45rem; color: var(--dim); }
.sf-retry svg { width: .95rem; height: .95rem; flex: none; }

/* A quiet grey pill with a fill and no edge - deliberately not the shape of
   anything it sits near, which is the whole point of it. Not white and not
   var(--bad): white beside a var(--dim) heading would make the destructive
   thing the brightest thing on the line, which is the exact inversion of what
   a section action is for. */
.sclear {
  flex: none; position: relative;
  display: inline-flex; align-items: center; gap: .4rem;
  height: calc(var(--sc-f) * 1.55); padding: 0 .6rem;
  border: 0; border-radius: 99px; cursor: pointer;
  background: var(--elev); color: var(--dim);
  font-size: var(--sc-f); font-weight: 700; line-height: 1.55;
  letter-spacing: .04em; text-transform: none; white-space: nowrap;
  transition: background .2s, color .2s, transform .15s var(--ease);
}
.sclear svg { width: .85rem; height: .85rem; flex: none; display: block; }
.sclear:hover, .sclear:focus-visible { background: var(--elev2); color: var(--fg); }
.sclear:active { transform: scale(.96); }
/* How big it looks and how big it is stop being the same number: both halves
   come off --sc-f, so the pill can stay inside a heading's line box while the
   target reaches the floor. Vertical only - a box reaching sideways would
   swallow the end of the heading's own text. */
.sclear::after {
  content: ''; position: absolute;
  inset: calc(var(--sc-reach) * -1) 0;
}

/* ---- failure, said as failure ----
   Four words used to cover a dead relay, a 500 and a genuinely empty
   catalogue. Two of those are the app lying about the catalogue, on the screen
   most likely to be used from a moving car on a bad cell. */
.sfail { display: flex; flex-direction: column; align-items: flex-start; gap: .3rem; padding: 0 var(--pad) 2rem; }
.sfail .empty { padding-left: 0; padding-right: 0; padding-bottom: .4rem; }

/* The affordance type=search draws natively is suppressed above because it
   matches nothing else here, and nothing replaced it. */

@media (max-width: 700px) {
  /* 48px. iOS's own floor is 44pt and the thumb on a 375px screen is the one
     that actually fits; 52.5 here would spend 5% of an 812px display on the
     header alone. */
  /* Keeps the notch inset that the earlier rule in this same block sets.
     A plain padding-top here silently gave it back, and the header slid under
     the status bar on an iPhone added to the home screen. */
  .top { padding-top: calc(.5rem + env(safe-area-inset-top, 0px)); padding-bottom: .5rem; }
  /* That said .ktab under this block, which is what the kind switch was before
     it became .chipbtn at .8333rem - and this block does not touch .chipbtn,
     so the sentence has been describing a shape that is not on the screen.
     Measured at 375, on a query answered at TMDB's 10,000 cap: 449px of head
     in 375, and the density switch pushed off the right edge.

     Settled by letting the head wrap. The switch drops to a second row,
     right-aligned where it already sits, and both are reachable.

     Wrapping rather than dropping the counts, because the overflow is
     count-dependent and so is the cost: a query answered with "Films 477"
     still lays out on one line at 38px, and only a query at TMDB's 10,000 cap
     spends the extra 32. Hiding the counts would spend them on every search to
     pay for the widest one. Measured at 375 with the cap: 449px of 375 and the
     switch off the right edge; wrapped, 0 overflow and the head at 70px.

     .kindsw stays nowrap. sDrawHead writes no whitespace between a label and
     its count, so "Films10,000" has no break opportunity anyway, and letting
     the three chips break apart would give the switch a third row to fall to
     rather than the second. */
  .shead { gap: .35rem; padding: .1rem var(--pad) .6rem; flex-wrap: wrap; }
  /* The switch is pushed right by its own margin, not by .sh-gap. The spacer
     is a zero-width span, so whether it breaks onto the second row WITH the
     switch or stays on the first is decided by a few pixels of slack - and it
     went one way at 375 and the other at 390, which put the switch hard right
     on one phone and hard left on the next. An auto margin holds it right on
     whichever row it lands on. */
  .shead .viewsw { margin-left: auto; }
  /* .ktab drops to 1.8rem here; the chip beside it has to drop with it or the
     row is 1.5px out of true on the screen with the least room for it. */
  .shead .kfilt { min-height: 1.8rem; }
  .sr-row .chipbtn, .sfail .chipbtn { padding: 0 .9rem; }
}

@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  /* .sec grows in the car and --sc-f is .sec's own font-size, so the Clear
     beside it has to grow too. Here rather than beside the other car type
     rules because .sidle declares --sc-f unconditionally further up the file:
     at equal specificity the later rule wins. */
  .sidle { --sc-f: 0.9667rem; }
  .shead { padding: .1rem var(--pad) .8rem; }
  /* The one place the 52px floor is actually about a thumb in a moving car. */
  .shead .kfilt, .shead .vtab { min-height: 3.5rem; }
  /* Width, not only height, and it is the axis the .ktab rule in the car block
     above cannot reach. .vtab is `padding: 0; width: 2.3rem` and that rule
     comes first, so the car's `.ktab { padding: 0 1.2rem }` wins the padding
     and box-sizing: border-box floors the used width at 2 * 1.2rem + 2 * 2px
     = 42.4px - under the 52 this app spends on anything reached without
     looking. .qhead joins .shead because the same two icon tabs sit in
     Discover's head and the shelf's, at the same 42.4px, for the same reason.
     One selector, so a later edit cannot move one head and forget two. */
  .shead .vtab, .qhead .vtab { width: 3.5rem; }
}

/* ---- reduced motion, the half that was never honoured ----
   The block at the top of this file zeroes animation-duration and
   transition-duration and never touches the DELAYS. Every `rise` carries
   `both` fill, so under reduced motion each delay became a hold at opacity 0
   for its full length - reduced motion turning an animation into a stall,
   app-wide, fixed in two declarations. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-delay: 0ms !important;
    transition-delay: 0ms !important;
  }
}


/* ------------------------------------------------------------- verdict -- */

/* The only bordered panel on this sheet, and that is the point rather than a
   style. Reviews below are separated by rules precisely so three strangers do
   not look like a control group; this one IS a single object with an edge
   around it, because it is one voice and not a person's - a machine's reading
   of the reviews underneath. Everything else on the page is a fact rendered
   as it arrived. The frame is what says this one is not. */
.vdcard {
  /* A card, at every width, and aligned on its BOX like a card - not hung out
     into the gutter to get its text onto the sheet's column. That was tried
     and it cost the thing the card is for: at narrow widths the hang did not
     fit, the frame had to come off below 900px, and what was left was a block
     with a rule under it. A bordered card whose text is inset by its own
     padding is what a card is; the reviews below it are cards now too, so the
     two agree with each other, which is the alignment that matters here.

     62ch of TEXT plus the padding either side, so the column inside the box is
     the measure prose actually wants. */
  max-width: calc(62ch + 2.2rem);
  /* The top margin is written here because nothing else can give it one. Every
     other section on this sheet takes its gap from the rule at the top of this
     file - `.detail .sec, .sheet .sec { margin: 1.6rem 0 .6rem }` - applied to
     the h2.sec it leads with. This card's headline is an h2.vd-h INSIDE the
     box and never a .sec, so that rule cannot reach it: measured, every other
     section sat 27px below the one above and this one sat at 0, flush against
     the last row of cast. Same 1.6rem, so it lines up with the rhythm rather
     than inventing a second one. */
  margin: 1.6rem 0;
  padding: 1rem 1.1rem 1.05rem;
  /* --line-hi, not --line, and every rule inside this card is the same.
     --line is #1f1f1f, which is a visible hairline against the page's #0b0b0b
     and disappears completely against --elev - measured, the card ground
     resolves to about #191919 and the rule to #1f1f1f, six values apart. The
     scale simply did not draw. --line-hi is what the app already uses wherever
     a border sits on an elevated ground; the settings card next door is the
     same pair. */
  border: .06em solid var(--line-hi);
  border-radius: var(--r-m);
  background: var(--elev);
  animation: rise .5s var(--ease) both;
}
/* Dismissal is a real removal, so it gets a real exit - the card leaves and
   the page closes over it rather than the block blinking out. */
.vdcard.gone {
  animation: fadeout .22s var(--ease) both;
  pointer-events: none;
}

.vd-head { display: flex; align-items: flex-start; gap: .75rem; }
/* The headline is an h2 by markup, because it heads a section, but it is not
   .sec - the section headings on this page are small, spaced capitals and
   this is a sentence you read. */
.vd-h {
  flex: 1 1 auto; min-width: 0; margin: 0;
  font-size: 1.15rem; font-weight: 800; letter-spacing: -.015em; line-height: 1.25;
  color: var(--fg);
}
/* Collapses the card, and points wherever the card is about to go: up while
   open, down while shut. Dim and small - it is a quieter action than anything
   else on this page, and it must be findable without inviting a press. */
.vd-x {
  flex: none; width: 2.2rem; height: 2.2rem; margin: -.35rem -.4rem 0 0;
  display: grid; place-items: center;
  background: none; border: 0; padding: 0;
  color: var(--dimmer); cursor: pointer;
}
/* The rotation is on the svg and the press-scale on the button, deliberately:
   one transform per element, so a press during the turn does not cancel it. */
.vd-x svg {
  width: 1.1rem; height: 1.1rem;
  transform: rotate(180deg);
  transition: transform .3s var(--ease);
}
.vdcard.shut .vd-x svg { transform: rotate(0deg); }
.vd-x:hover, .vd-x:focus-visible { color: var(--fg); }
.vd-x:active { transform: scale(.92); }
/* The waiting card's placeholder wears .vd-x itself rather than a class of its
   own, so the box that holds the head's height open is the SAME declaration at
   every breakpoint - including the 3rem the car block below gives it, and
   including whatever that becomes when someone notices it is 45px against a
   52px floor. Two rules kept in step by hand would eventually stop being in
   step. What the placeholder must not inherit is the affordance: an empty span
   showing a pointer and scaling under a press is a control that goes nowhere,
   which is the same defect .listcard.skel is being guarded against. */
.vdwait .vd-x { cursor: default; pointer-events: none; }

/* ---- what folds ----
   The prose, the pulse and the source line. The headline and the scale are
   outside this on purpose: collapsed, the card is still the whole verdict at a
   glance - the sentence and where it sits - in about 100px, and what goes away
   is the 250px of reasoning under it. A collapse that left only a title bar
   would be hiding the answer and keeping the furniture.

   overflow:hidden is the resting state, not something the transition adds. It
   is what makes height 0 a clip rather than a squash, and it is already what
   glideHeight sets while the wipe is running. */
.vd-body { overflow: hidden; }
.vdcard.shut .vd-body { height: 0; }
/* Nothing inside a folded card is a tab stop or a screen-reader stop - the
   height is zero either way, and a focus ring landing in an invisible
   paragraph is how a keyboard gets lost. */
.vdcard.shut .vd-body > * { visibility: hidden; }

/* ---- the scale ----
   A gauge, never an input. No thumb, no shadow, nothing that affords a drag -
   the marker is a triangle pointing down at the track it reads. */
.vd-scale { margin: .85rem 0 .2rem; }
.vd-track {
  position: relative; height: 1px;
  margin: 0 .4rem .95rem;
  background: var(--line-hi);
}
/* The three stops the labels sit under. */
.vd-track i {
  position: absolute; top: -.28rem; width: 1px; height: .62rem;
  background: var(--line-hi);
}
/* How far along the axis the verdict sits, drawn as distance rather than as a
   point. Dim, under the marker, and grown with scaleX from the left - a
   transform on one element, which composites, where an animated width would
   lay out the track on every frame. */
.vd-track u {
  position: absolute; left: 0; top: 0; height: 1px;
  background: var(--dim);
  transform: scaleX(0); transform-origin: left center;
  animation: pipfill .55s var(--ease) .1s forwards;
}
/* The verdict. White, which in this app means "here" - the same thing it
   means on a selected tab and a held version. It meant it on the ratings bar
   under the reviews heading too until that bar was deleted for saying this
   one's number a second time, so this triangle is now the only white mark on
   this whole run of the page, and the only white on the card that is not
   type. It arrives after the fill has finished travelling, so the eye follows
   the distance and then lands on the point. */
.vd-track b {
  position: absolute; top: -.46rem; margin-left: -.32rem;
  width: 0; height: 0;
  border-left: .32rem solid transparent;
  border-right: .32rem solid transparent;
  border-top: .42rem solid var(--fg);
  /* Behind the fill, not beside it. The fill runs .1s-.65s; the marker lands
     as it arrives, so the motion reads as one gesture that ends on the answer
     rather than as two things starting at once. */
  animation: fadein .3s var(--ease) .58s both;
}
/* Left, centre and right - which is why the outer two are pinned to the ends
   rather than centred on their ticks: a label centred under the 15% tick
   hangs off the card at the left edge, and this row has to survive "worth
   your time" at 1.1rem on a car. */
.vd-axis {
  display: flex; justify-content: space-between; gap: .5rem;
  font-size: 0.8rem; color: var(--dim);
}
.vd-axis span { flex: 0 1 auto; min-width: 0; }
.vd-axis span:nth-child(2) { text-align: center; }
.vd-axis span:last-child { text-align: right; }

.vd-prose {
  margin: .9rem 0 0; padding-top: .9rem;
  border-top: 1px solid var(--line-hi);
  color: var(--fg); font-size: 1rem; line-height: 1.6;
}

/* ---- what people praised and faulted ----
   One line each, chevron up or down. No colour on either: green and red would
   be a third and fourth hue in a sheet built on white plus two, and "the
   pacing is slow" is an opinion, not an error state. */
.vd-pulse { margin-top: .95rem; }
.vd-p {
  display: flex; align-items: baseline; gap: .55rem;
  padding: .22rem 0;
  font-size: 0.9333rem; line-height: 1.45; color: var(--fg);
}
.vd-pi { flex: none; display: block; width: .8rem; color: var(--dim); }
.vd-pi svg { width: .8rem; height: .8rem; display: block; }
.vd-up .vd-pi svg { transform: rotate(180deg); }
.vd-prule { height: 1px; margin: .55rem 0; background: var(--line-hi); }

/* Where it came from, and how much of it there was. Last, smallest, dimmest -
   but never absent, because "written from 3 reader reviews" is the difference
   between a summary and a verdict passing itself off as a consensus. */
.vd-src {
  margin-top: .9rem;
  font-size: 0.7333rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--dimmer);
}

/* ---- while it is being written ----
   Bars where the words will be, on the real track with its real stops. The
   frame is not a guess, so nothing moves when the answer lands - the card is
   refilled in place rather than replaced, and only the contents arrive.

   .skel is the app's own placeholder and carries @keyframes drift: light moved
   with a transform and faded with an opacity across an already-painted layer. One shared
   idiom, and the one that was written for a device that rasterises in
   software - an animated background-position here would repaint eight bars
   every frame. */
.vd-sk { display: block; height: .72rem; border-radius: var(--r-s); }
.vd-sk-h { height: 1.15rem; margin: .1rem 0 .05rem; }
/* The bar rhythm is the TEXT rhythm, not a guess at it: a prose line here is
   1rem at line-height 1.6, so a bar plus its margin has to come to the same
   ~1.6rem, and a pulse row to the ~1.75rem its own line plus padding makes.

   The bar COUNT is the one guess on this card, and it is deliberately a low
   one. Measured at --ui 1 on a 1188px sheet the open reservation comes to
   374.45 against a landed card of 481.73 and a corpus floor of 379.86 - under
   the shortest card there is. That is the point, not an oversight: about
   1100px of reviews and similar titles stand below this card, so a reservation
   that is sometimes too tall pulls them upward, while one that is always
   slightly too short only ever pushes them down. Five bars and three-and-three
   would land near 451 and shrink for roughly a quarter of titles. Do not add a
   bar; the rest of the distance is travelled by the glide in verdictBlock. */
.vd-skbody { margin: .9rem 0 0; padding-top: .95rem; border-top: 1px solid var(--line-hi); }
.vd-skbody .vd-sk { margin-bottom: .88rem; }
.vd-skbody .vd-sk:last-child { margin-bottom: 0; }
.vd-skpulse { margin-top: 1.05rem; }
.vd-skpulse .vd-sk { height: .66rem; margin-bottom: 1.1rem; }
.vd-skpulse .vd-sk:last-child { margin-bottom: 0; }
.vd-skpulse .vd-prule { margin: 1.1rem 0; }
/* The wait's own line, in the slot the source line will occupy. It breathes
   rather than blinks - subpulse is the same keyframe the subtitle button uses
   when it is busy, and this is the same fact about the same kind of moment. */
.vd-wait { animation: subpulse 1.9s var(--ease) infinite; }

/* The contents arriving, not the card. The card is already there - it has
   been holding its own border since the request went out - so the ladder is
   on what lands inside it. Three delays and a shared fourth, the same step
   the history and watchlist pages use, and the same reason: past three, a
   ladder is a stutter rather than an order. */
.vdcard.filled > * { animation: rise .42s var(--ease) both; }
.vdcard.filled > :nth-child(1) { animation-delay: .02s; }
.vdcard.filled > :nth-child(2) { animation-delay: .08s; }
.vdcard.filled > :nth-child(3) { animation-delay: .14s; }
.vdcard.filled > :nth-child(n+4) { animation-delay: .2s; }
/* Refilled in place, so it must not play its own entrance a second time. */
.vdcard.filled { animation: none; }

@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .vdcard { padding: 1.2rem 1.3rem; }
  .vd-h { font-size: 1.3rem; }
  .vd-sk { height: .82rem; }
  .vd-sk-h { height: 1.35rem; }
  .vd-axis { font-size: 0.8667rem; }
  .vd-prose { font-size: 1.1rem; }
  .vd-p { font-size: 1rem; padding: .3rem 0; }
  .vd-x { width: 3rem; height: 3rem; }
}

/* At 375px the three labels cannot share a line - "worth your time" alone is
   most of it - so the middle stop drops out and the scale keeps its ends. */
@media (max-width: 640px) {
  .vd-axis span:nth-child(2) { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .vdcard, .vdcard.filled > *, .vd-track b { animation: none; }
  /* A chevron that takes 300ms to turn is decoration; with the duration
     zeroed it would be a 1.5px jump, which is the harshest thing it can do. */
  .vd-x svg { transition: none; }
  /* Each of these would otherwise survive as a wrong resting state rather
     than as motion: a fill stuck at scaleX(0) is an empty scale, and a
     placeholder that breathes for five seconds is exactly what this setting
     is asking not to see. The sweep is left to the global rule, which zeroes
     its duration - .skel is shared and not this block's to switch off. */
  .vd-track u { animation: none; transform: scaleX(1); }
  .vd-wait { animation: none; }
  .vdcard.gone { animation: none; opacity: 0; }
  .vd-x:active { transform: none; }
}


/* ------------------------------------------------------------- reviews -- */

/* The one block on this page you read rather than scan, and the first
   long-form prose the app has ever put on a title page. It does not inherit
   the sheet's text cap: the facts grid and rows are 54rem, roughly 120
   characters a line, which is fine for a grid of facts and about double what
   continuous prose wants. 62ch sits inside the comfortable 45-75 band, and it
   is expressed in ch rather than rem so the --ui knob can scale the type
   without stretching the line past the count that matters. */
/* 62ch of TEXT plus the padding of the cards inside it, so a review's measure
   and the verdict's measure are the same column. The heading, the line and
   the sort row sit on the block's own edge - the gutter - which is where every
   other section heading on this sheet starts. */
.revBlock { max-width: calc(62ch + 2.2rem); }

/* ---- what the whole set thinks ----
   One dim line, under the heading and above the first voice, because it is
   the frame you read the voices inside. It is deliberately not the score in
   the hero: that one is tens of thousands of votes from people who wrote
   nothing, this is the handful whose words are directly below it, and on 134
   of 222 measured titles they span four points or more - which is why the
   RANGE is in the sentence and not only the average.

   The margin was the wrapper's. The wrapper held this line and a bar; with
   the bar gone it held one child, so it went and its spacing came here
   unchanged - the -.15rem collapses against .sec's 1.6rem/.6rem to 6.75px
   above, and .revsort has no margin-top, so .9rem is what you get below. */
.rg-t {
  display: block;
  margin: -.15rem 0 .9rem;
  font-size: 0.8rem; color: var(--dim);
  font-variant-numeric: tabular-nums;
}
/* There is no bar under that line any more, and it should not come back as a
   different shape.

   It drew st.lo, st.hi and st.avg as a band and a tick on a fixed 0-10 track,
   and the line above prints all three; it was aria-hidden, so it was never
   the version anyone on assistive tech got. What retired it is that it was
   the SECOND grey horizontal measure with a white mark on it inside one
   screen - .vd-track is a 1px --line-hi rule with a triangle on it, inside
   the card directly above this heading - and the two were drawing one number.
   The model derives the verdict's position from these same reviews, and
   across the cached verdicts position and the review average correlate at
   .94, a mean 3.9 points apart on a 0-100 scale. .vd-track is the one that
   stays, because its three axis labels are written per title and the middle
   one names who the film is for, which nothing computed from a rating can
   say.

   One measured note, so nobody rebuilds it believing it looked like the
   drawing: .rg-bar set overflow: hidden and .rg-bar b was an abspos child
   with top and bottom at -.11rem, so the overhang was clipped every time.
   The tick painted flush inside a 4.2px bar, never proud of it. */

/* ---- corrections ----
   A family per card, because the backlog is 113 files and about six answers.
   Nothing here uses a hue: dismissing a disc extra is not a warning and not a
   success, it is just a decision, and this stylesheet has exactly two hues
   spoken for. A decided card goes quiet instead - the same "done is dimmer"
   the version list already uses. */
/* ---- corrections ----
   This screen is a workbench, not a page of prose, so it takes the gutter and
   fills the width. Filenames are the content here and they are long; a 46rem
   measure wrapped every one of them twice and turned a list you scan into a
   paragraph you read.

   It lives under Library now. It is a statement about what is on the shelf,
   and it used to be two taps into Settings behind a heading about the app. */
.fxwrap { padding: 0 var(--pad) 7rem; }

.fixnote {
  margin: -.2rem 0 1rem; font-size: 0.8rem; color: var(--dim); line-height: 1.5;
  max-width: var(--prose);
}

/* ---- the tools row ---- */
.fxtools {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: 0 var(--pad) 1rem;
}
.fxfind {
  display: flex; align-items: center; gap: .5rem; flex: 1 1 18rem; min-width: 0;
  background: var(--elev); border: var(--rule-w) solid var(--line-hi);
  border-radius: 99px; padding: 0 1rem; min-height: 3rem;
  transition: border-color .18s;
}
.fxfind:focus-within { border-color: var(--accent); }
.fxfind svg { width: 1rem; height: 1rem; color: var(--dim); flex: none; }
.fxq {
  flex: 1 1 auto; min-width: 0; background: none; border: 0; outline: none;
  color: var(--fg); font: inherit; font-size: 0.9rem;
}
.fxq::placeholder { color: var(--dimmer); }
.fxq::-webkit-search-cancel-button { -webkit-appearance: none; }

/* ---- a family ----
   Collapsed it is one line you can scan; open it is every file it holds. The
   old card showed three names and no way to see the rest, which is fine for
   recognising a family and useless for deciding about one. */
.fxfam {
  border: var(--rule-w) solid var(--line-hi); border-radius: var(--r-m);
  background: var(--elev); margin-bottom: .55rem; overflow: hidden;
  transition: border-color .18s, opacity .18s;
}
.fxfam.is-staged { opacity: .66; border-color: var(--line); }
.fxfhead {
  display: flex; align-items: center; gap: .8rem; width: 100%;
  padding: .85rem 1rem; background: none; border: 0; color: inherit;
  font: inherit; text-align: left; cursor: pointer;
  transition: background .16s;
}
.fxfhead:hover { background: var(--elev); }
/* ICON.chev points DOWN, so closed is a quarter turn right and open is the
   glyph as drawn. Rotating the open state instead pointed it backwards. */
.fxfchev {
  flex: none; display: grid; place-items: center; color: var(--dim);
  transform: rotate(-90deg);
  transition: transform .18s var(--ease);
}
.fxfchev svg { width: .95rem; height: .95rem; }
.fxfam.open .fxfchev { transform: rotate(0deg); }
.fxftext { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: .1rem; }
.fxfname {
  font-size: 0.9333rem; font-weight: 800; color: var(--fg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fxfmeta {
  font-size: 0.7333rem; font-weight: 700; color: var(--dimmer);
  font-variant-numeric: tabular-nums;
}
.fxfstate {
  flex: none; font-size: 0.7333rem; font-weight: 800; color: var(--dim);
  letter-spacing: .04em;
}
/* ---- the accordion ----
   grid-template-rows from 0fr to 1fr, which is the one way to animate to a
   height the browser has measured rather than to one we guessed. Every
   max-height version of this is wrong twice: too small for a family with forty
   files, so it scrolls or clips, and too large for one with two, so the
   collapse spends most of its time animating empty space and the row appears
   to hesitate before it moves.

   The padding lives on the inner element, not here. Padding on a 0fr row is
   still painted, so a collapsed family would keep a band of empty background
   under its heading. */
.fxfbody {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows .28s var(--ease);
}
.fxfam.open .fxfbody { grid-template-rows: 1fr; }
/* min-height: 0 is what lets the row actually reach zero - a grid item's
   automatic minimum size is its content, so without this it collapses to the
   height of the list and stops.

   And NO padding here. overflow: hidden clips the contents but an element's
   own padding still contributes its height, so a bottom padding of 1rem left
   every closed family sitting on a 15px band of empty background - visible,
   and exactly the sort of thing that reads as a rendering fault rather than a
   design. The gutter moves to the two children instead, which puts the list's
   border-top on the same pixels it was on before. */
.fxfinner { overflow: hidden; min-height: 0; }
.fxfinner > .fxfiles { margin-inline: 1rem; }
.fxfinner > .fxfacts { margin: 0 1rem 1rem; }

.fxfiles {
  list-style: none; margin: 0 0 .8rem; padding: .2rem 0 0;
  border-top: 1px solid var(--line);
}
.fxfile {
  display: flex; align-items: center; gap: .7rem;
  padding: .3rem 0; border-bottom: 1px solid var(--line);
}
.fxfile:last-child { border-bottom: 0; }
.fxfn {
  flex: 1 1 auto; min-width: 0;
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 0.7333rem; color: var(--dim); line-height: 1.5;
  overflow-wrap: anywhere;
}
.fxone {
  flex: none; min-height: 2.4rem; padding: 0 .8rem; border-radius: 99px;
  border: var(--rule-w) solid var(--line-hi); background: none; color: var(--dim);
  font: inherit; font-size: 0.7333rem; font-weight: 800; cursor: pointer;
}
.fxone:hover { color: var(--fg); border-color: var(--accent); }
.fxmore {
  padding: .35rem 0; font-size: 0.7333rem; font-weight: 700; color: var(--dimmer);
}
.fxfacts { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }

/* ---- the workbench ----
   The third answer. The screen could say "ignore this family" and "file it as
   that", and nothing else - so anything the grouping got wrong was
   unreachable. Here you write the pattern and watch what it takes. */
.fxwork {
  border: var(--rule-w) solid var(--accent); border-radius: var(--r-m);
  background: var(--elev); padding: 1rem; margin-bottom: 1.2rem;
}
.fxwhead { display: flex; align-items: center; gap: .6rem; }
.fxwhead .sec { margin: 0; flex: 1 1 auto; }
.fxwx {
  flex: none; width: 2.6rem; height: 2.6rem; border: 0; border-radius: 99px;
  background: var(--elev2); color: var(--fg); cursor: pointer;
  display: grid; place-items: center;
}
.fxwx svg { width: .9rem; height: .9rem; }
.fxwq {
  width: 100%; box-sizing: border-box; margin: .2rem 0 .6rem;
  background: var(--bg2); border: var(--rule-w) solid var(--line-hi);
  border-radius: var(--r-s); padding: .8rem 1rem; color: var(--fg);
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 0.8667rem; outline: none;
}
.fxwq:focus { border-color: var(--accent); }
.fxwout { margin-bottom: .5rem; }
.fxwcount {
  font-size: 0.8rem; font-weight: 800; color: var(--dimmer);
  font-variant-numeric: tabular-nums;
}
/* The two hues this app has, used for the two things they mean. Amber is
   "results are being hidden", and a pattern that takes every unnamed file is
   about to hide all of them. */
.fxwcount.ok { color: var(--fg); }
.fxwcount.bad { color: var(--dimmer); }
.fxwcount.warn { color: var(--hide-tint); }
.fxwlist {
  list-style: none; margin: 0 0 .8rem; padding: 0;
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 0.7333rem; color: var(--dim); line-height: 1.6;
  max-height: 14rem; overflow-y: auto; overscroll-behavior-y: contain;
  overflow-wrap: anywhere;
}

/* ---- rules in force ---- */
.fxrule {
  display: flex; align-items: center; gap: .9rem; flex-wrap: wrap;
  padding: .7rem 0; border-bottom: 1px solid var(--line);
}
.fxrt { flex: 1 1 20rem; min-width: 0; display: flex; flex-direction: column; gap: .15rem; }
.fxrwhy { font-size: 0.8667rem; color: var(--fg); line-height: 1.45; }
.fxrwhen {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 0.7333rem; color: var(--dimmer); overflow-wrap: anywhere;
}
.fxrmeta {
  flex: 0 0 auto; font-size: 0.7333rem; font-weight: 700; color: var(--dimmer);
  font-variant-numeric: tabular-nums;
}
.fxrx { flex: none; }

/* ---- the commit bar ---- */
.fxstaged {
  flex: 1 1 auto; font-size: 0.8rem; font-weight: 800; color: var(--fg);
}
.fxdiscard {
  flex: none; min-height: 3rem; padding: 0 1rem; border: 0; border-radius: 99px;
  background: none; color: var(--dim); font: inherit; font-size: 0.8rem;
  font-weight: 800; cursor: pointer;
}
.fxdiscard:hover { color: var(--fg); }

@media (max-width: 700px) {
  .fxrmeta { flex: 1 1 100%; }
  .fxfile { flex-wrap: wrap; }
}
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  /* 52px, like every other control. */
  .fxfind { min-height: 3.5rem; }
  .fxone, .fxdiscard { min-height: 3.5rem; }
  .fxwx { width: 3.5rem; height: 3.5rem; }
  .fxfhead { padding: 1.05rem 1rem; }
}

/* Pinned, because the decisions above it scroll and the commit must not.
   Hidden at zero rather than disabled: a button that cannot do anything is
   better absent than greyed. */
.fixbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-fixbar);
  /* --tabsH is 0 except on a phone, where the tab bar is fixed to the same
     edge. See the phone block. */
  bottom: var(--tabsH, 0px);
  padding: .8rem var(--pad) calc(.8rem + env(safe-area-inset-bottom, 0px));
  background: var(--panel); border-top: var(--rule-w) solid var(--line);
  display: flex; justify-content: flex-end;
}
.fixapply {
  min-height: 3.4rem; padding: 0 1.6rem; border: 0; border-radius: 99px;
  background: var(--accent-deep); color: var(--accent-ink);
  font: inherit; font-size: 0.9rem; font-weight: 800; cursor: pointer;
  /* inline-flex so the button can hold a spinner beside its label without the
     label shifting when the spinner turns into a tick - both occupy the same
     1.1em box. */
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  transition: transform .12s ease;
}
.fixapply:active { transform: scale(.97); }
.fixapply[disabled] { opacity: .6; }

/* A flag that leads somewhere says so by being pressable. The amber stays
   amber - it still means "results are being held back from you". */
.sheet-pop .flag.flaggo {
  cursor: pointer; text-decoration: underline; text-underline-offset: .18em;
}
.sheet-pop .flag.flaggo:active { opacity: .7; }

/* .fixapply is the only one of these six left. .fixname, .fixeg, .fixdo and
   .fixundo went with the corrections screen's old markup, and .fixrx was
   renamed .fxrx in the same change - none of the five appears in app.js at
   all. The touch floor here matters only for the one that still exists. */
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .fixapply { min-height: 3.8rem; font-size: 1rem; }
}

/* ---- the correction sheet ----
   Inside the dialog the releases list already uses, so it inherits Escape, the
   backdrop and the leaving animation rather than reimplementing them. */
.fixsheet { padding: .2rem .9rem .9rem; }
.fixfn {
  margin: 0 0 .8rem; font-size: 0.7333rem; line-height: 1.5;
  color: var(--dim); word-break: break-all;
}
.fixsearch { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }
.fixq {
  flex: 1 1 12rem; min-width: 0; min-height: 3.2rem; padding: 0 .9rem;
  border: var(--rule-w) solid var(--line); border-radius: var(--r-s);
  background: var(--bg2); color: var(--fg); font: inherit; font-size: 0.9rem;
}
.fixq:focus { border-color: var(--accent); outline: none; }
.fixkinds { display: flex; gap: .35rem; }
.fixkind {
  min-height: 3.2rem; padding: 0 .9rem; border: var(--rule-w) solid var(--line);
  border-radius: 99px; background: none; color: var(--dim);
  font: inherit; font-size: 0.7333rem; font-weight: 800; cursor: pointer;
}
.fixkind.on { background: var(--accent-deep); color: var(--accent-ink); border-color: transparent; }

.fixres { margin-top: .7rem; }
.fixhit {
  display: flex; align-items: center; gap: .7rem; width: 100%;
  min-height: 3.6rem; padding: .4rem .5rem; margin-bottom: .3rem;
  border: 0; border-radius: var(--r-s); background: none; color: var(--fg);
  font: inherit; text-align: left; cursor: pointer;
  transition: transform .12s ease;
}
.fixhit:active { transform: scale(.985); background: var(--elev); }
.fixhp { flex: none; width: 2.2rem; height: 3.3rem; border-radius: 3px; overflow: hidden; background: var(--elev2); }
.fixhp img { display: block; width: 100%; height: 100%; object-fit: cover; }
.fixht { flex: 1 1 auto; min-width: 0; font-size: 0.8667rem; font-weight: 700; line-height: 1.35; }

/* What will happen, before it happens. The rename preview is the whole point:
   it is the one line that shows the blast radius in a form anyone can check. */
.fixplan {
  margin-top: .8rem; padding-top: .8rem;
  border-top: var(--rule-w) solid var(--line);
}
.fixplanh { margin: 0 0 .6rem; font-size: 0.9333rem; font-weight: 800; color: var(--fg); }
.fixseasons { display: flex; align-items: center; gap: .3rem; flex-wrap: wrap; margin-bottom: .7rem; }
.fixsl { font-size: 0.7333rem; font-weight: 800; color: var(--dim); margin-right: .3rem; }
.fixseason {
  min-width: 2.6rem; min-height: 2.6rem; border: var(--rule-w) solid var(--line);
  border-radius: 99px; background: none; color: var(--dim);
  font: inherit; font-size: 0.7333rem; font-weight: 800; cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.fixseason.on { background: var(--accent-deep); color: var(--accent-ink); border-color: transparent; }
/* Wider, because it is a word among numbers - and first, because it is the
   answer for most of what is left in the backlog. */
.fixseason.fixsp { min-width: auto; padding: 0 .8rem; }
.fixprev {
  margin: 0 0 .8rem; font-size: 0.7333rem; line-height: 1.5;
  color: var(--dim); word-break: break-all;
}
.fixsave {
  width: 100%; min-height: 3.4rem; border: 0; border-radius: 99px;
  background: var(--accent-deep); color: var(--accent-ink);
  font: inherit; font-size: 0.8667rem; font-weight: 800; cursor: pointer;
  transition: transform .12s ease;
}
.fixsave:active { transform: scale(.98); }
.fixsave[disabled] { opacity: .6; }

/* The door out of the Versions list. Quiet, and above "Find another release"
   so a mis-tap costs nothing. */
.sd-wrong { padding: .5rem .9rem .1rem; }
.wrongbtn { width: 100%; min-height: 3.2rem; font-size: 0.8rem; color: var(--dim); }

@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .fixq, .fixkind { min-height: 3.6rem; }
  .fixht { font-size: 0.9333rem; }
  .fixseason { min-width: 3rem; min-height: 3rem; }
  .fixsave { min-height: 3.8rem; }
}

/* ---- trailers ----
   A rail of the same footage from different CDNs, and a player that stays
   inside the app. The old control opened a YouTube page in another tab.

   The tiles are 16:9 because that is what the footage is, and they carry the
   title's own backdrop rather than a per-trailer thumbnail, which the service
   does not supply. Dimmed by OPACITY and never by a filter - filter and
   backdrop-filter do not paint at all on the car. */
.trblock { margin: 1.6rem 0 0; }
/* One card. There is one trailer; the servers it can come from are words
   underneath it, not four copies of the same picture.

   Capped rather than full-width. At the column width it came out 381x214 and
   read as a hero - the largest thing on a page whose actual subject is the
   title above it. A trailer is a thing you might tap, not the reason you are
   here, so it is sized like the cast shots beside it rather than like the
   artwork at the top. */
.trrail {
  display: flex; gap: .6rem; overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch;
  /* The top margin is folded into the shorthand that was already pulling this
     row out to the gutters - two margin declarations would have meant the
     second silently dropping the first's horizontal pull. */
  padding: calc(.2rem + var(--lift-room)) var(--pad) .4rem;
  margin: calc(var(--lift-room) * -1) calc(var(--pad) * -1) 0;
  /* This row has been full bleed since it was built and has been missing the
     other half of it the whole time: .trcard is scroll-snap-align: start, so
     with a gutter in front of it the first card snaps to the scrollport edge
     and sits under the padding. Same line and the same reason as on .rail
     .track - and the reason the other rails got it in the same pass rather
     than copying this block's gap onto four more rows. */
  scroll-padding-left: var(--pad);
  scrollbar-width: none;
}
.trrail::-webkit-scrollbar { display: none; }
.trcard {
  position: relative; flex: 0 0 auto; scroll-snap-align: start;
  width: 15rem; aspect-ratio: 16 / 9;
  border: 0; border-radius: var(--r-m); overflow: hidden; padding: 0;
  background: var(--elev2); cursor: pointer;
  transition: transform .14s var(--ease);
}
.trcard:active { transform: scale(.975); }
.trshot { position: absolute; inset: 0; display: block; }
.trshot img {
  display: block; width: 100%; height: 100%; object-fit: cover;
  opacity: 0; transition: opacity .3s var(--ease);
}
.trcard.ld .trshot img { opacity: 1; }
/* Only the fallback is dimmed. A real frame from this trailer is the point of
   the tile and should not be hidden behind the play glyph; the backdrop is a
   stand-in and should read as one. */
.trcard.generic .trshot img { opacity: .5; }
/* The glyph is the affordance, so it is the one thing at full strength. */
.trplay {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 3rem; height: 3rem; border-radius: 99px;
  background: rgba(0, 0, 0, .55); color: #fff;
  display: grid; place-items: center;
}
.trplay svg { width: 1.35rem; height: 1.35rem; margin-left: .1rem; }
/* A frame can be any brightness, so the label gets its own ground rather than
   relying on the picture being dark. */
.trcard::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 45%;
  background: linear-gradient(to top, rgba(0,0,0,.72), rgba(0,0,0,0));
  pointer-events: none;
}
.trwho {
  position: absolute; left: .55rem; right: .55rem; bottom: .5rem; z-index: 2;
  font-size: 0.7333rem; font-weight: 800; letter-spacing: .02em; color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .8);
}
.trtags { display: flex; align-items: center; gap: .3rem; flex-wrap: wrap; }
.trtag {
  display: inline-block; padding: .18em .5em; border-radius: var(--r-s);
  font-size: 0.6667rem; font-weight: 800; letter-spacing: .04em;
  line-height: 1.5; white-space: nowrap; text-shadow: none;
  /* A dark ground so it stays readable over a bright frame, AND a hairline so
     it still reads as a chip over black - which is most of the player head.
     Ground alone was invisible there, and the quality and audio tags came out
     looking like loose words beside the one real tag. */
  background: rgba(0, 0, 0, .55);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .22);
  color: rgba(255, 255, 255, .82);
}
/* The service is what you choose BY, so it is the one with weight. */
.trtag.svc { background: rgba(255, 255, 255, .92); color: #000; box-shadow: none; }
/* The resolution is what you compare ACROSS tiles - legible, never loud. */
.trtag.q { color: #fff; }
/* Dolby Vision or HDR. A picture claim like the resolution, so it sits between
   that and the audio rather than beside the audio - three steps of one colour,
   because a fourth chip is not worth a third hue and this app only has two. */
.trtag.r { color: rgba(255, 255, 255, .82); }
.trtag.a { color: rgba(255, 255, 255, .62); }

/* ---- the player ----
   Its own overlay rather than the main player: this is ninety seconds of
   footage with no session, no progress to report and nothing to resume, and
   routing it through openPlayer would open a Jellyfin playback session for a
   file Jellyfin has never heard of.

   It takes the whole screen. The previous version was a 64rem box with a
   control bar underneath, which made the footage a stamp in the middle of a
   black field and spent the space on chrome. A trailer is a picture; the
   picture gets the room and the controls lie on top of it.

   No backdrop-filter anywhere in here. The car does not paint it, and this is
   a blur over a DECODING VIDEO - the most expensive thing that could be asked
   for. The scrims are gradients, which cost nothing and read better. */
.trov {
  position: fixed; inset: 0; z-index: var(--z-full);
  background: #000;
  opacity: 0; transition: opacity .18s var(--ease);
}
.trov.in { opacity: 1; }
.trov.leaving { opacity: 0; }

.trstage { position: absolute; inset: 0; background: #000; }
.trvid {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: contain; background: #000;
}
/* The whole picture is the play/pause target, which is what a person expects
   of a video and what a car needs - a 1900x1000 hit area beats any button. */
.trtap { position: absolute; inset: 0; z-index: 1; cursor: pointer; }

/* Only while paused; it is the one control that has to be findable without
   waking anything up. */
.trbig {
  position: absolute; left: 50%; top: 50%; z-index: 2;
  transform: translate(-50%, -50%);
  width: 5.2rem; height: 5.2rem; border-radius: 99px;
  border: 0; background: rgba(0, 0, 0, .55); color: #fff;
  display: grid; place-items: center; cursor: pointer;
  transition: transform .16s var(--ease), background .16s;
}
.trbig svg { width: 2rem; height: 2rem; margin-left: .18rem; }
.trbig:hover { background: rgba(0, 0, 0, .72); }
.trbig:active { transform: translate(-50%, -50%) scale(.94); }

.trspin {
  position: absolute; left: 50%; top: 50%; z-index: 2;
  transform: translate(-50%, -50%);
  display: flex; gap: .3rem;
}
.trspin i {
  width: .45rem; height: .45rem; border-radius: 99px; background: #fff;
  animation: trbob .9s var(--ease) infinite;
}
.trspin i:nth-child(2) { animation-delay: .12s; }
.trspin i:nth-child(3) { animation-delay: .24s; }
@keyframes trbob { 0%, 60%, 100% { opacity: .28; } 30% { opacity: 1; } }

/* ---- the controls ----
   One layer over the picture, so head and foot fade together. It is
   pointer-events: none and gives them back to its own children, or the
   transparent middle would eat every tap meant for the picture. */
.trui {
  position: absolute; inset: 0; z-index: 3; pointer-events: none;
  opacity: 1; transition: opacity .22s var(--ease);
}
.trui.hid { opacity: 0; }
.trui > * { pointer-events: auto; }

.trhead {
  position: absolute; left: 0; right: 0; top: 0;
  display: flex; align-items: flex-start; gap: 1rem;
  padding: 1rem var(--pad);
  background: linear-gradient(to bottom, rgba(0,0,0,.72), transparent);
}
.trwhat { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
/* What you are watching, first. Four sources deep into a full-screen player
   the fact that had gone missing was WHAT this is a trailer for - the head
   named the CDN and nothing else. */
.trtitle {
  font-size: 1.15rem; font-weight: 800; color: #fff; letter-spacing: -.015em;
  text-shadow: 0 1px 4px rgba(0,0,0,.7);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.trmeta { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-top: .22rem; }
.trtagsbig .trtag { font-size: 0.7rem; padding: .22em .6em; }
.trpos {
  font-size: 0.7333rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: rgba(255,255,255,.62);
}
.trclose { margin-left: auto; }

.trfoot {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 2.6rem var(--pad) 1rem;
  background: linear-gradient(to top, rgba(0,0,0,.82), rgba(0,0,0,.42) 45%, transparent);
}

/* ---- the scrubber ----
   A 4px line in a 1.6rem box: the line is what you read, the box is what you
   hit. On a touchscreen the difference between those two numbers is the
   difference between a usable scrubber and a decorative one. */
.trscrub {
  position: relative; height: 1.6rem; cursor: pointer;
  display: flex; align-items: center; touch-action: none;
}
.trtrack {
  position: relative; width: 100%; height: 4px; border-radius: 99px;
  background: rgba(255, 255, 255, .22); overflow: hidden;
}
.trbuf {
  position: absolute; left: 0; top: 0; bottom: 0; width: 0;
  background: rgba(255, 255, 255, .3);
}
.trplayed {
  position: absolute; left: 0; top: 0; bottom: 0; width: 0;
  background: #fff;
}
.trknob {
  position: absolute; top: 50%; left: 0;
  width: .8rem; height: .8rem; border-radius: 99px; background: #fff;
  transform: translate(-50%, -50%) scale(.55);
  transition: transform .14s var(--ease);
  pointer-events: none;
}
.trscrub:hover .trknob, .trscrub:focus-visible .trknob { transform: translate(-50%, -50%) scale(1); }

.trrow {
  display: flex; align-items: center; gap: .35rem;
  margin-top: .1rem;
}
.trgrow { flex: 1 1 auto; }
.trtime {
  font-size: 0.8rem; font-weight: 700; color: rgba(255, 255, 255, .82);
  font-variant-numeric: tabular-nums; margin-left: .5rem; white-space: nowrap;
}

.tricon {
  flex: none; width: 2.9rem; height: 2.9rem; border: 0; border-radius: 99px;
  background: none; color: #fff; cursor: pointer;
  display: grid; place-items: center;
  transition: background .16s, opacity .16s;
}
.tricon svg { width: 1.15rem; height: 1.15rem; }
.tricon:hover { background: rgba(255, 255, 255, .14); }
.tricon:active { transform: scale(.94); }
.tricon:disabled { opacity: .3; cursor: default; }
.tricon:disabled:hover { background: none; }
.tricon.off { color: rgba(255, 255, 255, .55); }
.trpp svg { width: 1.35rem; height: 1.35rem; }

/* ---- the end card ----
   A trailer that stops with three more behind it should say so, rather than
   leaving a black rectangle and a close button. */
.trend {
  position: absolute; inset: 0; z-index: 4;
  display: grid; place-items: center;
  background: rgba(0, 0, 0, .72);
}
.trend-box { text-align: center; padding: var(--pad); max-width: 26rem; }
.trend-k {
  font-size: 0.7333rem; font-weight: 800; letter-spacing: .18em;
  text-transform: uppercase; color: var(--dim); margin-bottom: .4rem;
}
.trend-n {
  font-size: 1.5rem; font-weight: 800; color: #fff; letter-spacing: -.02em;
  margin-bottom: 1.1rem;
}
.trend-b { display: flex; gap: .5rem; justify-content: center; flex-wrap: wrap; }

@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  /* A little larger where the screen is, and where the tap target matters. */
  .trcard { width: 19rem; }
  .trwho { font-size: 0.8rem; }
  /* 3.5rem is the 52px floor. Every control in this player clears it, which
     the old bar's 2.8rem chips did not. */
  .tricon { width: 3.5rem; height: 3.5rem; }
  .tricon svg { width: 1.3rem; height: 1.3rem; }
  .trpp svg { width: 1.55rem; height: 1.55rem; }
  /* The line stays 4px; the box it lives in grows, because that is the part a
     thumb has to find. */
  .trscrub { height: 2.4rem; }
  .trknob { transform: translate(-50%, -50%) scale(1); }
  .trtitle { font-size: 1.35rem; }
  .trtagsbig .trtag { font-size: 0.7667rem; }
  .trtag { font-size: 0.7rem; }
  .trtime { font-size: 0.8667rem; }
}
@media (prefers-reduced-motion: reduce) {
  .trcard, .trov, .trshot img, .trui, .trknob, .tricon { transition: none; }
  .trspin i { animation: none; opacity: .7; }
}

/* ---- the order ----
   Its own row under the line: the line says the reviewers disagreed - the
   range clause is what does it - and this is what you press because of it, so
   they read top to bottom as one thought. */
.revsort { display: flex; gap: .3rem; margin: 0 0 1rem; flex-wrap: wrap; }

/* A card each.

   This was a rule between reviews and not a card around each, on the argument
   that three bordered panels make a section of three strangers look like a
   control group. That was the wrong call once the verdict above them became a
   card: a bordered card sitting on a stack of unbordered rows reads as the
   only real object in the section, and the reviews it was written from read as
   its footnotes. They are the source, not the appendix. Same border, same
   ground, same radius as the verdict - one family, and the verdict is simply
   the one at the top.

   The rule between them is gone with it; the gap does that work now. */
.rev {
  padding: .9rem 1.1rem 1rem;
  margin-bottom: .7rem;
  border: .06em solid var(--line-hi);
  border-radius: var(--r-m);
  background: var(--elev);
}
.rev:last-child { margin-bottom: 0; }
/* Only the ones that actually overflow are a target. */
.rev.can { cursor: pointer; }

.rev-by {
  display: flex; align-items: baseline; gap: .55rem;
  margin: 0 0 .45rem;
  font-size: 0.8rem; color: var(--dim);
}
.rev-who { color: var(--fg); font-weight: 700; }
/* Tabular so a column of 8 and 10 does not jitter. Inherits .rev-by's colour:
   the star is a glyph, not an accent, and there is no third hue to reach for. */
.rev-rate { font-variant-numeric: tabular-nums; }
/* Which season the review is about. Sits with the author and the score
   because it is the same kind of fact - who wrote it, what they gave it, what
   they were watching - and it takes no colour for the same reason the star
   does not: this line is one voice, not three signals. */
.rev-sea { white-space: nowrap; }
.rev-when { color: var(--dimmer); margin-left: auto; white-space: nowrap; }

/* 1rem rather than the person bio's 0.9rem. The bio is a glance target; this
   is read. Reading time is not the constraint - the median review is 124
   words, about 37 seconds - so the type can afford the size, and at arm's
   length in a car it needs it.

   pre-line because 54.5% of bodies carry real paragraph structure and a
   further 42.3% contain no newline at all; single newlines inside a paragraph
   are the author's and are kept.

   No filter is involved anywhere, which is the point on a device where filter
   never paints. */
.rev-body {
  margin: 0; color: var(--fg);
  font-size: 1rem; line-height: 1.65;
  white-space: pre-line;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 5;
  -webkit-box-orient: vertical;
}
.rev.open .rev-body { -webkit-line-clamp: unset; display: block; }
/* Held unclamped for the length of the wipe, in BOTH directions. Closing
   without it, the clamp reapplies on the first frame: the paragraph cuts to
   five lines while the box is still tall, so the text disappears and an empty
   box shrinks after it. Held, the full text stays put and overflow:hidden
   above wipes down over it - the opening motion, run backwards. */
.rev.folding .rev-body { -webkit-line-clamp: unset; display: block; }
.rev-body p { margin: 0 0 .75em; }
.rev-body p:last-child { margin-bottom: 0; }
/* 22.6% of bodies use no emphasis at all, so both of these are absent more
   often than not; neither may carry colour. */
.rev-body strong { font-weight: 700; }
.rev-body em { font-style: italic; }

/* A real button, not just an ellipsis. The clamp hides between 81% and a
   tenth of a review, so the browser's own ellipsis is far too quiet to be the
   only signal that there is more - and once open there has to be a way back. */
.rev-more {
  /* Block, because a button is inline-block and the repair link below it is
     too: on a card that was both cut at 4,000 characters and clamped, the two
     ran together on one line. */
  display: block;
  margin-top: .45rem; padding: .35rem 0;
  min-height: 2.6rem;
  background: none; border: 0; color: var(--dim);
  font: inherit; font-size: 0.7333rem; font-weight: 800;
  letter-spacing: .1em; text-transform: uppercase;
  cursor: pointer; text-align: left;
}
.rev-more:active { transform: scale(.97); }

/* Reviews four and up, as one group. display:none rather than height or
   opacity, because they must be out of the layout entirely: a clamped card is
   5 lines whether or not you can see it, so forty-eight of them left in the
   flow would put Collection several screens further down on every talkative
   title, which is the exact cost the clamp exists to avoid. It is also what
   makes the arming rule in reviewsBlock necessary - a display:none card
   measures zero and would never be given a fold.

   The group is NOT what animates, and that is deliberate twice over. Fifty
   cards is a seven-thousand-pixel subtree, and fading one of those is a layer
   that size on a software rasteriser - worse than the per-card ladder the
   search grid already rejected, not better. What animates is the three that
   land where the button you just pressed used to be, which is the only place
   anyone is looking: the button sits under the list, so reviews four, five
   and six arrive under your thumb and seven onward are already past the
   bottom of a 747px screen. Same ladder as the entry, same three delays. */
.revx { display: none; }
.revBlock.all .revx { display: block; }
.revBlock.all .revx > .rev:nth-child(-n+3) { animation: rise .45s var(--ease) both; }
.revBlock.all .revx > .rev:nth-child(1) { animation-delay: .02s; }
.revBlock.all .revx > .rev:nth-child(2) { animation-delay: .08s; }
.revBlock.all .revx > .rev:nth-child(3) { animation-delay: .14s; }

/* The section's own control, and it has to read as belonging to the section
   rather than to the last card - .rev-more sits inside a card and is one
   review's business, this one is all of them. So it takes the same rule the
   cards use between themselves and spans the measure. */
.rev-all {
  display: block; width: 100%;
  /* No rule above it any more. It separated the control from the last review
     when reviews were rows divided by rules; under a stack of cards the gap
     already does that, and a full-width line under a rounded card cuts across
     the shape it is meant to follow. */
  margin-top: .9rem; padding: .5rem 0 .2rem;
  border: 0;
  background: none; color: var(--dim);
  font: inherit; font-size: 0.7333rem; font-weight: 800;
  letter-spacing: .1em; text-transform: uppercase;
  cursor: pointer; text-align: left;
}
.rev-all:active { transform: scale(.99); }

/* Only ever rendered on the 4.4% the relay had to cut, so it is a repair
   notice and not a call to action - dim, inline, and nothing like .btn. */
.rev-rest {
  display: inline-block; margin-top: .5rem;
  font-size: 0.8rem; color: var(--dim);
  text-decoration: underline; text-underline-offset: .2em;
}
/* And it waits for the point where the text actually runs out. Offering to go
   and read the rest somewhere else while five lines of the beginning are still
   folded is offering the wrong thing - the reader has not run out of review
   yet, they have run out of card. A cut review that never overflowed the clamp
   has no fold to wait for, so it keeps its link. */
.rev.can:not(.open) .rev-rest { display: none; }

/* Touch only, and read at arm's length across a cabin rather than at a desk.
   The type steps up and the tap area with it: five lines at 1.1rem/1.65 is
   ~136px, and three cards plus the heading come to ~640px of the 747px
   viewport - one screen, with the next heading just showing, which is what
   tells you the page continues. */
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .rev { padding: 1.1rem 1.3rem 1.2rem; margin-bottom: .85rem; }
  .rev-by { font-size: 0.8667rem; margin-bottom: .55rem; }
  .rev-body { font-size: 1.1rem; }
  .rev-more { min-height: 3.5rem; font-size: 0.8rem; }
  .rev-all { min-height: 3.5rem; font-size: 0.8rem; padding-top: 1rem; }
  .rev-rest { font-size: 0.8667rem; margin-top: .7rem; }
  .rg-t { font-size: 0.8667rem; }
  /* .ktab is 3.5rem in the car for the 52px floor; this used to pull the
   review sort tabs back under it at (0,2,0), which beats the car rule. */
.revsort .ktab { min-height: 2.4rem; }
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .revsort .ktab { min-height: 3.5rem; }
}
}

/* On a 375px phone the measure is the screen, not the cap, and the date drops
   under the name rather than squeezing a long username. */
@media (max-width: 640px) {
  .rev-by { flex-wrap: wrap; gap: .4rem .55rem; }
  .rev-when { margin-left: 0; }
  /* The card stays - it is the shape of the section now - but at 390px the
     gutter is 13px and 18px of card padding on top of it leaves the prose a
     very narrow column, so the inset comes down to meet it. */
  .rev { padding: .85rem .9rem .9rem; margin-bottom: .6rem; }
  .vdcard { padding: .9rem; }
}

/* -------------------------------------------------------------- motion --
   The section arrives four screens below the fold, on a request that has
   already resolved once by the time anyone scrolls to it, so this is motion
   nobody waits on. Same keyframe, ease and ladder step as the history and
   watchlist pages - it is the one entrance this app writes.

   THREE delays and no more, which is the whole discipline here. The list can
   hold fifty cards; the ladder is on the three that are visible before anyone
   presses anything, and the other forty-seven arrive inside .revx as a single
   element. Every reason is next door in the search grid's note: a ladder that
   long is that many animated elements on a software rasteriser, and it holds
   the tail at opacity 0 for as long as the ladder is deep. */
.revBlock { animation: rise .5s var(--ease) both; }
.revlist > .rev { animation: rise .45s var(--ease) both; }
.revlist > .rev:nth-child(1) { animation-delay: .02s; }
.revlist > .rev:nth-child(2) { animation-delay: .08s; }
.revlist > .rev:nth-child(3) { animation-delay: .14s; }

/* Re-running on a sort IS the sort's animation. paint() builds new elements,
   so the ladder runs again by itself - three cards restating themselves in
   order is the cheapest honest way to say the list is not the one you were
   just looking at, and it needs no second keyframe to say it. */

@media (prefers-reduced-motion: reduce) {
  /* The global rules at the top and foot of this file zero every DURATION,
     which is not the same as every delay: a delayed transition survives them
     as a stall rather than as a movement. These say it outright anyway,
     because a .01ms scale is still a wrong-state frame, and the fold's inline
     transition is the one thing here the cascade reaches rather than removes.

     The .rg-bar pair that used to end this selector list went with the bar
     itself, and the list was rejoined to its braces in the same edit - on
     purpose. A selector left dangling here would have silently eaten the rule
     below it, and the symptom would have been the sort tabs keeping their
     press transform under reduced motion, on a machine nobody tests on. */
  .revBlock, .revlist > .rev, .revBlock.all .revx > .rev { animation: none; }
  .rev-more:active, .rev-all:active, .revsort .ktab:active { transform: none; }
}

/* ======================================================= wide screens ====
 *
 * Measured on a 1990px window, where 1888 is available to a title page: the
 * facts grid used 810 of it, the activity list 810, the review list 619 and
 * the box office bar 510. Between half and two thirds of the screen was empty,
 * with everything stacked down the left.
 *
 * The fix is NOT to unpin the caps. They are mostly right: a line of review
 * prose 1888px wide is unreadable, and a fact that reads "Director / Pete
 * Docter" stretched over 400px looks worse than one that does not. What a wide
 * screen wants is MORE COLUMNS, not longer lines - so these rules add columns
 * and leave every measurement inside them alone.
 *
 * Placed at the END of this file on purpose. Several of the caps it lifts
 * are declared thousands of lines below where this block would otherwise
 * sit - .revBlock's 62ch measure is one - and at equal specificity the
 * later rule wins. Raising specificity instead would have meant guessing
 * which ancestor to name for each one.
 *
 * 1200px is the threshold because that is where two readable columns fit with
 * the page gutter still on both sides. The car sits at 1307 and gets this,
 * which is right: it was wasting 490px of its own width on one narrow column.
 * Phones and the 820-1199 band are untouched.
 */
@media (min-width: 1200px) {
  /* A title page becomes two columns for the two blocks that are narrow by
     nature - the facts and the activity. Everything else on the sheet is a
     rail or a wall and already spans, so it keeps spanning: the default here
     is full width and only these two opt out of it. */
  .detail .sheet { display: grid; grid-template-columns: 1fr 1fr; column-gap: 2.6rem; align-items: start; }
  .detail .sheet > * { grid-column: 1 / -1; min-width: 0; }
  .detail .sheet > .factsSlot { grid-column: 1; }
  /* The episode page calls its facts block .epFacts and was the one page this
     never reached: its facts kept the default full-width span, which pushed
     Activity out of row one and left the whole right-hand side of the details
     empty with the activity list stranded below it. Same pairing, second
     name. */
  .detail .sheet > .epFacts { grid-column: 1; }
  .detail .sheet > .actSlot { grid-column: 2; }
  /* Their own caps come off inside a column that is already half the page. */
  .detail .actSlot, .detail .kwrow { max-width: none; }
  .detail .facts-grid { max-width: none; }

  /* Reviews pack, the way the calendar, history and settings walls do.
   *
   * CSS columns were the first attempt and this replaces them. Columns balance
   * by HEIGHT: they cut the list wherever the total divides, and the hidden
   * overflow group had to be told to span the full width - which drew two
   * enormous cards underneath a set of columned ones, in a list that is
   * supposed to be one kind of thing.
   *
   * A packed grid instead. Same contract as .histbody: a symmetric gap, no
   * margin on the card, and .packed - written by the packer once it has
   * measured - zeroes the row gap and lets each card span the rows it needs.
   * auto-fill rather than a column count, so the number of columns follows the
   * window and 34rem keeps each one near the 62ch measure the block was capped
   * at before.
   *
   * .revx is display: contents when open, so the cards inside it become grid
   * items of this list rather than a block sitting across it. Closed it is
   * display: none and they are not rendered at all, which is why the packer
   * leaves them out of its measurements. */
  .revBlock { max-width: none; }
  .revlist {
    display: grid; align-items: start;
    grid-template-columns: repeat(auto-fill, minmax(34rem, 1fr));
    gap: 1.4rem;
  }
  .revlist.packed { grid-auto-rows: minmax(2px, auto); row-gap: 0; }
  .revlist.packed > .rev, .revlist.packed .revx > .rev { grid-row: span var(--sp, 1); }
  .revlist > .rev, .revlist .revx > .rev { margin-bottom: 0; }
  .revBlock.all .revx { display: contents; }

  /* A person page, which was two narrow blocks down the left of a wide screen:
     the header 780 of 1890 and the filmography 840.

     The header lifts its cap and only the BIO keeps one, because the bio is
     the only prose in it - the name, the facts, the four figures and the
     career histogram are all better for the room, and the histogram most of
     all: it is fifty-one bars of one year each and it was drawing them in
     540px.

     The filmography packs its year groups into columns, the way the reviews
     and the walls do. A year is a self-contained group, so a column break can
     only ever fall between years.

     The spine loses its first-and-last trimming here, and that is not
     cosmetic. Those two rules exist to stop the line dangling at the top and
     bottom of ONE continuous run; in columns every column has a top and a
     bottom, so the trim was landing on two groups out of thirty and the other
     twenty-eight dangled. Every group now ends its own spine at its last item,
     which is what makes a column of years read as a column of years. */
  /* The header stops being one column. It was five blocks stacked - name,
     facts, four figures, the career histogram, the biography - each as wide as
     it wanted and the rest of a 1900px row left blank beside them, which is
     what "reflect these new changes" was pointing at.
     Widening it alone does not fix it: the bio has a reading measure and
     nothing else in the stack is prose, so a wider .pinfo just moves the blank
     space. So the bio moves to a second column beside the figures, where its
     measure is the column width rather than a gap. Name and facts still span
     both, because a name that stopped half way across would read as a column
     heading rather than a title. */
  .personhead .pinfo {
    max-width: none; flex: 1 1 auto;
    display: grid; align-items: start;
    /* 34rem is the floor because that is what the four figures need to stay
       on one line - measured at 522px of numerals, labels and gaps. At 20rem
       the left column came out 300px on a 1200 window and they wrapped two,
       one, one, which is the exact raggedness this was moved to fix. */
    grid-template-columns: minmax(34rem, 1fr) minmax(0, 50rem);
    column-gap: 3.2rem;
  }
  .personhead .pname,
  .personhead .pfacts { grid-column: 1 / -1; }
  .personhead .pstats,
  .personhead .pcareer { grid-column: 1; }
  .personhead .pbio { grid-column: 2; grid-row: 3 / span 2; max-width: none; }
  /* The gap comes off the figures as well as the floor going on the column.
     It is set by 3vw elsewhere, which is 39px at this width and made the four
     of them 522 wide - eight past a 34rem column, so they wrapped two and two
     at exactly the size chosen to stop them wrapping. 24px puts them at 477
     with room to spare, and they are a set of four related numbers rather than
     four separate ones, so closer suits them anyway. */
  .personhead .pstats .figures { gap: 1.6rem; }
  /* A timeline in newspaper columns, because a timeline has ONE order and a
     grid destroyed it. Packed into a grid the years came out 2028, 2027, 2026
     across the top and 2028, 2024, 2022 down the first column - so whichever
     way you read it you were skipping years, and because the packed rows do
     not line up there was nothing to tell you which way was meant.
     Columns give the sequence back: straight down the first column, 2028 to
     2025, then back to the top of the next for 2024. That is how a printed
     filmography reads, and it is the same fix the Coming up agenda needed for
     the same reason.
     30rem, so a row of poster, title and tail is not ellipsed - measured at
     26rem, where a 1890 window drew four 417px columns and the long titles
     cut. */
  .ptl {
    max-width: none; display: block;
    columns: 30rem; column-gap: 2.6rem;
  }
  /* A year is never split. Broken, its label heads an empty space in one
     column while its films head nothing in the next. */
  .tl-year { break-inside: avoid; }
  /* Sticky is meaningless inside a column - there is no scroll of its own to
     stick within, and a sticky box in a fragmented flow is not something to
     rely on. The year sits with its films. */
  .ptl .tl-y { position: static; }
  .ptl > .tl-year .tl-items::before { top: 0; bottom: .9rem; height: auto; }
  /* The tail rides with the title instead of with the column edge. A row is
     poster / title / tail, and the title track was 1fr - which at 340px put
     Series about a centimetre from the name and at 570px put it four, marooned
     in white space with the wrong row's name nearest it. max-content sizes the
     title to the title, and start stops the leftover space being handed to the
     tail's own track. */
  .ptl .tl-it {
    grid-template-columns: 3rem minmax(0, max-content) auto;
    justify-content: start;
  }

  /* The week, several days at a time. Each day is one .cw-grp so a column
     break can never land between a day and the films under it. */
  /* Newspaper columns, not a grid, and the difference is what the days do
     when they are uneven - which they always are, four things today against
     one tomorrow.
     A grid makes every row as tall as its tallest cell, so one busy day held a
     column of air open beside it in all the others. Packing the rows fixed the
     air but not the reading: with the groups riding up to different heights
     there is no longer a row to read across, and nothing tells you whether the
     next day is to the right or below.
     Multicol answers both at once. It balances the columns itself, so no
     column is much longer than another, and it keeps the one order this
     content has - down a column, then back to the top of the next, which is
     how a page of dates has always been read. It is also less machinery: no
     measuring, no spans, no observer of its own.
     34rem, so the count follows the width and --ui together and there is no
     second breakpoint to keep in agreement with the first. */
  .calwid .cw-list {
    display: block; columns: 34rem; column-gap: 2.6rem;
  }
  /* A day is never split across a column break. It is a rule with its
     programmes under it; broken, the rule heads an empty space in one column
     and the programmes head nothing in the next. */
  .calwid .cw-grp { break-inside: avoid; }
  /* Block, and that is not decoration: column-span applies to block-level
     boxes only, and a <button> is inline-block, so the spanner was being
     ignored outright and the line was flowing as the last item of whichever
     column it fell in - under Sat 5 Sep rather than under the week. */
  .calwid .cw-rest { display: block; width: 100%; column-span: all; }

  /* The day wall that stood here is gone. Both screens are month grids now,
     and a grid needs no measure cap, no date gutter and no four-track row -
     which is what took 798px of dead window at 1999px with it. */
}

/* Three columns of reviews once there is room for three readable ones, and the
   box office bar can afford to be longer - it is a bar, and a longer bar reads
   its own proportion better rather than worse. */
@media (min-width: 1700px) {
  /* No column count here any more - auto-fill above decides how many 34rem
     tracks fit, which is 2 at 1200 and 3 by about 1800 without a second
     breakpoint having to agree with the first. */
  .boxo { max-width: 46rem; }
}

/* ---- the search field, on the screen it is about ----
 *
 * The header carried this on every screen and now carries a magnifier that
 * opens the screen instead. Same recipe as the pill it replaces - a rounded
 * ground, the icon at the head, a clear control that is ours rather than the
 * platform's - but in page colours rather than the over-video ones, because it
 * is no longer sitting on an arbitrary frame.
 *
 * The 2.9rem floor and the car's 3.5rem are the same numbers the header pill
 * carried, and they matter here for the same reason: this is the one entry
 * point to the whole search flow and it is reached without looking. */
.sfield {
  display: flex; align-items: center; gap: .55rem;
  max-width: 34rem; min-width: 0;
  /* The page gutter, supplied here rather than inherited: .body-clear carries
     no horizontal padding and every child on this screen brings its own, so
     without this the field ran the full width of a phone while the head above
     it sat on the margin. */
  margin: 0 var(--pad) 1rem;
  background: var(--elev); border: .06em solid var(--line-hi);
  border-radius: 99px; padding: 0 1rem; min-height: 2.9rem;
  /* One number for the clear disc, because the hit area is derived from it
     and the two must not be able to drift apart by hand. */
  --sfx: 1.8rem;
  transition: border-color .2s, background .2s;
}
.sfield:focus-within { border-color: #fff; background: var(--elev2); }
.sfield .sf-i { display: grid; place-items: center; flex: none; color: var(--dimmer); }
.sfield .sf-i svg { width: 1.1rem; height: 1.1rem; display: block; }
.sfield:focus-within .sf-i { color: var(--fg); }
.sfield .sf-in {
  flex: 1 1 auto; min-width: 0;
  background: none; border: 0; outline: none; color: var(--fg);
  font: inherit; font-size: 0.9333rem; font-weight: 600; padding: .5rem 0;
}
.sfield .sf-in::placeholder { color: var(--dimmer); }
/* The platform's own clear button is drawn as a platform control and matches
   nothing else here, so it is suppressed and replaced. Abandoning a long term
   cost nine backspaces on a car keyboard without one. */
.sfield .sf-in::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
/* A shape at rest, which it did not have. It was background: none until a
   :hover the car cannot send and a phone does not have - so on both of the
   devices this field is actually used from, the only control inside it was an
   unbounded cross at var(--dimmer) that grew a body on an interaction the
   hardware cannot perform. Every other icon-only control in this app is a
   disc before you touch it.
   The word is what gives the two section Clears their shape. There is no room
   for a word in a field, so this one gets the disc instead. */
.sfield .sf-x {
  flex: none; position: relative; display: grid; place-items: center;
  width: var(--sfx); height: var(--sfx); border-radius: 99px;
  background: var(--elev2); border: 0; cursor: pointer; color: var(--dim);
  transition: color .18s var(--ease), background .18s var(--ease),
              transform .15s var(--ease);
}
.sfield .sf-x:hover, .sfield .sf-x:focus-visible {
  color: var(--fg); background: rgba(255, 255, 255, .14);
}
.sfield .sf-x:active { transform: scale(.9); }
.sfield .sf-x svg { width: .95rem; height: .95rem; display: block; }
/* 27px drawn, 52px touchable on a phone; 39px drawn, 52px touchable in the
   car, where the whole box still fits inside the field's own 52.5px pill and
   overhangs nothing. Vertical only: a box reaching left would swallow the last
   centimetre of the caret's own field. And padding is not an option - this is
   a flex child of .sfield, so padding on it grows the field. */
.sfield .sf-x::after {
  content: ''; position: absolute;
  inset: min(0px, calc((var(--sfx) - 52px) / 2)) 0;
}

@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .sfield { min-height: 3.5rem; max-width: 40rem; --sfx: 2.6rem; }
  .sfield .sf-in { font-size: 1.0333rem; }
  .sfield .sf-i svg { width: 1.25rem; height: 1.25rem; }
  .sfield .sf-x svg { width: 1.15rem; height: 1.15rem; }
}

/* ---- asking in words ----
 * The row is a DOOR, and it is open from the frame the key probe answers, at
 * any field length. It used to arrive on the third keystroke on top of results
 * that had already landed for the two-character prefix - so the one screen
 * whose head is otherwise reserved to the pixel was shoving itself down. Being
 * there at idle removes that shove rather than adding one, which is why the
 * invitation and the fix are the same change.
 *
 * Every chip in it clears 52px through the shared floor list above. The
 * sparkle marks a press that SPENDS and nothing else in this design does -
 * that is the whole cost vocabulary, and it is learned in one sitting.
 */
.sask { display: flex; flex-wrap: wrap; gap: .35rem .5rem; margin: 0 var(--pad) 1rem; }
.sask-row { display: flex; flex-wrap: wrap; gap: .5rem; width: 100%; }
/* Without an explicit size an SVG carrying only a viewBox expands to fill its
   button, which is how this first shipped: a sparkle the size of a poster. */
.sask .chipbtn, .ask-back .chipbtn, .ask-turn .chipbtn { gap: .45rem; }
.sask .chipbtn svg, .ask-back .chipbtn svg, .ask-turn .chipbtn svg {
  width: .95rem; height: .95rem; flex: none;
}
/* Its natural width while it says "Ask in words", so a remembered question
   fits beside it on one line at 412px rather than wrapping under it and
   spending 110px of a phone's idle screen on a door. It only claims room once
   it is carrying the reader's own sentence, and by then it is the only chip in
   the row on a narrow screen anyway. */
.sask-row .sask-b { flex: 0 1 auto; max-width: 100%; }
.sask-row .sask-b.spend { flex: 1 1 12rem; max-width: 22rem; }
/* Beaten by .sask-row .chipbtn's max-width: 100% at one class more, which is
   how a 62-character question drew at full width with its ellipsis unreached.
   Scoped to match. */
.sask-row .sask-q { flex: 0 1 auto; max-width: 17rem; }
.sask-b span, .sask-q span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Grey, and it says what a press costs. There is no bare .note rule in this
   sheet, so the old hint rendered at full --fg - which made the loudest thing
   in the row a sentence about what NOT to type. */
.sask-n { width: 100%; font-size: 0.7333rem; color: var(--dimmer); }

/* ---- the answer ----
 * No border, and that is a decision rather than an omission. The verdict
 * card's frame is this app's one device for "generated, not fetched", and its
 * own comment says the frame means something because it is the only one on
 * this sheet. Spending it here would buy nothing the words are not already
 * saying, and would make an eight-poster wall visually heavier than the
 * catalogue wall directly below it.
 */
.askp { margin: 0 0 1.4rem; }
.ask-k { margin: 0 var(--pad) .5rem; }
/* Your words, at full strength, in every state. The old busy heading named the
   question and the landed one dropped it, so an answer could be read with no
   idea at all what produced it. */
.ask-q {
  margin: 0 var(--pad) .35rem; max-width: 62ch;
  font-size: 1rem; line-height: 1.5; color: var(--fg);
}
/* The one cyan here, and it is on the query string, which is exactly where the
   accent's definition puts it: you have narrowed the question that leaves this
   machine. A refinement clause IS that, in the present tense. The root stays
   white, so after three turns the shape of what you have done is legible
   without reading it. */
.ask-nar { font-style: normal; color: var(--ask-tint); }
/* Promoted out of .sec .note - 0.7333rem in the dimmest grey, the slot a rail
   uses for "12 titles" - because it is the only thing on screen explaining the
   list. */
.ask-note {
  margin: 0 var(--pad) .8rem; max-width: 62ch;
  font-size: 0.9333rem; line-height: 1.55; color: var(--dim);
}
.ask-bq { margin: 0 var(--pad) .45rem; font-size: 0.8rem; color: var(--dim); }
.ask-back, .ask-turn {
  display: flex; flex-wrap: wrap; gap: .5rem; margin: 0 var(--pad) 1rem;
}
.ask-turn { margin-top: .2rem; }
.ask-wall > .grid, .ask-wall > .rows { padding: 0 var(--pad); }
/* Where it came from and how big it was. Last, smallest, dimmest, and never
   absent - the role the verdict card's source line already plays. */
.ask-src {
  margin: .2rem var(--pad) 0;
  font-size: 0.7333rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--dimmer);
}
/* Amber's one job in this app is THIS ANSWER IS NOT THE WHOLE ANSWER, and a
   count of names the lookup refused is precisely that. On its own line, rather
   than welded to the model's sentence by a middot in the same grey. */
.ask-cut {
  margin: .35rem var(--pad) 0;
  font-size: 0.7333rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--hide-tint);
}
/* Not .empty. That is this app's TERMINAL message class and a wait is not a
   terminal message - the old busy paragraph wore it anyway. subpulse is the
   same keyframe the subtitle button uses for the same fact about the same kind
   of moment, and it needs no reduced-motion rule: it is 0%,100% { opacity: 1 },
   so the global block leaves it resting at full strength on its own. */
.ask-wait {
  margin: .2rem var(--pad) 0; font-size: 0.8rem; color: var(--dim);
  animation: subpulse 1.9s var(--ease) infinite;
}
.ask-sk {
  display: block; height: .72rem; border-radius: var(--r-s);
  margin: 0 var(--pad) .8rem; max-width: 28rem;
}
.askp .skelgrid { padding: 0 var(--pad); }

@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  .sask-n { font-size: 0.8667rem; }
  /* Room for a whole composed question at 1307px, where there is plenty. */
  .sask-row .sask-q { max-width: 26rem; }
  .ask-q { font-size: 1.1rem; }
  .ask-note { font-size: 1rem; }
  .ask-bq { font-size: 0.8667rem; }
  .ask-wait { font-size: 0.9rem; }
}

/* ---- what you opened, on the screen you go to when looking for something ----
   The empty search screen was a sentence and two word chips. Terms are a thin
   record of a visit - "cat" says what you asked, not what you found - and the
   thing wanted again is usually the title rather than the words that reached
   it. */
.sopened { margin: 1.4rem 0 0; }
.sopened > .grid { padding: 0 var(--pad); }

/* ---- refresh, in the corner ----
 * Out of the header entirely. It is the one control on the bar that is not
 * about where you are - everything else there navigates, and this pulls new
 * content - so it was the odd thing in a row of tabs, and it is the only one
 * you ever want while looking at something further down the page.
 *
 * A child of .screen rather than of .top, and that is structural rather than
 * tidy: .top centres itself with a transform, which makes it a containing
 * block for fixed descendants, so a fixed child of the header anchors to the
 * header. The nav bar hit exactly this and moves in the DOM for the same
 * reason.
 *
 * Same ground as both capsules, at the same .5 whose contrast floor was worked
 * out against a near-white frame with no blur.
 */
.screen > .fab {
  position: fixed; z-index: var(--z-corner);
  right: var(--pad); bottom: calc(var(--pad) + env(safe-area-inset-bottom, 0px));
  width: 3rem; height: 3rem; border-radius: 50%;
  display: grid; place-items: center;
  color: rgba(255,255,255,.78);
  background: rgba(0, 0, 0, .55);
  border: .06em solid var(--line-hi);
  backdrop-filter: var(--glass);
  -webkit-backdrop-filter: var(--glass);
  transition: background .2s, color .2s, opacity .22s var(--ease),
              transform .22s var(--ease);
}
.screen > .fab svg { width: 1.15rem; height: 1.15rem; }
/* Out of the way while its own panel is up. The panel is what the button opens
   and it reports everything the button could, so leaving a live control
   underneath it is offering to start a thing that is already running. It
   shrinks into the corner rather than blinking out, and comes back the same
   way as the panel leaves - the two motions cross, which is what makes it read
   as one control becoming another rather than two things flickering. */
html.tb-panel .screen > .fab {
  opacity: 0; transform: scale(.55) translateY(.5rem); pointer-events: none;
}
/* White while a pull is running, the same as the header's version always was.
   This was briefly replaced by a box-shadow ring on the belief that `color`
   could not be overridden here - it could; every property that appeared
   immovable is simply one this transition list animates, and the browser it
   was tested in never ticks a transition, so the computed value read back
   frozen at its starting point. The ring was a fix for a bug that did not
   exist. */
.screen > .fab.busy { color: #fff; }
/* And from the root as well, for the reason the spin is keyed there: chrome()
   rebuilds this button on every screen change, so a class written onto it goes
   grey again the moment you navigate mid-pull. */
html.tb-pulling .screen > .fab { color: #fff; }
.screen > .fab:hover { color: #fff; }
.screen > .fab:active { transform: scale(.92); }

/* ---- the ring: a run, seen from outside its panel ---- */
/* Six segments of arc rather than a percentage, because the relay reports a
   phase and has never reported a fraction - so this draws exactly what the
   panel's own track draws, in the one place still on screen after the panel is
   dismissed. The angle arrives as --pull on the ROOT element, which is why
   none of this needs re-applying when chrome() rebuilds the button on every
   screen change. */
.screen > .fab .fabring {
  position: absolute; inset: -.32rem; width: auto; height: auto;
  pointer-events: none;
  transform: rotate(-90deg);              /* twelve o'clock, where a dial starts */
  opacity: 0; transition: opacity .3s var(--ease);
}
.screen > .fab .fabring circle { fill: none; stroke-width: 2.4; }
.screen > .fab .fabring .fr-t { stroke: rgba(255, 255, 255, .16); }
.screen > .fab .fabring .fr-b {
  stroke: #fff; stroke-linecap: round;
  /* 2 pi r at r = 20, in the viewBox's own units. */
  stroke-dasharray: 125.66;
  /* Eased, because the steps arrive as jumps a second and a half apart: the
     sweep between two of them is the only thing that says the job moved rather
     than the poll landing. */
  stroke-dashoffset: calc(125.66px * (1 - var(--pull, 0)));
  transition: stroke-dashoffset .5s var(--ease);
}
html.tb-pulling .screen > .fab .fabring,
html.tb-pulldone .screen > .fab .fabring { opacity: 1; }
/* Before the first phase can be placed. A ring sitting at zero for the opening
   seconds of every pull reads as a control that failed to start, so it travels
   instead - and stops travelling the moment there is a real step to show. */
html.tb-pullwait .screen > .fab .fabring .fr-b {
  stroke-dasharray: 26 100; stroke-dashoffset: 0;
  transition: none; animation: fabsweep 1.3s linear infinite;
}
@keyframes fabsweep { to { stroke-dashoffset: -125.66px; } }
/* The finish: it completes, swells once, and goes. This is the only thing the
   button says about how a run ended and it only says it for a success - a
   failure empties the ring in silence, because a ring that fills either way is
   a lie told in the one place with no room to correct it. The panel is where a
   failure is explained, and the button is how you get back to it. */
html.tb-pulldone .screen > .fab .fabring { animation: fabpop .76s var(--ease) both; }
html.tb-pulldone .screen > .fab { color: #fff; }
@keyframes fabpop {
  0%   { opacity: 1; transform: rotate(-90deg) scale(1); }
  40%  { opacity: 1; transform: rotate(-90deg) scale(1.16); }
  100% { opacity: 0; transform: rotate(-90deg) scale(1.16); }
}

@media (max-width: 700px) {
  /* Above the bar, not beside it - the bar spans the screen, so there is no
     beside. Measured against the bar rather than guessed: it comes to 62px
     tall at the default interface size plus whatever chin the phone asks for,
     and the button needs to clear that with a gap of its own. */
  .screen > .fab {
    bottom: calc(5.4rem + env(safe-area-inset-bottom, 0px));
  }
}
@media (pointer: coarse) and (min-width: 820px) and (min-height: 600px) {
  /* The floor everything reached without looking clears in this block. */
  .screen > .fab { width: 3.5rem; height: 3.5rem; }
  .screen > .fab svg { width: 1.3rem; height: 1.3rem; }
}

/* The visitor key rows, on This device.

   .set-opts is flex-wrap by design - it exists for pills, which SHOULD sit
   side by side and reflow. These are three labelled text fields, and left on
   that rule they wrapped two-up with TorBox alone underneath, which reads as
   a layout accident rather than a list of three equal things. Column, and
   each row is its own flex line so the button sits against its own field. */
.settings .set-opts.svc-rows { flex-direction: column; align-items: stretch; }
.settings .svc-row { display: flex; gap: .5rem; align-items: center; }
.settings .svc-row input { flex: 1 1 auto; min-width: 0; }

/* A service is now the input line PLUS a sentence saying what breaks without
   it, so it needs a block of its own - .svc-row is a flex line and a hint
   dropped into it would sit beside the field. The .35rem ties the sentence to
   its own input, against the .5rem that .set-opts puts between services, so
   the three read as three groups rather than six loose lines.

   The link takes no colour of its own on purpose. Cyan and amber both mean
   something specific in this app - narrowed, and hidden - and "where to get a
   key" is neither; an underline is enough to read as a link without claiming
   one of those. nowrap keeps "Get a PublicMetaDB key" from breaking across
   lines mid-phrase on a narrow card. */
.settings .svc-item { display: flex; flex-direction: column; gap: .35rem; }
.settings .svc-item .set-h { margin: 0; }
.settings .svc-get { color: inherit; text-decoration: underline;
                     text-underline-offset: 2px; white-space: nowrap; }
.settings .svc-get:hover { color: var(--fg); }

/* The service key boxes and the Server address field are rows of the same
   shape. Sized rather than left to the input default so they do not sit
   narrower than the buttons beside them on a wide card. (A Relay key box
   used the same rule and is gone; the class is not.) */
.settings .set-rk { flex: 1 1 auto; min-width: 0; }
