:root {
  --bg: #0b0f1a;
  --surface: #141b2b;
  --surface-2: #1c2540;
  --border: #24304d;
  --text: #e8ecf4;
  --muted: #8b94a8;
  /* The logo's own electric blue and vivid gold. The plain accent is a shade
     deeper than the logo blue, so white text on a button still has contrast;
     the bright one is the logo blue itself, for glows and highlights. */
  --accent: #1f8fd6;
  --accent-bright: #00b0f3;
  --accent-2: #fdb90a;
  --accent-2-bright: #ffce45;
  --danger: #ef4444;
  --success: #22c55e;
  /* The same colours again, in a form that lets a glow set its own
     transparency without anybody hand-converting them a second time. **That
     second copy is exactly how nine glows once drifted off the brand
     entirely.** */
  --accent-rgb: 31 143 214;
  --accent-bright-rgb: 0 176 243;
  --accent-2-rgb: 253 185 10;
  --danger-rgb: 239 68 68;
  --success-rgb: 34 197 94;
  /* A link: blue into gold, the same as the mark itself where its two strokes
     cross. **Not** the other gradient in the wordmark, which is an unrelated
     blend where the letters overlap — don't "fix" this to match that one. */
  --link-from: var(--accent-bright);
  --link-to: var(--accent-2);
  /* The focus ring, named once so there is one place to change it. */
  --focus-ring: 2px solid var(--accent-bright);
  --focus-ring-offset: 2px;
  /* How tall the bar at the top is. Anything else that sticks to the top
     offsets by this, so it clears the header. */
  --header-h: 52px;
  /* How wide the content gets on a big screen. The header's inner row matches
     it, so the logo lines up with everything underneath. */
  --stage-w: 1320px;

  /* Corner rounding. These were already the sizes in use — named here rather
     than invented. A full circle stays written out where it is used, because
     it says what it is. */
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 12px;
  --radius-pill: 999px;

  /* Spacing. There was no scale at all before this — twenty-nine different
     sizes, most used exactly once. A plain grid, here for new work to reach
     for rather than forced back onto everything already written. */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;

  /* Text sizes: sixteen near-identical ones collapsed into nine real steps.
     The fixed-width family is for anything that counts down or changes in
     place, where characters of differing width would make the whole line
     jitter. */
  --font-sans: system-ui, sans-serif;
  --font-mono:
    ui-monospace, 'SF Mono', 'Cascadia Code', 'Roboto Mono', Consolas,
    'Courier New', monospace;
  --text-2xs: 0.65rem;
  --text-xs: 0.75rem;
  --text-sm: 0.85rem;
  --text-base: 1rem;
  --text-md: 1.05rem;
  --text-lg: 1.3rem;
  --text-xl: 1.6rem;
  --text-2xl: 2rem;
  --text-display: 3rem;

  /* Shadows. Six declarations already shared one colour at four sizes, two of
     them identical and unnamed; this is those, named. */
  --elevation-1: 0 4px 14px rgba(5, 8, 15, 0.6);
  --elevation-2: 0 6px 24px rgba(5, 8, 15, 0.6);
  --elevation-3: 0 8px 28px rgba(5, 8, 15, 0.6);
  --elevation-4: 0 12px 32px rgba(5, 8, 15, 0.65);

  /* Dark only. It always was, but nothing said so — which left the browser's
     own controls and scrollbars guessing. */
  color-scheme: dark;
}

* {
  box-sizing: border-box;
}

/* Buttons and boxes do not inherit the page's font on their own. Left alone
   the browser gives them its own, so a label inside a button came out in a
   different typeface from the text beside it. */
button,
input,
select,
textarea {
  font: inherit;
}

/* One focus ring for everything. Each control used to declare its own, which
   is how four of them ended up with a ring and the rest with nothing. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

/* No width cap here: each screen sets its own. Capping the page itself would
   force the wide screens to break back out of it, which is what the old layout
   did.

   --blank-room is extra empty space under the footer, so the last blanks can
   scroll to the top. fitDailyBlankRoom in app.js sets it; it is zero the rest
   of the time. */
body {
  font-family: var(--font-sans);
  margin: 0;
  padding: 0 1rem calc(4rem + var(--blank-room, 0px));
  background: var(--bg);
  color: var(--text);
  /* The footer sits at the end of the page, never in the middle of it: the
     body fills the viewport and the screen on show takes up whatever slack is
     left. Here rather than per screen, which is how a screen came to be
     missing it. */
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}

.screen:not(.hidden) {
  flex: 1 0 auto;
}

/* The plain prose pages — privacy, terms, data deletion. They use the type
   and spacing scales directly rather than any component. */
.legal-page {
  padding-block: var(--space-6) var(--space-7);
}

.legal-page h1 {
  font-size: var(--text-2xl);
  margin-bottom: var(--space-2);
}

.legal-page .legal-updated {
  color: var(--muted);
  font-size: var(--text-sm);
  margin-top: 0;
  margin-bottom: var(--space-6);
}

.legal-page h2 {
  font-size: var(--text-lg);
  margin-top: var(--space-7);
  margin-bottom: var(--space-3);
}

.legal-page p,
.legal-page li {
  line-height: 1.6;
  color: var(--text);
}

.legal-page ul,
.legal-page ol {
  padding-left: var(--space-5);
}

.legal-page a {
  color: var(--accent-bright);
}

/* The page you land on for a moment while a sign-in finishes. It only needs
   not to be a flash of unstyled text in the corner. */
.auth-callback {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  color: var(--muted);
  font-size: 0.95rem;
}

/* When a sign-in cannot finish here, the page stays and says why instead of
   dropping the player on the front page, signed out, with nothing to go on. */
.auth-callback-panel {
  max-width: 22rem;
  padding-inline: var(--space-4);
  text-align: center;
}

.auth-callback-panel h1 {
  margin: 0 0 var(--space-3);
  color: var(--text);
  font-size: var(--text-lg);
}

/* Focused for a screen reader's sake, not as something to tab to. */
.auth-callback-panel h1:focus {
  outline: none;
}

.auth-callback-panel p {
  margin: 0 0 var(--space-5);
  line-height: 1.45;
}

.auth-callback-panel .btn {
  display: block;
  width: 100%;
  margin: 0;
  text-decoration: none;
}

/* Room held for the message a tap on Copy writes, so the button does not jump. */
.auth-callback-panel .auth-callback-copy-status {
  min-height: 2.9em;
  margin: var(--space-3) 0 0;
  font-size: var(--text-sm);
}

/* A screen takes focus when it arrives, so the next Tab starts inside it. It
   is not a control, though, and a ring drawn round a whole screen reads as an
   error — the visible feedback is the first thing tabbed to. */
.screen:focus,
.screen:focus-visible {
  outline: none;
}

/* Site-wide ambiance: a faint brand-blue glow anchored to the top of the
   viewport. Fixed (not scrolled) and behind everything, so it never distracts. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(
    1200px 500px at 50% -10%,
    rgb(var(--accent-bright-rgb) / 0.1),
    transparent 60%
  );
}

/* A cinema's own falloff toward the edges of the room, drawn over the glow
   above rather than beside it, and behind everything else on the page. Kept
   very quiet on purpose: texture on a second look, never a costume. */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(
    ellipse 90% 70% at 50% 50%,
    transparent 55%,
    rgba(0, 0, 0, 0.35) 100%
  );
}

/* The header. A slim bar everywhere, turned into a lit hero on the landing by
   an override further down.

   The bar itself runs edge to edge — the negative margin cancels the page's own
   side padding — while the row inside it keeps the logo lined up with the
   content below. Capping the bar itself left it stopping abruptly mid-screen,
   and every wide screen then needed its own override to compensate. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  height: var(--header-h);
  margin: 0 -1rem 1.25rem;
  padding: 0 1rem;
  background: rgba(20, 27, 43, 0.72);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border);
}

.site-header-inner {
  display: flex;
  align-items: center;
  height: 100%;
  max-width: var(--stage-w);
  margin-inline: auto;
}

/* A button that still looks like the plain logo. */
.site-brand {
  display: inline-flex;
  align-items: center;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.site-logo {
  display: block;
  height: 30px;
  width: auto;
}

/* Pushed to the far edge of the header's inner row, so it lines up with the
   content below exactly as the logo does. A row rather than the default
   stacking, or two buttons here would sit one above the other and the second
   would fall out through the bottom of the header. */
.header-actions {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: auto;
}

/* One of these per button-and-panel pair, so each panel hangs under its own
   button rather than under whichever one happens to be rightmost. */
.header-menu {
  position: relative;
  display: flex;
}

.header-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 1px solid var(--border);
  /* **Round, unlike every other small control here**, because a ring is drawn
     around it when it opens. From a rounded square that is a change of shape —
     one outline fading out while a different one sweeps in. Round, the same
     sweep reads as the button putting out a halo. Signed in, what is inside is
     a circle too. */
  border-radius: 50%;
  background: var(--surface);
  color: var(--muted);
  cursor: pointer;
  transition:
    color 0.15s,
    border-color 0.15s,
    background 0.15s;
}

.header-btn svg {
  width: 19px;
  height: 19px;
}

/* Only where there is a real pointer: a tap on a phone leaves hover stuck on
   the button until something else is tapped, which lit its border up and left
   it lit underneath the ring. */
@media (hover: hover) {
  .header-btn:hover {
    color: var(--text);
    border-color: var(--accent-bright);
  }
}

/* Hover alone isn't enough to mark which button a visible popover belongs
   to once there are two of them side by side — the icon itself tints, and
   .header-btn-orbit (below) draws the rest of the "open" signal.

   The button's own border and fill go while it's open, so the orbit is the
   only ring on screen — two concentric circles 5px apart, both lit, read as a
   target rather than as one highlight. Signed in this is invisible either way:
   the avatar covers the button's own edge (see .account-btn-avatar). */
.header-btn[aria-expanded='true'],
.header-btn.is-pulsing {
  color: var(--accent-bright);
  border-color: transparent;
  background: transparent;
}

/* A halo that draws itself round the button as it opens, rather than a flat
   change of colour. The sweeping line and the dot travelling along it are the
   game's own two marks — a link and a node — so the one interactive corner of
   the header still speaks the game's language.

   It sits *beside* the button rather than inside it, because the button
   rewrites its own contents to swap icons and would wipe out a ring nested
   within. */
.header-btn-orbit {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
  pointer-events: none;
}

.header-btn-orbit svg {
  width: 100%;
  height: 100%;
  overflow: visible;
}

.orbit-ring {
  fill: none;
  stroke: var(--accent-bright);
  stroke-width: 1.5;
  stroke-linecap: round;
  transform-origin: 22px 22px;
  transform: rotate(-90deg);
  stroke-dasharray: 120;
  stroke-dashoffset: 120;
  opacity: 0;
  transition:
    stroke-dashoffset 0.45s ease,
    opacity 0.2s ease;
}

.orbit-dot {
  fill: var(--accent-2);
  opacity: 0;
  transform-origin: 22px 22px;
  transform: scale(0.4);
  transition:
    opacity 0.2s ease 0.35s,
    transform 0.2s ease 0.35s;
}

/* The same "open" look, held for one sweep by hand: signed out the button goes
   somewhere instead of opening a panel, and the tap deserves the same answer
   either way. */
.header-btn[aria-expanded='true'] ~ .header-btn-orbit .orbit-ring,
.header-btn.is-pulsing ~ .header-btn-orbit .orbit-ring {
  stroke-dashoffset: 0;
  opacity: 0.9;
}

.header-btn[aria-expanded='true'] ~ .header-btn-orbit .orbit-dot,
.header-btn.is-pulsing ~ .header-btn-orbit .orbit-dot {
  opacity: 1;
  transform: scale(1);
}

@media (prefers-reduced-motion: reduce) {
  .orbit-ring,
  .orbit-dot {
    transition: opacity 0.15s ease;
  }
}

.settings-panel {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  z-index: 60; /* above the header itself */
  width: 15rem;
  padding: 0.85rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2);
  box-shadow: var(--elevation-3);
  animation: header-panel-enter 0.12s ease-out;
}

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

@keyframes header-panel-enter {
  from {
    opacity: 0;
    transform: translateY(-4px);
  }
}

.settings-row {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.3rem 0;
  font-size: 0.85rem;
  cursor: pointer;
}

.settings-row input {
  flex: none;
  width: 15px;
  height: 15px;
  accent-color: var(--accent-bright);
  cursor: pointer;
}

.settings-note {
  margin: 0.5rem 0 0;
  color: var(--muted);
  font-size: 0.75rem;
  line-height: 1.45;
}

/* The account panel. The same shape as the panel above, so it wears that class
   too and only adds what differs. */
.account-panel {
  width: 16rem;
}

/* Fills the button rather than sitting inside it: two circles a few pixels
   apart read as a doughnut, and the avatar is a perfectly good outline for the
   control on its own. What is left of the button around it is its border,
   which reads as a thin rim. */
.account-btn-avatar {
  width: 100%;
  height: 100%;
  background: var(--border);
  color: var(--text);
  font-size: 0.9rem;
  font-weight: 700;
}

.btn-provider {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition:
    border-color 0.15s,
    background 0.15s;
}

.btn-provider:hover {
  background: var(--border);
  border-color: var(--accent-bright);
}

.btn-provider img {
  flex-shrink: 0;
}

#account-claim-form input[type='text'],
#magic-link-form input[type='email'] {
  width: 100%;
  box-sizing: border-box;
  margin-bottom: var(--space-2);
}

.account-claim-submit {
  width: 100%;
  margin: 0;
}

.account-handle {
  margin: 0 0 var(--space-3);
  font-weight: 700;
  word-break: break-word;
}

.account-panel-btn {
  display: block;
  width: 100%;
  box-sizing: border-box;
  margin: 0 0 var(--space-2);
}

.account-panel-btn:last-child {
  margin-bottom: 0;
}

/* Who you are, above the numbers. Centred because everything else in this
   card is — the figures are a readout, not a form. */
.account-identity {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-5);
}

.account-identity .avatar {
  width: 56px;
  height: 56px;
  background: var(--border);
  color: var(--text);
  font-size: 1.3rem;
  font-weight: 700;
  flex-shrink: 0;
}

.account-identity-name {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 700;
}

/* Behind a line and last: the one thing here that cannot be undone shares a
   card with the numbers, so distance is what keeps them apart. */
.account-danger {
  margin: var(--space-5) 0 0;
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

#btn-delete-account {
  margin: 0;
}

/* Narrower than a dialog normally is: a row of figures is the bulk of what is
   here, and a wide card would stretch them into unrelated numbers rather than
   one readout. Written against the dialog class as well, so it beats the width
   that class sets further down the file. */
.modal-card.account-card {
  max-width: 24rem;
  text-align: center;
}

/* The numbers, drawn as one card wherever they appear — the recap and the
   account screen show the same markup and **must look like the same thing,
   because they are**. On the recap that card is what separates them from the
   chain above; in the dialog it groups them apart from the name above and the
   delete button below. */
.stats-box {
  width: 100%;
  max-width: 24rem;
  padding: var(--space-5);
  border-radius: var(--radius-lg);
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--elevation-1);
  text-align: center;
}

.stats-title {
  margin: 0 0 var(--space-4);
  font-size: var(--text-xs);
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.12em;
}

/* Three across and two down: one thing per column, one span of time per row.
   It was a single row until the chain-length pair arrived, which would have
   made it six across on a phone. */
.stats-figures {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-4) var(--space-2);
}

.stats-figure {
  min-width: 0;
}

.stats-figure-value {
  display: block;
  font-size: var(--text-xl);
  font-weight: 700;
  line-height: 1.1;
}

.stats-figure-label {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--text-2xs);
  color: var(--muted);
  text-wrap: balance;
}

.stats-next {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

.stats-next-label {
  display: block;
  font-size: var(--text-2xs);
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* Evenly spaced digits, so the countdown does not change its own width every
   second as the numbers change. */
.stats-next-value {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--text-lg);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* The score. The biggest thing on the screen after the title, with the receipt
   under it deliberately quiet — that is there to be checked, not read. */
.recap-score {
  margin: 0 auto;
  width: 100%;
  max-width: 20rem;
  text-align: center;
}

/* The figure and its unit on one line, sitting on the same baseline so the
   small word rides the number's own line rather than floating at its middle.
   Allowed to wrap: a big negative score is wide, and a unit that drops below on
   the narrowest phone beats one that runs off the edge. */
.recap-score-figure {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  /* The line carries the figure's size, so the unit below can be a fraction
     of it and so can the gap — which is how the board's own smaller version
     stays in proportion. */
  font-size: var(--text-2xl);
  gap: 0.15em;
  margin: 0;
}

.recap-score-value {
  font-size: 1em;
  font-weight: 800;
  color: var(--accent-2);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

/* The voice every small line on this screen speaks in. Four things are
   captions here, and each used to have its own size, case and colour — which is
   what made them read as sentences somebody dropped in rather than parts of one
   screen. One class, so they cannot drift apart again. */
.recap-caption {
  font-size: var(--text-2xs);
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.12em;
}

/* The unit, measured against the figure rather than set outright: half the
   number and tight to it, the same pair the board's strip draws. A ratio, so it
   holds if the figure ever changes size. **It has to sit below the caption
   rule**, which it overrides and which would otherwise win on order alone. */
.recap-score-unit {
  font-size: 0.5em;
  letter-spacing: 0.06em;
}

/* Under the figure: the run's shape, on one line. It used to be a second
   paragraph in a different size below this, which cost a line of a screen that
   has none to spare. Gone entirely on a single unextended chain. */
.recap-score-caption {
  display: block;
  margin: var(--space-1) 0 0;
  line-height: 1.5;
}

.recap-score-lines {
  margin: var(--space-4) 0 0;
  font-size: var(--text-sm);
  color: var(--muted);
}

/* Rows: the label to the left, the figure to the right, so the numbers line
   up down the edge like a receipt. */
.recap-score-line {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-1) 0;
}

.recap-score-line dt,
.recap-score-line dd {
  margin: 0;
}

.recap-score-line dd {
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.recap-score-floor {
  margin: var(--space-2) 0 0;
  font-size: var(--text-2xs);
  color: var(--muted);
  text-align: left;
}

.recap-score-note {
  margin: var(--space-3) 0 0;
  font-size: var(--text-xs);
  color: var(--muted);
}

/* The day's opening card: a miniature of the board behind it. The two given
   films at the ends, the same dashed line the board draws between empty nodes
   running between them, and the strip's own two shapes standing in for what is
   still blank. **Nothing here is a new diagram** — it is the game's own
   vocabulary at a size that fits in one look, which is the one thing the board
   itself cannot do, since its first and last node are a screen and a half
   apart.

   One number for the poster width, because the dashed line has to sit on its
   centre and a second hand-typed copy is how those two drift apart. */
.modal-card.daily-intro-card {
  --intro-poster-w: 72px;
  max-width: 24rem;
  padding: var(--space-5);
}

.daily-intro-eyebrow {
  margin: 0;
  padding-right: 2.5rem;
  font-size: var(--text-2xs);
  font-weight: 700;
  color: var(--accent-bright);
  text-transform: uppercase;
  letter-spacing: 0.12em;
}

.daily-intro-eyebrow:empty {
  display: none;
}

.daily-intro-title {
  margin: var(--space-1) 0 var(--space-4);
  padding-right: 2.5rem;
  font-size: var(--text-lg);
  font-weight: 800;
  letter-spacing: -0.01em;
}

.daily-intro-chain {
  display: flex;
  flex-direction: column;
}

.daily-intro-end {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.daily-intro-poster {
  flex: none;
  display: block;
  width: var(--intro-poster-w);
  aspect-ratio: 2 / 3;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--accent-bright);
  box-shadow: 0 0 0 3px rgb(var(--accent-bright-rgb) / 0.12);
}

/* An end that is an actor, which only a chain grown from one has. A circle,
   the way a person is drawn everywhere else — and its border drops to the plain
   one so that the accent colour keeps meaning "film". */
.daily-intro-poster-actor {
  aspect-ratio: 1;
  border-radius: 50%;
  border-color: var(--border);
  box-shadow: none;
}

.daily-intro-poster img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Cut off rather than left to wrap: a long title runs to four lines beside a
   small poster, and this card has two of them plus the gap between to fit on
   screen. */
.daily-intro-end-title {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1.35;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
}

/* Indented onto the title column, so the dots line up with the two titles
   rather than with the posters and the line has the poster column to itself. */
.daily-intro-gap {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-4) 0 var(--space-4)
    calc(var(--intro-poster-w) + var(--space-4));
}

/* The board's own unmade link, stood on end: nothing has been joined here yet,
   so it gets exactly the dash the board draws. **Painted rather than using the
   browser's own dashed border**, because that has a different rhythm — and two
   dashes in one app disagreeing is the kind of drift nobody can name but
   everybody sees. */
.daily-intro-gap::before {
  content: '';
  position: absolute;
  left: calc(var(--intro-poster-w) / 2 - 1px);
  top: 0;
  bottom: 0;
  width: 2px;
  background: repeating-linear-gradient(
    to bottom,
    var(--border) 0 4px,
    transparent 4px 9px
  );
}

.daily-intro-pips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem;
}

/* Hollow, unlike the board's, because every one of these is still blank. The
   same two shapes in the same proportions, so the row met a moment later is
   recognisably this one. */
.daily-intro-pip {
  flex: none;
  border: 1px solid var(--border);
  background: var(--surface-2);
}

.daily-intro-pip-movie {
  width: 9px;
  height: 13px;
  border-radius: 2px;
}

.daily-intro-pip-actor {
  width: 11px;
  height: 11px;
  border-radius: 50%;
}

.daily-intro-gap-label {
  font-size: var(--text-xs);
  color: var(--muted);
}

.daily-intro-copy {
  margin: var(--space-4) 0 0;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--muted);
}

/* Full width, and the only button in the dialog: the ✕ and Escape are the ways
   out, and this is the way on.

   **Written against the card's own class on purpose.** The general button rule
   is declared later in this file and would otherwise win on order alone, which
   silently took back every property the two share.

   The top margin matches the title's bottom margin, so
   the space under the chain is the space above it. */
.daily-intro-card .daily-intro-start {
  width: 100%;
  margin: var(--space-4) 0 0;
  border-radius: var(--radius-pill);
  padding-block: 0.85rem;
}

/* The leaderboard. Ranged left, unlike the centred numbers elsewhere: a
   ranking is a list of rows, and centring a column of names stops the ranks and
   the scores lining up with each other. */
.modal-card.leaderboard-card {
  max-width: 26rem;
  text-align: left;
  padding: var(--space-5) var(--space-5) var(--space-4);
}

.leaderboard-head {
  margin-bottom: var(--space-4);
  /* Clears the close button, which sits over this corner and would otherwise
     land on top of a long puzzle number. */
  padding-right: 2.5rem;
}

.leaderboard-eyebrow {
  margin: 0;
  font-size: var(--text-2xs);
  font-weight: 700;
  color: var(--accent-bright);
  text-transform: uppercase;
  letter-spacing: 0.12em;
}

.leaderboard-eyebrow:empty {
  display: none;
}

.leaderboard-title {
  margin: var(--space-1) 0 0;
  font-size: var(--text-lg);
  font-weight: 800;
  letter-spacing: -0.01em;
}

.leaderboard-sub {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
  color: var(--muted);
}

/* Loading, failed, nobody yet. It sits *under* the permanent subtitle rather
   than replacing it, so the dialog never loses its explanation while it
   waits. */
.leaderboard-note {
  margin: var(--space-3) 0 0;
  font-size: var(--text-sm);
  color: var(--muted);
}

/* Scrolls inside the card rather than making it taller: fifty rows would
   otherwise push the countdown off the bottom of a phone. Measured against the
   *smallest* the screen gets rather than the largest, or the list would sit
   underneath the browser's own bars. */
.leaderboard-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 46svh;
  overflow-y: auto;
}

.leaderboard-list li {
  position: relative;
}

/* One grid, shared by the rows in the list and the pinned "you" row below it,
   so the two line up as one table even though they are different elements.
   The rank column is fixed-width so two- and three-digit places don't shunt
   the names sideways. */
.leaderboard-row {
  position: relative;
  display: grid;
  grid-template-columns: 1.75rem 28px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
}

/* The chain, stood on end. A hairline runs the height of every row behind the
   rank chips, and the chips — opaque, above it — become its links; it is the
   same connector the board draws between nodes, which is what stops the left
   column reading as a stray column of numbers. Trimmed to half height at both
   ends so the chain starts and finishes on a link rather than in mid-air. */
.leaderboard-row::before {
  content: '';
  position: absolute;
  left: calc(var(--space-3) + 0.875rem);
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--border);
}

.leaderboard-list li:first-child .leaderboard-row::before {
  top: 50%;
}

.leaderboard-list li:last-child .leaderboard-row::before {
  bottom: 50%;
}

/* The pinned row below the list is deliberately off the chain — it is not the
   next place down, and a connector into it would say it was. */
.leaderboard-you .leaderboard-row::before {
  display: none;
}

/* A chip, not a numeral: it has to be opaque to break the hairline behind it,
   and a pill takes a third digit without widening the column. */
.leaderboard-rank {
  position: relative;
  z-index: 1;
  justify-self: center;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.625rem;
  height: 1.625rem;
  padding-inline: 0.3rem;
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  border: 1px solid var(--border);
  font-size: var(--text-xs);
  font-weight: 700;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* The podium, and the only colour in the dialog. First place takes the gold
   solid; second and third carry it as an outline, so the three read as one
   group that thins out rather than three unrelated states. Gold and not the
   accent blue because blue is this app's "you can press this". */
.leaderboard-row.is-rank-1 .leaderboard-rank {
  background: var(--accent-2);
  border-color: var(--accent-2-bright);
  color: #1a1204;
  box-shadow: 0 0 0 3px rgb(var(--accent-2-rgb) / 0.14);
}

.leaderboard-row.is-rank-2 .leaderboard-rank,
.leaderboard-row.is-rank-3 .leaderboard-rank {
  border-color: rgb(var(--accent-2-rgb) / 0.55);
  color: var(--accent-2-bright);
}

.leaderboard-row.is-rank-3 .leaderboard-rank {
  border-color: rgb(var(--accent-2-rgb) / 0.3);
}

.leaderboard-row .avatar {
  width: 28px;
  height: 28px;
  background: var(--border);
  font-size: var(--text-xs);
  font-weight: 700;
}

/* Truncates rather than wrapping: a 24-character handle would otherwise take
   two lines and break the row rhythm the ranks depend on. */
.leaderboard-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.leaderboard-score {
  display: flex;
  align-items: baseline;
  gap: 0.25rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Same unit the board's readout and the recap's total print, at the same
   weight — a column of bare four-digit numbers next to a column of ranks
   otherwise reads as a second ranking. */
.leaderboard-score-unit {
  font-size: var(--text-2xs);
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* The player's own row, wherever it appears. Tinted rather than outlined so
   it reads as "this one is yours" without looking like a control. */
/* Five columns rather than four while the chip is present: the name shrinks to
   its own width so the chip sits beside it, and the score keeps the right edge
   the other rows put it on. */
.leaderboard-row.is-you {
  grid-template-columns: 1.75rem 28px minmax(0, auto) auto 1fr;
  background: rgb(var(--accent-rgb) / 0.16);
}

/* :not(...) rather than a later override, because a podium place that happens
   to be yours is still a podium place — the gold has to win, and the "this one
   is yours" job is already done by the tint and the chip. */
.leaderboard-row.is-you:not(.is-rank-1, .is-rank-2, .is-rank-3)
  .leaderboard-rank {
  color: var(--text);
  border-color: rgb(var(--accent-bright-rgb) / 0.5);
}

/* Says which row is yours in words, for the scroll down a fifty-row list where
   a tint alone is something you have to already be looking for. The name it
   follows can truncate; this can't, so it sits outside that column's ellipsis.
   grid-column keeps it on the name's track rather than claiming a fifth. */
.leaderboard-you-chip {
  justify-self: start;
  padding: 0.1rem 0.4rem;
  border-radius: var(--radius-pill);
  background: rgb(var(--accent-bright-rgb) / 0.18);
  color: var(--accent-bright);
  font-size: var(--text-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.leaderboard-row.is-you .leaderboard-score {
  grid-column: 5;
  justify-self: end;
}

/* Pinned below the list when the player didn't make the visible cut. The rule
   above it says "there is a gap here" without printing an ellipsis row. */
.leaderboard-you {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}

/* One line, label left and countdown right — the stacked version in the
   figures box reads as another list entry when it sits under a list. */
.leaderboard-next {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

.leaderboard-next .stats-next-value {
  margin-top: 0;
  font-size: var(--text-base);
  font-family: var(--font-mono);
  color: var(--accent-bright);
}

/* Same idiom as .daily-finish.is-confirming: borrow --danger for the armed
   state instead of a separate confirm dialog — a second modal on top of this
   one would trap focus twice for one irreversible click. */
#btn-delete-account.is-confirming {
  color: var(--danger);
  border-color: var(--danger);
}

/* Toasts. Bottom-right, and the position was measured rather than reasoned:
   top-right under the header looked like the free quadrant, but it sits
   directly on the turn-order panel — the one thing a player checks to see
   whose turn it is. That panel's height isn't fixed either (147px at two
   players, 228px at four), so no constant offset clears it.

   The game layout is content-sized, not viewport-height: its bottom edge
   measures 564–611px at 1024/1440/1920. So the bottom of a normal desktop
   window is genuinely empty, and this corner collides with nothing. On a
   window shorter than ~700px it will overlay the chat panel, which is the
   cheapest thing here to cover for five seconds — and chat toasts are already
   suppressed when the log is being watched.

   column-reverse so the newest toast is the one nearest the corner and older
   ones ride up out of the way.

   z-index 110 sits above the node modal's 100. A notification a modal can
   swallow isn't a notification, and someone who left the modal open is exactly
   who needs telling it's their turn. Still below the narrow-viewport notice
   (200), which is meant to be the only thing on screen while it's up.

   The shadow is written out because this predates the shadow scale. */
/* Placement is per-screen, because no single corner is free on all of them —
   found in the 1024/1440 pass, measured at both.

   Default is bottom-centre, which on the game screen sits over the play
   column. That column's height is capped by the move form (lowest control
   y=553 at 1024, y=506 at 1440), so it cannot grow into the stack. Bottom
   right was the original choice and is wrong there: the side column's chat
   composer drops to y=755 once the log fills, putting a toast on the one
   control the player may be reaching for. Bottom left is no better — the chain
   column is viewport-bounded too, and a long chain fills it with focusable
   nodes.

   The recap screen inverts it: its buttons are centred and run to y=744, while
   the right side is empty. Hence the override below.

   Covering non-interactive text for five seconds is fine. Covering a control
   never is. */
.toast-stack {
  position: fixed;
  bottom: 1rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 110;
  display: flex;
  flex-direction: column-reverse;
  gap: 0.5rem;
  width: min(21rem, calc(100vw - 2rem));
  /* The container spans a whole column of the viewport; only the toasts
     themselves should take clicks. */
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.7rem 0.5rem 0.7rem 0.85rem;
  border: 1px solid var(--border);
  /* The variant rides on the left edge, so one shape carries every kind. */
  border-left: 3px solid var(--muted);
  border-radius: 10px;
  background: var(--surface-2);
  box-shadow: var(--elevation-3);
  font-size: 0.85rem;
  line-height: 1.45;
}

.toast-text {
  flex: 1;
  margin: 0;
}

.toast-turn {
  border-left-color: var(--accent-2);
}

.toast-good {
  border-left-color: var(--success);
}

.toast-warn {
  border-left-color: var(--danger);
}

/* A standing instruction, not a notification. It does not expire, so it has to
   earn the room it keeps: the accent runs right round the card instead of down
   one edge, the text steps up to the body size at full weight, and a soft glow
   lifts it off the board behind. The × is the way out and stays exactly where
   every other toast keeps it — the one thing about this card that should not
   be special. */
.toast-prompt {
  border-color: rgb(var(--accent-bright-rgb) / 0.5);
  border-left-color: var(--accent-bright);
  background: linear-gradient(
    to right,
    rgb(var(--accent-bright-rgb) / 0.14),
    var(--surface-2) 60%
  );
  box-shadow:
    var(--elevation-3),
    0 0 0 1px rgb(var(--accent-bright-rgb) / 0.18),
    0 0 22px rgb(var(--accent-bright-rgb) / 0.22);
}

.toast-prompt .toast-text {
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--text);
}

/* Not the shared button style: that carries layout margins and has no hover or
   focus state at all. This one gets its own rather than being born with that
   gap. */
.toast-action {
  margin: 0.55rem 0 0;
  padding: 0.35rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
}

.toast-action:hover {
  border-color: var(--accent-bright);
  color: var(--accent-bright);
}

.toast-action:active {
  transform: translateY(1px);
}

.toast-close {
  flex: none;
  width: 22px;
  height: 22px;
  padding: 0;
  margin: -0.1rem 0 0;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--muted);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}

.toast-close:hover,
.toast-close:focus-visible {
  color: var(--text);
  background: var(--surface);
}

@media (prefers-reduced-motion: no-preference) {
  .toast {
    animation: toast-in 220ms ease-out;
  }

  /* Holding the last frame is only safe because the element is removed the
     moment the animation ends. */
  .toast.is-leaving {
    animation: toast-out 180ms ease-in forwards;
  }

  /* Up from the bottom edge, not in from the right: the stack is centred now,
     so there is no right edge for a toast to have come from. */
  @keyframes toast-in {
    from {
      opacity: 0;
      transform: translateY(12px);
    }
  }

  @keyframes toast-out {
    to {
      opacity: 0;
      transform: translateY(12px);
    }
  }
}

/* Site footer: TMDB attribution (required by their API terms, D32) and where
   privacy and terms live. Stays at the standard 700px column even on the
   screens that break out wider — a full-width rule under the recap would carry
   more weight than fine print should.

   **It shows on every screen, the board included.** There was once a rule
   hiding it during play, on the grounds that it was clutter in the middle of a
   turn. That rule named screens this app does not have — it was written for
   the multiplayer modes and never matched anything after the carve-out — and
   the layout it was worried about is gone too: measured on the board with a
   full chain, the footer starts 650px below the fold on a 390x844 phone and
   827px below it on a 375x667. It is at the end of the page, not in the middle
   of a turn.

   Kept everywhere rather than hidden on the board, deliberately: privacy and
   terms belong on the screen players spend their time on, and the attribution
   being present throughout is the safest reading of TMDB's terms. D44 settled
   that those terms ask for it on the page, not above the fold. */
.site-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.85rem;
  max-width: 700px;
  margin: 3rem auto 0;
  padding-top: 1.25rem;
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: 0.8rem;
  line-height: 1.5;
}

.footer-attribution {
  display: flex;
  align-items: center;
  gap: 0.85rem;
}

.site-footer p {
  margin: 0;
}

/* Empty today (see the HTML comment above the <ul>) — :empty collapses it so
   justify-content: space-between doesn't leave a phantom gap on the right. */
.footer-links {
  display: flex;
  gap: 1rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.footer-links:empty {
  display: none;
}

/* The only unstyled links in the app, which meant the UA's default blue and —
   once anyone had opened them — its default *purple* sitting under the recap.
   Muted like the notice beside them; they are fine print, not calls to act. */
.footer-links a {
  color: var(--muted);
  text-decoration-color: rgb(139 148 168 / 0.4);
  text-underline-offset: 2px;
}

.footer-links a:hover,
.footer-links a:focus-visible {
  color: var(--text);
  text-decoration-color: currentColor;
}

/* TMDB's mark, shown as supplied — their trademark, so it isn't recoloured or
   filtered to match the palette. width/height on the <img> keep it from
   reflowing the footer while it loads. */
.footer-tmdb {
  flex-shrink: 0;
  display: block;
}

.footer-tmdb img {
  display: block;
  height: 28px;
  width: auto;
}

/* Accessible-but-invisible: keeps the landing <h1> in the document outline and
   for screen readers while the hero logo carries it visually. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

h1 {
  margin-bottom: 0.25rem;
}

.muted {
  color: var(--muted);
}

.hidden {
  display: none !important;
}

/* Screen entrance animation. One constraint, load-bearing: translate and
   opacity only, never scale. drawChainConnector measures positions *relative*
   to a container, so a uniform translate cancels out exactly; a scale would not,
   and connector lines drawn during the animation would land permanently
   misplaced.

   No screen carries a permanent transform any more, so these keyframes apply
   to all five. (The game and recap screens used to need a duplicate set that
   preserved their translateX(-50%) breakout.) */
@media (prefers-reduced-motion: no-preference) {
  .screen-enter {
    animation: screen-enter 320ms cubic-bezier(0.16, 1, 0.3, 1) both;
  }
}

@keyframes screen-enter {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

input[type='text'] {
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 0.6rem 0.75rem;
  border-radius: 6px;
  font-size: 1rem;
}

/* Border tint for pointer focus; the shared ring above still applies on
   :focus-visible, so a keyboard user gets the same 2px ring as every other
   control instead of a 1px colour change nothing else uses. */
input[type='text']:focus {
  border-color: var(--accent);
}

.btn {
  padding: 0.6rem 1.2rem;
  font-size: 1rem;
  font-weight: 600;
  border-radius: 6px;
  border: none;
  cursor: pointer;
  margin: 0.25rem 0.5rem 0.25rem 0;
}

.btn-primary {
  background: var(--accent);
  color: white;
  transition:
    box-shadow 0.15s,
    background 0.15s;
}

.btn-primary:hover:not(:disabled) {
  background: var(--accent-bright);
  /* Was a Tailwind blue (#60a5fa) — a different hue from the fill it glows
     around. Fixed as part of the design-tokens tranche. */
  box-shadow: 0 0 12px rgb(var(--accent-bright-rgb) / 0.5);
}

.btn-primary:active:not(:disabled) {
  background: var(--accent);
  box-shadow: none;
}

.btn-primary:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* A button held disabled while it waits on a fetch is busy, not unavailable.
   The dimming says the right thing either way; the cursor doesn't. Declared
   after :disabled deliberately — same specificity, so order is what decides. */
.btn[aria-busy='true'] {
  cursor: progress;
}

.btn-secondary {
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
  transition:
    border-color 0.15s,
    background 0.15s;
}

/* Matched to .btn-primary's shape above rather than newly invented: the border
   is this button's only strong edge, so it carries the hover the way the fill
   does on the primary. */
.btn-secondary:hover:not(:disabled) {
  background: var(--border);
  border-color: var(--accent-bright);
}

.btn-secondary:active:not(:disabled) {
  background: var(--surface-2);
}

.btn-secondary:disabled,
.btn-secondary[aria-disabled='true'] {
  opacity: 0.4;
  cursor: not-allowed;
}

/* A third tier below secondary, for a control that has to be present but must
   not compete — the recap's "Done" sitting under three real choices. Four
   bordered buttons in a column read as four equal offers, which is exactly
   what that stack is not. */
/* The quietest of the three button weights that still reads as a button:
   no fill, but a visible edge. A borderless version worked while it sat alone
   under a stack and stopped working the moment there were three side by
   side — with no edge, they read as text rather than as choices. */
.btn-outline {
  background: none;
  color: var(--muted);
  border: 1px solid var(--border);
  transition:
    color 0.15s,
    border-color 0.15s;
}

@media (hover: hover) {
  .btn-outline:hover:not(:disabled) {
    color: var(--text);
    border-color: var(--accent-2);
  }
}

/* THE LANDING. One centred column, sized for a phone and simply allowed to
   breathe on a wider screen — there is no second column to place, so nothing
   has to move.

   The logo's own treatment below is carried over unchanged on purpose: it is
   the strongest brand moment the app has. What changed is only where the
   column sits and what sits under it. */
/* The landing owns the whole viewport rather than a guessed slice of it: the
   body becomes a full-height column for this screen only, the screen flexes
   into whatever is left between header and footer, and the content centres in
   that. Sized by the layout rather than by a min-height constant, which on a
   short phone overflowed into a scroll and on a tall one left a void under the
   day line. svh, not vh, so a mobile browser's retracting toolbar doesn't
   change the answer mid-scroll. */
body[data-screen='landing'],
body[data-screen='shareGone'] {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  padding-bottom: var(--space-4);
}

body[data-screen='landing'] #screen-landing,
body[data-screen='shareGone'] #screen-share-gone {
  flex: 1;
  display: flex;
}

/* The header wordmark stands down on the landing, and only there. The hero
   under it is the same file at six times the size, a couple of hundred pixels
   away, so the bar was repeating the one thing the screen was already saying
   loudest — and repeating it for screen readers too, which heard the name from
   both images and again from the visually-hidden h1. Everywhere else the
   header logo is what identifies the app while the screen shows something
   else, so it stays. .header-actions keeps its margin-left: auto, so the
   account button holds the right edge on its own. */
body[data-screen='landing'] .site-brand,
body[data-screen='shareGone'] .site-brand {
  display: none;
}

.landing {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: var(--space-6);
  padding-block: var(--space-6);
}

/* Deliberately not pushed to the foot of the column with an auto margin: that
   reads as a hole between the button and the day rather than as space around
   a group. The four parts centre together and the slack sits outside them. */
.landing-day {
  margin-top: var(--space-2);
}

.landing-mark {
  position: relative;
  display: flex;
  justify-content: center;
}

/* The same two-pool blue/gold lighting the original landing used, with the
   pools recentred for a centred mark — the blue still sits above and left of
   the gold, which is the relationship that makes it read as light rather than
   as a gradient. Held at what used to be the peak of its pulse: the sweep is
   the motion now, and the two moving together was too much. */
.landing-mark::before {
  content: '';
  position: absolute;
  inset: -45% -18%;
  z-index: -1;
  background:
    radial-gradient(
      50% 65% at 38% 32%,
      rgb(var(--accent-bright-rgb) / 0.42),
      transparent 70%
    ),
    radial-gradient(
      50% 65% at 62% 72%,
      rgb(var(--accent-2-rgb) / 0.3),
      transparent 70%
    );
  filter: blur(28px);
  transform: scale(1.08);
}

/* Height-driven like the original, with a width guard added: this is a
   wordmark, so on a narrow phone it is width that runs out first. */
.hero-logo {
  display: block;
  height: clamp(76px, 19vw, 128px);
  width: auto;
  max-width: 86vw;
  filter: drop-shadow(0 0 20px rgb(var(--accent-bright-rgb) / 0.4))
    drop-shadow(0 0 32px rgb(var(--accent-2-rgb) / 0.22));
}

/* A one-time overshoot on arrival. The breathe that used to follow it is now
   the sweep below. */
@media (prefers-reduced-motion: no-preference) {
  .hero-logo {
    animation: hero-logo-arrive 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
  }
}

@keyframes hero-logo-arrive {
  from {
    opacity: 0;
    transform: scale(0.88);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

.hero-mark {
  position: relative;
  display: inline-block;
}

/* Light that paints only where the logo's ink is: the mask is the logo. */
.hero-sweep {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  -webkit-mask: url(/logo.svg) center / contain no-repeat;
  mask: url(/logo.svg) center / contain no-repeat;
}

/* The wedge, hidden until the animation shows it — so reduced motion draws
   nothing rather than a bright patch that never moves. */
.hero-sweep::before {
  content: '';
  position: absolute;
  left: -80%;
  top: -320%;
  width: 260%;
  height: 740%;
  opacity: 0;
  background: conic-gradient(
    from 0turn,
    transparent 0turn 0.02turn,
    rgb(255 255 255 / 0.92) 0.07turn,
    rgb(var(--accent-bright-rgb) / 0.55) 0.11turn,
    transparent 0.18turn,
    transparent 1turn
  );
}

/* Once per visit to the landing, starting as the arrival above settles. It
   does not repeat: a loop on the first screen became something to tune out.
   The screen is display:none while away, so coming back plays it again. */
@media (prefers-reduced-motion: no-preference) {
  .hero-sweep::before {
    animation: hero-sweep 2.5s ease-in-out 700ms both;
  }
}

@keyframes hero-sweep {
  0% {
    transform: rotate(0turn);
    opacity: 0;
  }
  8% {
    opacity: 1;
  }
  88% {
    opacity: 1;
  }
  100% {
    transform: rotate(1turn);
    opacity: 0;
  }
}

/* One sentence, and it has to do the whole job of saying what this is —
   measure capped so it breaks into two balanced lines on a phone rather than
   one long one and an orphan. */
.landing-tagline {
  margin: 0;
  max-width: 22rem;
  font-size: var(--text-xl);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.25;
  text-wrap: balance;
}

@media (min-width: 640px) {
  .landing-tagline {
    max-width: 30rem;
    font-size: var(--text-2xl);
  }
}

/* Sits close under its heading, as one message. */
.share-gone-note {
  margin: calc(-1 * var(--space-4)) 0 0;
  max-width: 20rem;
  color: var(--muted);
  font-size: var(--text-md);
  line-height: 1.4;
  text-wrap: balance;
}

/* Full-width targets on a phone, capped so they don't stretch into a banner
   on a desktop. Stacked rather than side by side: Play is the only thing
   most visits want, and a row of two equal pills would say otherwise. */
.landing-actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-3);
  width: min(100%, 20rem);
}

.landing-actions .btn {
  width: 100%;
  border-radius: var(--radius-pill);
  padding-block: 0.85rem;
  font-size: var(--text-md);
}

#btn-play-daily {
  font-size: var(--text-lg);
  font-weight: 700;
}

/* Exactly one of the three is visible at a time; each is a single control, so
   the wrapper only exists to be toggled. */
.landing-gate {
  display: contents;
}

.landing-gate.hidden {
  display: none;
}

/* Read-only — the account's handle is the name, and the streak beside it is
   the reason the account is worth having on a daily puzzle. */
/* A button — it opens their numbers — but deliberately still shaped like the
   line of text it looks like: anything bordered here would read as a third
   action competing with Play, and a streak beside your own name is a greeting
   rather than a call to act. The padding is the tap target. */
.landing-identity {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
  min-height: 44px;
  padding: var(--space-1) var(--space-3);
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  color: inherit;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}

.landing-identity:hover {
  border-color: var(--border);
}

.landing-identity .avatar {
  width: 30px;
  height: 30px;
  background: var(--border);
  color: var(--text);
  font-size: 0.9rem;
}

.landing-streak {
  color: var(--muted);
  font-weight: 400;
}

/* The day's identity, and the page's quietest line by design: it is the thing
   a returning player checks, not the thing a new one reads. */
.landing-day {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.7rem;
  padding-top: var(--space-5);
  width: min(100%, 20rem);
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: var(--text-xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* The cadence line and the date are one group under one rule. .landing-day
   draws that rule itself, and the shared page still relies on it doing so,
   so it is handed to the wrapper here rather than taken off the class. */
.landing-today {
  width: min(100%, 20rem);
  margin-top: var(--space-2);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border);
}

.landing-today .landing-day {
  width: auto;
  margin-top: var(--space-2);
  padding-top: 0;
  border-top: 0;
}

.landing-cadence {
  margin: 0;
  color: var(--muted);
  font-size: var(--text-sm);
}

.landing-day-dot {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.6;
}

/* The number takes the gold half of the brand pair — the mark's two strokes
   are blue and gold, and giving the day's index the gold makes it read as a
   stamp on the page rather than more muted text. Tabular so it doesn't
   shift width as it climbs. */
.landing-number {
  color: var(--accent-2);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.error-text {
  color: var(--danger);
  font-weight: 600;
  min-height: 1.2em;
  margin: 0.5rem 0 0;
}

/* Only shown once there's text to sit in front of, so the reserved empty
   state above stays truly empty rather than showing a lone icon. */
.error-text:not(:empty)::before {
  content: '⚠';
  margin-right: 0.35rem;
}

@media (prefers-reduced-motion: no-preference) {
  .error-text.is-shaking {
    animation: error-shake 400ms ease-in-out;
  }
}

/* LOGIN screen (index.html's #screen-login) — a dedicated centred screen
   replacing the old header popover for sign-in. No bordered card: it sits
   straight on the same near-black background as every other screen, the
   same minimal chrome the rest of the app already uses. */
/* Same idiom as the landing: the body owns the viewport height and the screen
   takes the slack, so the card centres against what is actually on screen.
   100svh, not 100vh — on a phone 100vh is measured with the URL bar retracted,
   which is taller than the visible area and pushed the legal line under the
   fold on first paint. */
body[data-screen='login'] {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  padding-bottom: var(--space-4);
}

body[data-screen='login'] #screen-login {
  flex: 1;
}

.screen-login {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
}

.login-back {
  position: absolute;
  top: 0;
  left: 0;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  /* Padded out to a 44px thumb target rather than sized by its own text. The
     negative inline margin cancels the padding it would otherwise be indented
     by, so the label still starts level with the card edge. */
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  margin-left: calc(var(--space-3) * -1);
  background: none;
  border: none;
  color: var(--muted);
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}

.login-back svg {
  width: 16px;
  height: 16px;
}

.login-back:hover {
  color: var(--text);
}

/* One column width for the whole card, so the provider buttons, the divider
   and the email field all share an edge. 22rem is close to full-bleed inside
   a 390px phone's body padding and still a tidy narrow column on a desktop —
   the old 420px cap only ever mattered on the desktop side. */
.login-card {
  width: min(100%, 22rem);
}

.login-mark {
  display: block;
  margin: 0 auto var(--space-5);
  border-radius: var(--radius-lg);
  background: var(--surface-2);
  border: 1px solid var(--border);
  box-shadow: var(--elevation-2);
}

.login-title {
  font-size: var(--text-2xl);
  margin: 0 0 var(--space-2);
  text-wrap: balance;
}

/* Balanced for the same reason as .landing-tagline: at 352px this wraps, and
   the natural break left "first." alone on the second line. */
.login-subtitle {
  color: var(--muted);
  font-size: var(--text-sm);
  margin: 0 0 var(--space-5);
  text-wrap: balance;
}

/* Stacked, not side by side. Two long labels sharing the width of a phone both
   spilled out of their own buttons — and a column makes a third way in a
   one-line addition rather than a re-layout. */
.login-providers {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}

/* Deliberately the landing's primary-action shape — same pill, same block
   padding — so signing in doesn't look like it belongs to a different app
   than the button that got you here. Also clears 44px. */
.login-providers .btn-provider {
  border-radius: var(--radius-pill);
  padding-block: 0.85rem;
  font-size: var(--text-md);
}

/* Same idiom as Resend's "or" rule: two half-width lines either side of the
   word, sized off the card's own width rather than a hand-typed pixel gap. */
.login-divider {
  position: relative;
  margin: var(--space-5) 0;
  color: var(--muted);
  font-size: var(--text-xs);
}

.login-divider::before,
.login-divider::after {
  content: '';
  position: absolute;
  top: 50%;
  width: calc(50% - 1.6rem);
  border-top: 1px solid var(--border);
}

.login-divider::before {
  left: 0;
}

.login-divider::after {
  right: 0;
}

.login-form {
  text-align: left;
}

.login-label {
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--text-xs);
  color: var(--muted);
  font-weight: 600;
}

/* font-size stays at a literal 1rem, not --text-sm: iOS zooms the page in on
   focus for anything under 16px, and it does not zoom back out. */
.login-form input[type='email'] {
  width: 100%;
  box-sizing: border-box;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 0.75rem;
  border-radius: var(--radius-sm);
  font-size: 1rem;
}

.login-form input[type='email']:focus {
  border-color: var(--accent);
}

/* Every full-width control on this screen shares one shape. The retry button
   is scoped to .screen-login because .account-panel-btn is also worn by the
   header popover's buttons, which are not thumb targets on a phone. */
.login-submit,
.login-claim .account-claim-submit,
.screen-login .account-panel-btn {
  width: 100%;
  border-radius: var(--radius-pill);
  padding-block: 0.85rem;
  font-size: var(--text-md);
}

.login-submit {
  margin: var(--space-3) 0 0;
}

.login-claim {
  text-align: left;
}

.login-claim .login-title,
.login-claim .login-subtitle {
  text-align: center;
}

/* Matches the email field's target height rather than the app-wide default,
   so the two steps of the same sign-in don't change size under the thumb. */
.login-claim input[type='text'] {
  width: 100%;
  box-sizing: border-box;
  padding: 0.75rem;
  margin-bottom: var(--space-2);
}

.login-claim .account-claim-submit {
  margin: 0;
}

.login-claim #btn-claim-cancel {
  margin-top: var(--space-3);
}

.login-legal {
  margin: var(--space-6) 0 0;
  color: var(--muted);
  font-size: var(--text-xs);
  line-height: 1.6;
}

.login-legal a {
  color: var(--accent-bright);
}

@keyframes error-shake {
  10%,
  90% {
    transform: translateX(-1px);
  }
  20%,
  80% {
    transform: translateX(2px);
  }
  30%,
  50%,
  70% {
    transform: translateX(-4px);
  }
  40%,
  60% {
    transform: translateX(4px);
  }
}

.countdown {
  /* Block, but hugging its text so the pulse below scales the number rather
     than a full-width box (which would read as the digits sliding right). */
  width: max-content;
  font-size: var(--text-2xl);
  font-weight: bold;
  font-family: var(--font-mono);
  margin-bottom: 1rem;
  transform-origin: left center;
}

/* Base node: a centred icon with a caption. The recap's horizontal rail uses it
   as-is; the live vertical rail reshapes it into a row further down. */
.chain-node {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  width: 64px;
  text-align: center;
}

.chain-node .node-thumb {
  width: 56px;
  height: 56px;
  object-fit: cover;
  background: var(--surface-2);
}

/* 2:3, the shape posters are actually drawn at — a square crop slices the top
   and bottom off every one. The same ratio as .modal-image-movie, so a poster
   is the same shape wherever it appears. */
.chain-node-movie .node-thumb {
  width: 44px;
  height: 66px;
  border-radius: 6px;
  border: 2px solid var(--accent);
}

.chain-node-actor .node-thumb {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: 2px solid var(--border);
}

.node-label {
  font-size: 0.7rem;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 64px;
}

/* Non-scrolling positioned wrapper: it's the flex child that fills the bounded
   column, and it anchors the hover-scroll arrows (which must stay put while the
   rail scrolls underneath them). */
.chain-scroll-wrap {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* The chain is a filmstrip, so give it perforations. Holes are punched in
   --bg (the darkest surface) out of a --surface band, so they read as holes
   rather than as light dots — the inverse is what makes a dashed line.

   Anchored to the wrapper, not the rail, so the band runs the column's full
   height like the gate a film runs through, instead of stopping wherever the
   chain happens to end. This is the app's only cinematic motif outside the
   logo, and it costs one gradient and 18px of the column. Kept low-contrast
   deliberately: texture noticed on second look, not a costume. */
.chain-scroll-wrap::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 11px;
  border-radius: 3px;
  opacity: 0.65;
  pointer-events: none;
  background:
    repeating-linear-gradient(to bottom, transparent 0 7px, var(--bg) 7px 17px),
    var(--surface);
}

/* No band on the recap's horizontal rail: the perforations would run across
   the strip instead of along it. */
.chain-scroll-wrap-horizontal::before {
  content: none;
}

/* Scrolls the zigzag rail internally instead of stretching the page. flex: 1
   with min-height: 0 lets it fill (and shrink within) the wrapper so overflow
   actually kicks in. Wraps #chain (which keeps its full natural height) rather
   than scrolling .chain-rail itself, so drawChainConnector's SVG measurements
   stay correct. overflow-x is hidden to avoid a phantom horizontal scrollbar
   (the connector SVG is overflow: visible, so its lines are unaffected).
   scroll-snap settles scrolling on a whole node so nodes are never left
   half-cut at the top edge. */
.chain-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  scroll-snap-type: y proximity;
  scroll-padding-top: 0.5rem;
  /* `contain` only stops the bounce from chaining to the page — the rail
     would still rubber-band against its own edge, which is the actual
     flicker (repeated bounce-back on every scroll tick once you're at the
     boundary). `none` suppresses that local bounce too, so the rail just
     stops. */
  overscroll-behavior-y: none;
}

/* The Chain Complete recap's horizontal counterpart to .chain-scroll-wrap /
   .chain-scroll above — same "wrap anchors the arrows, inner scrolls" split,
   just rotated onto the x-axis. */
.chain-scroll-wrap-horizontal {
  flex-direction: row;
  width: 100%;
  max-width: 100%;
}

.chain-scroll-horizontal {
  width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x proximity;
  scroll-padding-top: 0;
  scroll-padding-left: 0.5rem;
  /* Same fix as .chain-scroll's overscroll-behavior-y, rotated onto x — see
     that comment. `none`, not `contain`: the bounce being fixed is the
     rail's own edge rubber-banding, not chaining to the page behind it. */
  overscroll-behavior-x: none;
  /* overflow-y is hidden here (this rail never scrolls vertically), so any
     Y component in a real trackpad gesture — natural drift while swiping,
     not perfectly axis-locked — would otherwise pass straight through to
     the page instead of being contained by the X-only rule above. The recap
     screen is the one screen tall enough for that leak to show as a bounce. */
  overscroll-behavior-y: none;
}

.chain-rail {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  padding: 0.25rem 0;
}

/* padding-left reserves the lane the sprocket band runs in (see
   .chain-scroll-wrap::before) — the thumbs shift right uniformly, which
   drawChainConnector handles for free, since it measures rendered rects
   rather than assuming positions.

   padding-right exists for a subtler reason: chain-node-actor's margin-left:
   auto pins actor nodes flush against the rail's own right edge, with no
   slack. .chain-scroll's overflow-x: hidden then clips the :hover scale
   (scale(1.06), a few px of bleed) clean off on that side — the actor node
   visibly loses its right edge rather than growing into it, reading as
   "covered" by nothing at all. Movie nodes never showed this: their own
   :hover bleed lands inside the sprocket lane's 18px instead of at a bare
   edge. 10px comfortably covers the largest bleed on the largest node this
   rail renders (Daily's own, at 1.06 of ~130px). */
.chain-rail:not(.chain-rail-horizontal) {
  padding-left: 18px;
  padding-right: 10px;
}

.chain-rail-svg {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 0;
  pointer-events: none;
  overflow: visible;
}

/* Dashed while the edge runs into the ghost slot — the line itself says
   "not linked yet", so the state is carried by the connector rather than by a
   row of empty thumbnails. */
.chain-connector {
  fill: none;
  stroke: var(--border);
  stroke-width: 2;
  stroke-dasharray: 4 5;
}

/* Lit up, and solid, once both endpoints of the edge are real nodes. */
.chain-connector-active {
  stroke: var(--accent-bright);
  stroke-width: 3;
  stroke-dasharray: none;
  filter: drop-shadow(0 0 4px var(--accent-bright));
}

/* A new link landing — the payoff of a whole turn, which until now just
   appeared, lit, between one repaint and the next. The glow now sweeps the
   edge from the node the chain came from to the one that just filled, which
   is what makes a link read as *connecting* rather than as switching on.
   Pure paint: stroke-dashoffset moves nothing the connector measured, so a
   sweep can never shift the rail underneath it.

   drawChainConnector decides which edges get the class — only ones that were
   unlit on the previous draw — and rebuilds the SVG from scratch every time,
   so there is nothing to strip on animationend: the animated element is gone
   by the next repaint. */
@media (prefers-reduced-motion: no-preference) {
  /* The other half of a link landing: the node itself. Added by
     submitDailyGuess to the node it just filled, and shorter than the sweep
     below on purpose — the answer settles first, the connector then travels
     into it. Nothing strips the class, and nothing needs to: the next rebuild
     of the rail takes the element carrying it away.

     It brightens from .chain-node-pending's own resting opacity rather than
     fading in from zero, because the node is not *appearing*: its poster and
     title have been on screen since the tap. Starting at zero made an accepted
     guess blink out and climb back — the very thing this was meant to smooth.

     Opacity only, no transform: `both` holds the final keyframe, and a held
     transform outranks .chain-rail .chain-node:hover's scale, which would
     leave every filled node unable to respond to the pointer again (the same
     trap node-retract documents). */
  .chain-node-settling {
    /* ease-out, not the expo-out curve this used while it was a fade-in from
       zero: over the much smaller distance a brighten covers, expo lands
       inside 200ms and reads as a snap. Same easing as node-retract, which is
       the same gesture in the other direction. */
    animation: node-settle 380ms ease-out both;
  }

  /* Overrides .chain-connector-active's stroke-dasharray: none, and so has to
     stay after it in the file — the two rules have equal specificity. Left out
     of the reduced-motion case deliberately: with no dasharray the edge is
     simply lit, immediately, which is the right still frame of this. */
  .chain-connector-arriving {
    stroke-dasharray: 1;
    animation: connector-travel 460ms linear both;
  }
}

@keyframes node-settle {
  from {
    /* settleDailyPendingNode sets this to the opacity the pending breathe had
       actually reached; the fallback is .chain-node-pending's resting value. */
    opacity: var(--settle-from, 0.62);
  }
  to {
    opacity: 1;
  }
}

@keyframes connector-travel {
  from {
    stroke-dashoffset: 1;
  }
  to {
    stroke-dashoffset: 0;
  }
}

.chain-rail .chain-node {
  position: relative;
  z-index: 1;
  background: none;
  border: none;
  padding: 0.35rem;
  border-radius: 10px;
  cursor: pointer;
  transition: transform 0.15s;
  scroll-snap-align: start;
}

.chain-rail .chain-node-movie {
  margin-right: auto;
}

.chain-rail .chain-node-actor {
  margin-left: auto;
}

/* A row rather than a stack. The picture still hugs its own edge and the text
   flows inward from it, so the zigzag survives and the lines between nodes need
   no changes at all — they are measured from the pictures, not from the nodes.

   Kept away from the sideways version, where the recap lays the same nodes out
   across the screen and a stack is still the right shape. */
.chain-rail:not(.chain-rail-horizontal) .chain-node {
  flex-direction: row;
  align-items: center;
  gap: 0.6rem;
  width: auto;
  max-width: 100%;
  text-align: left;
}

.chain-rail:not(.chain-rail-horizontal) .chain-node-actor {
  flex-direction: row-reverse;
  text-align: right;
}

/* Two lines at ~150px instead of one line at 64px, which is what rendered every
   title of normal length as "Mission: I…". */
.chain-rail:not(.chain-rail-horizontal) .node-label {
  max-width: 150px;
  white-space: normal;
  line-height: 1.3;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* The Chain Complete recap's rail: same interactive nodes and SVG connector
   as the live vertical rail, just rotated onto the x-axis — movie nodes hug
   the top edge, actor nodes hug the bottom, so the zigzag runs left-to-right
   instead of top-to-bottom. min-height gives the top/bottom auto-margins
   room to actually separate the two rows (mirrors how the vertical rail's
   fixed-width column gives its left/right auto-margins room to work). */
/* Centered so a short chain sits in the middle of the stage instead of
   hugging the left edge — .chain-rail-horizontal has no width of its own, so
   it fills .chain-scroll-horizontal and its flex items otherwise pack to
   flex-start. `safe center` (falls back to plain `center` in browsers that
   don't parse it) matters once the chain is long enough to scroll: plain
   center on an overflowing flex container centers the overflow itself,
   which clips the first node at scrollLeft 0 rather than landing on it. */
/* Tightened for a phone: the old gap and height were sized for a chain laid
   across a desktop, and on a phone they bought whitespace at the cost of how
   many nodes you could see at once. The zigzag stays — it is the shape this
   game is recognised by. */
.chain-rail-horizontal {
  flex-direction: row;
  align-items: center;
  justify-content: center;
  justify-content: safe center;
  min-height: 168px;
  gap: var(--space-3);
  padding: 0.25rem var(--space-4);
}

@media (min-width: 640px) {
  .chain-rail-horizontal {
    min-height: 190px;
    gap: 1.5rem;
  }
}

.chain-rail-horizontal .chain-node {
  flex-shrink: 0;
}

.chain-rail-horizontal .chain-node-movie {
  margin-right: 0;
  margin-bottom: auto;
}

.chain-rail-horizontal .chain-node-actor {
  margin-left: 0;
  margin-top: auto;
}

.chain-rail .chain-node:not(.chain-node-placeholder):hover {
  transform: scale(1.06);
}

.chain-rail .chain-node.selected {
  background: var(--surface-2);
  box-shadow: 0 0 0 2px var(--accent-2);
}

/* The single ghost slot at the end of the rail: where the next link will land.
   Dimmed rather than absent, so the alternation stays legible on an empty
   chain. */
.chain-rail .chain-node-placeholder {
  cursor: default;
  opacity: 0.55;
}

.chain-node-placeholder .node-label {
  color: var(--muted);
  font-style: italic;
}

.chain-node-placeholder .node-thumb-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  background: var(--surface-2);
  border: 1px dashed var(--border);
}

.node-thumb-placeholder svg {
  width: 55%;
  height: 55%;
}

/* Quiet on purpose: this is the one control nobody should be drawn to, so it
   stays at its own height rather than growing to fill the row.

   Pressing it once arms it, and the armed state borrows the danger colour
   rather than restyling the button — unmistakable when it happens, and
   invisible until then. */
.daily-finish.is-confirming,
.daily-finish.is-confirming:hover {
  color: var(--danger);
  border-color: var(--danger);
}

/* Held rather than truly disabled, so it can be tapped and answer. It still has
   to *look* unavailable — .btn-secondary's dimming is what says so to everyone
   not using a screen reader — but it keeps its pointer and lights up for
   nothing, because unlike a dead button there is something to be had by
   pressing it. */
.daily-finish[aria-disabled='true'] {
  cursor: pointer;
}

.daily-finish[aria-disabled='true']:hover {
  background: var(--surface-2);
  border-color: var(--border);
}

/* The small upper-case label above a boxed panel — "Fill this blank" and its
   like. It labels a panel that already has its own surface and border, so it
   is deliberately lighter than a heading would be. */
.panel-label {
  display: block;
  font-size: var(--text-2xs);
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-bottom: 0.6rem;
}

/* Every avatar in the app is one of these, sized and coloured where it is used.
   An initial in a circle and nothing else — there was once a rule here dressing
   a profile photo that no account ever had. The clipping stays: it is what
   keeps a long initial inside the circle. */
.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  overflow: hidden;
  flex-shrink: 0;
}

/* What a tapped node expands into: a plain, quiet card, deliberately lighter
   than the board it came from. It sits above the rail in the DOM so it opens
   in place when a node is selected. */
.node-detail {
  margin-bottom: 1.25rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--elevation-1);
  padding: 0.85rem;
}

.node-detail-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.5rem;
}

.node-detail-kind {
  display: inline-block;
  font-size: 0.65rem;
  font-weight: 700;
  color: var(--muted);
  border: 1px solid var(--border);
  padding: 0.1rem 0.4rem;
  border-radius: 4px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.node-detail-expand {
  background: none;
  border: 1px solid var(--border);
  color: var(--muted);
  border-radius: 4px;
  width: 22px;
  height: 22px;
  font-size: 0.8rem;
  line-height: 1;
  padding: 0;
  cursor: pointer;
}

.node-detail-expand:hover {
  color: var(--text);
  border-color: var(--accent-bright);
}

.node-detail-body {
  display: flex;
  gap: 0.65rem;
}

.node-detail-image {
  object-fit: cover;
  background: var(--surface-2);
  border: 1px solid var(--border);
  flex-shrink: 0;
}

.node-detail-image-movie {
  width: 46px;
  height: 68px;
  border-radius: 5px;
}

.node-detail-image-actor {
  width: 52px;
  height: 52px;
  border-radius: 50%;
}

.node-detail h4 {
  margin: 0 0 0.3rem;
  font-size: 0.85rem;
}

/* Every inline summary caps its description and scrolls the rest — and the
   cap has to *look* like a cap. At a plain 6rem the box ended 0.71 of a line
   into the sixth, so a long overview was sliced through the middle of its
   letters with nothing to say it had been: no fade, and no scrollbar to see
   on a phone. Roughly seven in ten real TMDB descriptions are long enough to
   hit it.

   The fade is what says "there is more", and the padding is what the fade
   falls on when the text is short enough not to need one — without it the
   gradient lands on the last line that fits and reads as a clipped render.
   The full text is a tap away on the ⤢ button either way, so the cap moves
   the reading rather than ending it. */
.node-detail-text {
  margin: 0;
  font-size: 0.75rem;
  color: var(--muted);
  line-height: 1.4;
  max-height: 7rem;
  padding-bottom: 1.75rem;
  overflow-y: auto;
  mask-image: linear-gradient(
    to bottom,
    #000 calc(100% - 1.75rem),
    transparent
  );
}

.field-frame {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 1rem;
  flex: 1;
  min-width: 160px;
}

.autocomplete-field {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.autocomplete-field label {
  font-size: 0.85rem;
  color: var(--muted);
}

/* Positioned wrapper for the dropdown: it holds the absolute placement and the
   hover-scroll arrows, while the <ul> inside it is the actual scroller. */
.suggestions-wrap {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 10;
  margin-top: 4px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface-2);
  overflow: hidden;
}

/* In the flow of the rung it opens inside, rather than floating over it.
   Overlaying would cover the chain below the node being filled — which is the
   half of the ladder the answer has to fit into.

   The height is pinned while results show, because an in-flow list that sized
   itself to the result count would shove the rest of the chain up and down on
   every keystroke. A message ("Searching…", "No matches") is one line and
   sizes itself: a full-height box holding two words reads as a list that
   failed to load. */
.suggestions-wrap.in-flow {
  position: static;
  margin-top: 0.6rem;
}

.suggestions-wrap.in-flow .suggestions {
  height: min(42svh, 300px);
  max-height: none;
}

.suggestions-wrap.in-flow.is-message .suggestions {
  height: auto;
}

/* Opened upward, because the list didn't fit below the field — see
   positionSuggestions in app.js, which also sets the list's max-height to the
   space it actually has, and which stops the flip at the submit bar rather than
   at the top of the viewport. */
.suggestions-wrap.flip-up {
  top: auto;
  bottom: 100%;
  margin-top: 0;
  margin-bottom: 4px;
}

/* Resting cap only: positionSuggestions overrides this inline every time the
   list is shown, narrowing it to whatever fits on screen. */
.suggestions {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 280px;
  overflow-y: auto;
}

/* Pointer-only, and hidden here rather than only styled below: everything that
   gives these arrows a size lives inside the @media (hover: hover) block, so on
   a touch device the element got no styling at all and laid its raw chevron out
   at full flex size — a 300px arrow floating over the board. The hover styles
   are an enhancement on top of this, not the whole definition. */
.scroll-arrow {
  display: none;
}

/* Hover-to-scroll arrows overlaid on the top/bottom edge of a scroll container
   (the chain rail and the autocomplete dropdowns). Hovering one scrolls that
   container continuously; JS hides the arrow when there's nothing to scroll to
   that way. Only meaningful with a pointer, so hidden on touch (native scroll
   still works). */
@media (hover: hover) {
  .scroll-arrow {
    position: absolute;
    left: 0;
    right: 0;
    height: 26px;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 20;
    color: var(--muted);
    cursor: default;
    transition: color 0.15s ease;
  }

  .scroll-arrow:hover {
    color: var(--accent-bright);
  }

  .scroll-arrow svg {
    width: 20px;
    height: 20px;
  }

  .scroll-arrow-up {
    top: 0;
    background: linear-gradient(
      to bottom,
      var(--surface-2) 40%,
      transparent 100%
    );
  }

  .scroll-arrow-down {
    bottom: 0;
    background: linear-gradient(to top, var(--surface-2) 40%, transparent 100%);
  }

  /* Left/right variants for the recap's horizontal rail — same arrow, rotated
     onto the x-axis: full height instead of full width, pinned to a side
     edge instead of top/bottom. */
  .scroll-arrow-left,
  .scroll-arrow-right {
    left: auto;
    right: auto;
    top: 0;
    bottom: 0;
    width: 32px;
    height: auto;
  }

  .scroll-arrow-left {
    left: 0;
    background: linear-gradient(
      to right,
      var(--surface-2) 40%,
      transparent 100%
    );
  }

  .scroll-arrow-right {
    right: 0;
    background: linear-gradient(
      to left,
      var(--surface-2) 40%,
      transparent 100%
    );
  }
}

.suggestion {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0.75rem;
  cursor: pointer;
  border-bottom: 1px solid var(--border);
}

.suggestion:last-child {
  border-bottom: none;
}

.suggestion:hover:not(.used) {
  background: var(--border);
}

.suggestion.active {
  outline: var(--focus-ring);
  /* Inset, so the highlight can't overflow the dropdown's rounded edge. */
  outline-offset: -2px;
}

.suggestion img {
  width: 32px;
  height: 48px;
  object-fit: cover;
  border-radius: 4px;
  background: var(--border);
  flex-shrink: 0;
}

/* "Searching…" / "No matches". Deliberately not a .suggestion: it must not
   pick up the hover background or the pointer cursor, both of which would
   invite a click on a row that does nothing. Height matches a suggestion row
   closely enough that the dropdown doesn't jump when results replace it. */
.suggestion-message {
  padding: 0.75rem;
  color: var(--muted);
  font-size: 0.9rem;
}

/* Node detail modal — the expanded, full-description view of a chain node */

/* Height in dvh rather than the bottom half of inset: 0, which is the bug
   that made these unusable on a phone. A position:fixed element resolves
   against the *layout* viewport, which on mobile Chrome is the tall one — the
   one you only see with the URL bar retracted — so the overlay's bottom edge
   sat behind the browser chrome. A card aligned to that edge went with it,
   and nothing scrolls to reach it because the overlay is fixed. dvh tracks
   what is actually on screen. */
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100dvh;
  background: rgba(5, 8, 15, 0.75);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  z-index: 100;
}

.modal-card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--accent-2);
  border-radius: 12px;
  padding: 1.75rem;
  max-width: 520px;
  width: 100%;
  max-height: 100%;
  overflow-y: auto;
  /* A card that scrolls internally shouldn't hand the leftover scroll to the
     board behind it once it hits an end. */
  overscroll-behavior: contain;
}

.modal-close {
  position: absolute;
  top: 0.85rem;
  right: 0.85rem;
  width: 32px;
  height: 32px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 50%;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}

.modal-close:hover {
  border-color: var(--accent-bright);
}

.modal-body {
  display: flex;
  gap: 1.25rem;
  margin-top: 1rem;
}

.modal-image {
  object-fit: cover;
  background: var(--surface-2);
  border: 1px solid var(--border);
  flex-shrink: 0;
}

.modal-image-movie {
  width: 130px;
  height: 195px;
  border-radius: 10px;
  border-color: var(--accent-2);
}

.modal-image-actor {
  width: 140px;
  height: 140px;
  border-radius: 50%;
}

.modal-body h3 {
  margin: 0 0 0.75rem;
  font-size: 1.2rem;
}

.modal-text {
  margin: 0;
  font-size: 0.95rem;
  color: var(--text);
  line-height: 1.6;
}

/* THE RECAP. The same centred column the landing and sign-in screens use: edge
   to edge inside the page's own padding on a phone, capped so it does not
   stretch on a desktop.
 
   Centring means every block shrinks to its own width by default; the ones that
   have to fill the width ask for it below. */
.recap-layout {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-5);
  padding: var(--space-2) 0 var(--space-6);
}

.recap-header {
  width: 100%;
  max-width: 640px;
  text-align: center;
}

/* One row of three equal buttons, width-matched to the stats box above so the
   column reads as one width. A row rather than the stack this used to be:
   stacked, the third button sat below the fold on a phone, which is the one
   thing a recap's buttons must not do. flex-basis: 0 so the three are equal
   regardless of label length — "Play again" is twice the width of "Exit" and
   would otherwise take twice the room.

   margin: 0 undoes .btn's inherited 4px/8px, which in a row is uneven gaps and
   an 8px overhang past the right edge. */
/* What is left of the stats box, and now the score's counterpart: the run's
   number above, the clock to the next one under it. Built like
   .recap-score-figure and for the same reason — a small tracked label beside a
   big tabular figure is the pair this screen already uses, so the two read as
   one design rather than as a caption that wandered in. It closes the block
   rather than sitting under the figure: the receipt is what built the score
   above it, and a countdown wedged between the two made the arithmetic read as
   the countdown's. The space the banked best gave up is why it can afford the
   size. */
.recap-next {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
}

/* Bright and not gold: gold on this screen means points earned, and a
   countdown has not been earned by anybody. Tabular so it doesn't twitch its
   own width once a second. */
.recap-next-value {
  font-size: var(--text-lg);
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.recap-actions,
.my-slot-actions {
  display: flex;
  gap: var(--space-2);
  width: 100%;
  max-width: 24rem;
}

.recap-actions .btn,
.my-slot-actions .btn,
.shared-actions .btn {
  flex: 1 1 0;
  min-width: 0;
  margin: 0;
  border-radius: var(--radius-pill);
  padding-inline: var(--space-2);
  padding-block: 0.85rem;
  font-size: var(--text-sm);
  white-space: nowrap;
}

/* The recap is the one screen where the footer is competing for room rather
   than closing off a short page, and it inherited 80px of nothing between the
   last button and the TMDB mark — the layout's own 2rem tail plus the footer's
   3rem margin — with the body's 4rem below that again. On a phone that put the
   attribution a whole thumb-flick past the last thing worth reading. Tightened
   only here: the landing and login pages are short enough that the generous
   version is still right for them. */
body[data-screen='dailyRecap'] {
  padding-bottom: var(--space-4);
}

body[data-screen='dailyRecap'] .recap-layout {
  padding-bottom: var(--space-3);
}

body[data-screen='dailyRecap'] .site-footer {
  margin-top: var(--space-5);
}

/* The run as a picture, with the tappable boxes over it. Everything inside is
   drawn by hand; this only decides how much room it gets, which is the whole of
   what decides how big its cells are.

   **A height rather than a shape**: the drawing fits whatever box it is handed,
   and on a phone what binds is almost always the height left over. This much
   leaves the three buttons below reachable without scrolling on the smallest
   screen the app is built for. */
.run-overview {
  position: relative;
  width: 100%;
  height: min(42vh, 22rem);
  min-height: 11rem;
  overflow: hidden;
  border-radius: var(--radius-lg);
}

/* A maxed run spends its height on the congratulation instead of on the last
   inch of the drawing, so the three actions still close above the fold. */
.recap-layout[data-run-view='maxed'] .run-overview {
  height: min(36vh, 19rem);
}

/* --- The banked best, as its own room ---------------------------------------
   This and the recap are one piece of markup, and for as long as they spoke
   with one *voice* too they were indistinguishable: a celebration headline over
   a run set hours ago, and the day's best quoted under a number that already
   was it. **What differs is posture** — the recap is a moment, this is a
   record — so the title gives up its colour and its size, and the line under it
   takes them. */
.recap-layout[data-recap-mode='best'] .final-score {
  font-size: 1.4rem;
  color: var(--text);
  margin-bottom: 0;
}

/* **And with the tabs up it goes entirely.** They already say "Today's best",
   so the title would be the same sentence twice — and they stand *in place of*
   it precisely so they cost no height on a screen whose buttons already fall
   off the bottom on a long run. */
.recap-layout[data-recap-mode='best']
  .my-chains-tabs:not(.hidden)
  ~ .final-score {
  display: none;
}

.recap-stamp {
  margin: var(--space-2) 0 0;
  color: var(--accent-2);
}

/* See-through, sitting over a picture that has already drawn the node. **The
   target is a whole chain and its number, never a single cell**: at the sizes a
   long run draws, aiming at one node would be a coin flip. Its text is for
   screen readers; the picture underneath is the label. */
.run-chain-hit {
  position: absolute;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: transparent;
  font-size: 0;
  cursor: pointer;
  transition:
    background 0.12s,
    border-color 0.12s;
}

.run-chain-hit:hover,
.run-chain-hit:focus-visible {
  background: rgb(var(--accent-bright-rgb) / 0.1);
  border-color: rgb(var(--accent-bright-rgb) / 0.55);
  outline: none;
}

/* The drawing and its caption, close enough to read as one object. The
   column's own gap is var(--space-5), which put the caption midway between
   the picture it describes and the buttons below it — near enough to neither
   to belong to either. */
.run-overview-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: 100%;
}

.run-overview-hint {
  margin: 0;
  text-align: center;
}

/* The cap, and only the cap. Sits where the hint line would, so hitting the
   ceiling replaces the invitation to keep going rather than sitting beside
   it. */
.run-ending {
  text-align: center;
  transition: opacity 0.45s ease;
}

/* Held back while a maxed run draws itself, so the congratulation lands on a
   finished grid rather than narrating one that is still arriving. */
.run-ending-waiting {
  opacity: 0;
}

.run-ending-line {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: bold;
}

.run-ending-sub {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
  color: var(--text-dim);
}

/* The way back out of a chain. A row, because the button and the label are one
   thing: "back to the run, from bonus chain 3". */
.run-crumb {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  width: 100%;
  max-width: 640px;
}

/* The chain's number, exactly as the picture draws it — same block, same
   colours, same figure — so the two name a chain the same way and the tap that
   got here is visibly answered. The colours are written out because they are
   matching a canvas that paints in written-out colours; the two are one design,
   and the only way this fails is by drifting apart. */
.run-crumb-badge {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 1.6rem;
  height: 1.6rem;
  border: 1px solid rgb(0 176 243 / 0.45);
  border-radius: 0.3rem;
  background: #0e1626;
  color: #7fd0f7;
  font-size: 0.95rem;
  font-weight: 700;
  line-height: 1;
}

.run-crumb-back {
  flex: 0 0 auto;
  margin: 0;
  padding: 0.4rem 1.1rem;
  font-size: var(--text-sm);
}

/* The filmstrip's minimap. One dot per node; the active one is whichever node
   is nearest the middle of the strip (markActiveStripDot). Sized off the same
   tokens as the board's pip row so the two minimaps in this app match. */
.recap-strip-dots {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-top: calc(var(--space-4) * -1);
}

.recap-strip-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--border);
  transition: background 0.15s;
}

.recap-strip-dot.is-active {
  background: var(--accent-bright);
}

/* Now the actual <h1> — the film count is the headline, not "Chain Complete"
   over a subtitle. A timeout at length 1 is a loss; a count doesn't
   congratulate it the way "Chain Complete" did. */
.final-score {
  font-size: 2rem;
  font-weight: bold;
  color: var(--accent-bright);
  margin: 0 0 0.25rem;
}

/* ---------------------------------------------------------------------
   Chain of the Day — docs/plan-chain-of-the-day.md.

   The board is the screen, and on a phone it is the *whole* screen: one
   column at every width, centred and capped, with no side column and no
   breakpoint that re-lays it out. The desktop build put the search field in
   a 300px column beside the rail and stacked that column under the ladder
   below 760px, which on a phone meant tapping a blank sent the field a
   screen and a half away.

   A selected node opens where it is instead — mountDailyRung (app.js) moves
   the panel into the rail directly under the node — so the only rule on this
   screen is "tap a node and the node opens". A blank opens the search, a
   filled node opens its summary, and the chain parts around whichever it is.
   --------------------------------------------------------------------- */

/* Roughly a large phone, held. Wider screens get the same column centred
   rather than a second layout: the chain is a fixed-length ladder and a
   1320px stage was only ever somewhere for it to be stranded in a corner
   of. .chain-rail's own 380px cap (below) is what holds the zigzag's angle,
   so this is the frame around it, not the thing sizing it. */
.daily-board {
  max-width: 420px;
  margin-inline: auto;
  padding-bottom: var(--space-8);
}

/* The day's identity, above the strip and free to scroll away with the top
   of the chain — you need to know which puzzle you're on when you arrive,
   not while you're solving it. */
.daily-cadence {
  margin: 0 0 var(--space-1);
  font-size: var(--text-sm);
  color: var(--muted);
}

.daily-day {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.daily-date {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--text);
}

.daily-number {
  font-size: var(--text-sm);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* Pushed to the far end of the row, away from the date it isn't part of. */
.daily-day .help-btn {
  margin-left: auto;
  align-self: center;
}

/* Pinned under the app header while the chain scrolls beneath it. The ladder
   is about a screen and a half on a phone, so this row is the only view of
   the whole puzzle at once and the only way to reach a node without scrolling
   to it — which is what earns it permanent pixels when the date above it
   doesn't get any.

   Its own background, and the full bleed either side of the column, because
   nodes pass underneath it: without both, the chain shows through. */
.daily-strip {
  position: sticky;
  top: var(--header-h);
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  margin-bottom: var(--space-2);
  background: var(--bg);
  border-bottom: 1px solid var(--border);
  box-shadow: 0 6px 12px -8px rgb(0 0 0 / 0.9);
}

/* One pip per node, in chain order — a map of the puzzle rather than a
   progress bar. Films are rounded rectangles and actors are circles, the same
   two shapes the rail uses, so "which kind of blank is left, and where" reads
   at a glance while the rail is scrolled somewhere else. Tapping one jumps to
   that node. Never wraps here: a second row would double what the strip costs
   on every screen, and eleven to sixteen pips fit one row comfortably. */
.daily-pips {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

.daily-pip {
  padding: 0;
  border: 1px solid var(--border);
  background: var(--surface-2);
  cursor: pointer;
  flex: none;
  transition:
    transform 0.12s ease,
    background 0.15s ease,
    border-color 0.15s ease;
}

.daily-pip-movie {
  width: 11px;
  height: 16px;
  border-radius: 3px;
}

.daily-pip-actor {
  width: 14px;
  height: 14px;
  border-radius: 50%;
}

.daily-pip-known {
  background: var(--accent);
  border-color: var(--accent-bright);
}

.daily-pip:hover {
  transform: scale(1.3);
}

.daily-pip.selected {
  border-color: var(--accent-2);
  box-shadow: 0 0 0 2px rgb(var(--accent-2-rgb) / 0.5);
}

/* Demoted under the score above it: the pips beside it already say how much of
   the chain is filled, shape by shape, so the fraction is a second telling.
   The running total isn't told anywhere else. */
.daily-progress {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--muted);
  white-space: nowrap;
}

/* Right-aligned so the two numbers share an edge rather than a left margin:
   they change width as they climb (a 3-digit score becomes 4) and a ragged
   right edge next to the pips reads as a wobble. position: relative is for the
   delta, which floats out of the top of this box. */
.daily-readout {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  line-height: 1.1;
}

/* Gold, like everything earned on this screen, with evenly spaced digits so
   the strip does not twitch as it counts up. **Sized to be the thing you read
   in that strip**: the hint button is on screen while you decide whether to
   press it, and a cost you have to hunt for is a cost you press past. */
.daily-score {
  font-size: var(--text-lg);
  font-weight: 800;
  color: var(--accent-2);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  letter-spacing: -0.01em;
}

/* A run in the red. Not --danger's full weight — nothing has gone wrong, the
   peeks just haven't been earned back yet — but plainly not the gold that
   means points in hand. */
.daily-score.is-negative {
  color: var(--muted);
}

/* A unit, so a gold number over a "3 / 9" isn't a second fraction. Quiet
   enough that the figure is still what you read. */
.daily-score-unit {
  margin-left: 3px;
  font-size: var(--text-2xs);
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* What just changed, floating off the top of the score. Absolute so it costs
   the sticky strip no height — it is a moment, not a row — and pointer-events
   none so it can pass over the pips without eating a tap on one. Green up,
   amber down: the score's own gold means "in hand", these mean "just moved". */
.daily-score-delta {
  position: absolute;
  right: 0;
  bottom: calc(100% - 2px);
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  font-size: var(--text-2xs);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}

.daily-score-delta.is-up {
  color: var(--success);
  background: rgb(var(--success-rgb) / 0.16);
}

.daily-score-delta.is-down {
  color: var(--accent-2-bright);
  background: rgb(var(--accent-2-rgb) / 0.16);
}

.daily-score-delta.is-active {
  opacity: 1;
  animation: daily-score-delta 1.1s ease-out forwards;
}

@keyframes daily-score-delta {
  0% {
    opacity: 0;
    transform: translateY(6px);
  }
  18% {
    opacity: 1;
    transform: translateY(0);
  }
  70% {
    opacity: 1;
    transform: translateY(-2px);
  }
  100% {
    opacity: 0;
    transform: translateY(-10px);
  }
}

/* The delta still appears and still says what changed — it just doesn't
   travel. JS clears .is-active on a timer either way, so it leaves without
   the animation's help. */
@media (prefers-reduced-motion: reduce) {
  .daily-score-delta.is-active {
    animation: none;
    transform: none;
  }
}

/* The chain is a filmstrip, so give it perforations. Holes are punched in
   --bg (the darkest surface) out of a --surface band, so they read as holes
   rather than as light dots — the inverse is what makes a dashed line. Runs
   the rail's full height like the gate a film runs through. This is the app's
   only cinematic motif outside the logo, and it costs one gradient and 18px
   of the column; kept low-contrast deliberately, texture noticed on second
   look rather than a costume.

   On .chain-rail rather than a scrolling wrapper around it, because there is
   no such wrapper anymore — the rail scrolls with the page. */
.chain-rail:not(.chain-rail-horizontal)::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 11px;
  border-radius: 3px;
  opacity: 0.65;
  pointer-events: none;
  background:
    repeating-linear-gradient(to bottom, transparent 0 7px, var(--bg) 7px 17px),
    var(--surface);
}

/* The ladder keeps its own width rather than taking the column's. The zigzag's
   diagonals are drawn between the thumbs the auto-margins push to opposite
   edges, so the rail's width *is* the angle of the chain — let it stretch and
   the links flatten into near-horizontal threads with a lake of empty space
   between them, which stops reading as a chain at all. */
.daily-board .chain-rail {
  max-width: 380px;
  gap: 1.1rem;
}

/* Clears the strip stuck to the top, so a node scrolled to the top lands just
   below it rather than underneath it. */
.daily-board .chain-node {
  scroll-margin-top: calc(var(--header-h) + 3.25rem);
}

/* Big, because here the chain *is* the board and can afford the size — along
   with labels at reading size rather than caption size.
   Scoped past .chain-rail:not(.chain-rail-horizontal) .node-label (0,3,0)
   rather than tying with it, so the order these blocks appear in the file
   isn't load-bearing. */
.daily-board .chain-node-movie .node-thumb {
  width: 58px;
  height: 87px;
}

.daily-board .chain-node-actor .node-thumb {
  width: 62px;
  height: 62px;
}

.daily-board .chain-rail:not(.chain-rail-horizontal) .node-label {
  max-width: 200px;
  font-size: var(--text-sm);
}
/* A blank is the game itself, so it gets a pointer and sits only lightly
   dimmed: **these are targets, not scenery.** */
.chain-node-blank {
  cursor: pointer;
}

.chain-rail .chain-node-blank {
  opacity: 0.8;
}

.chain-rail .chain-node-blank:hover {
  opacity: 1;
  transform: scale(1.06);
}

/* A guess the server has not ruled on yet (showDailyPendingNode). The poster
   and the title are already real — what is provisional is whether they stay,
   so the node reads as present but unsettled: dimmed, breathing, and with the
   confident accent border traded for a dashed one. Everything here is paint,
   deliberately: a pending node occupies exactly the space the filled node will,
   so accepting one can't shift the rail and rejecting one can't shift it back. */
.chain-node-pending {
  opacity: 0.62;
}

.chain-node-pending .node-thumb {
  border-style: dashed;
  border-color: var(--muted);
}

@media (prefers-reduced-motion: no-preference) {
  .chain-node-pending {
    animation: node-pending-breathe 1.1s ease-in-out infinite;
  }

  /* Not a bounce. A rejection that draws attention to itself is worse than one
     that simply happens — two small counter-moves and the blank is back. */
  /* No fill mode: a shake starts and ends at transform: none, so `both` would
     buy nothing and cost something — a held final keyframe outranks
     .chain-node-blank:hover's scale, leaving every rejected node unable to
     respond to the pointer again. */
  .chain-node-retracting {
    animation: node-retract 260ms ease-out;
  }
}

@keyframes node-pending-breathe {
  50% {
    opacity: 0.95;
  }
}

@keyframes node-retract {
  0%,
  100% {
    transform: none;
  }

  25% {
    transform: translateX(-4px);
  }

  75% {
    transform: translateX(4px);
  }
}

/* One-click shortcut to clearDailyNode, drawn by drawDailyClearButtons at
   the corner of a filled node's thumb — same absolutely-positioned-sibling
   trick the badges use, because .chain-node is a <button> and a button
   cannot contain another one. Opacity is short of 1 so the rail doesn't read as covered in
   controls once several nodes are filled, but --bg/--text/--muted (rather
   than --surface-2/--border/--muted) keep it legible over both the dark
   background and a bright poster/photo crop. Mouse-only affordance (see
   the aria-hidden/tabIndex on the element itself) — the flyout's "Clear this
   link" stays the keyboard/AT path to the same action. */
.daily-node-clear {
  position: absolute;
  z-index: 2;
  transform: translate(-50%, -50%);
  width: 22px;
  height: 22px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--muted);
  background: var(--bg);
  color: var(--text);
  opacity: 0.85;
  cursor: pointer;
  transition:
    opacity 0.15s ease,
    color 0.15s ease,
    border-color 0.15s ease,
    transform 0.15s ease;
}

.daily-node-clear svg {
  width: 13px;
  height: 13px;
}

.daily-node-clear:hover {
  opacity: 1;
  color: var(--danger);
  border-color: var(--danger);
  transform: translate(-50%, -50%) scale(1.15);
}

/* A rung: the card a node opens into, sitting in the ladder between the node
   it belongs to and the rest of the chain. Both panels take this — the search
   for a blank, the summary for a filled node — because from the player's side
   they are one thing, "the node I just tapped, opened".

   Stretched to the rail's full width (the nodes either side of it are pushed
   to opposite edges by their auto margins, so a rung matching them would sit
   under one and read as belonging to it). Opaque, and above the connector SVG,
   so the chain visibly runs into the top of the card and out of the bottom
   rather than across it. */
.daily-rung {
  position: relative;
  z-index: 1;
  align-self: stretch;
  margin: 0;
  background: var(--surface-2);
  border: 1px solid var(--accent-2);
  border-radius: var(--radius-md);
  box-shadow: var(--elevation-3);
  padding: var(--space-4);
}

/* The fill panel at rest, above the chain: one button, and two lines that only
   the "no puzzle" and "chain complete" cases have anything to put in. No card
   and no border — parked, it is not part of the chain. */
.daily-fill {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.daily-fill.is-docked {
  margin-bottom: var(--space-5);
}

/* Full-width docked, where it's the only thing on the row and the way into the
   puzzle; the field frame it shares a card with when mounted has its own
   edges, so nothing there needs stretching. */
.daily-fill.is-docked #btn-daily-next-blank {
  width: 100%;
}

/* The column's own gap already spaces the label from what follows. */
.daily-fill .panel-label {
  margin-bottom: 0;
}

/* No second box inside the card: mounted, the rung *is* the frame, and a
   bordered field inside a bordered card reads as two panels that failed to
   merge. */
.daily-rung .field-frame {
  background: none;
  border: none;
  border-radius: 0;
  padding: 0;
}

/* Names the actual neighbours ("Who was in both Joker and Gladiator?"), so
   it carries the puzzle rather than restating the label above it. Given the
   weight of real text for that reason, not the muted caption it replaced. */
.daily-prompt {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--text);
}

/* Its own row under the chain, full width and the same size as "Fill the next
   blank". Bordered rather than filled: it is the only way a chain ends, so it
   has to be easy to find, but it is not the next move. Gold was considered and
   dropped — on this screen it already means the score and the selected link. */
.daily-board .daily-finish {
  display: block;
  width: 100%;
  margin: var(--space-6) 0 0;
}

/* The node this card belongs to is one thumb above it, showing the same poster
   at a larger size and the same title beside it. Repeating both inside the card
   reads as a rendering fault; what the rung is actually for is the overview,
   which the rail has nowhere to put. */
#daily-node-detail .node-detail-image {
  display: none;
}

/* The empty line .error-text reserves is there so an arriving error doesn't
   shift the layout. At the bottom of a rung it's the last thing in the card,
   with only the card's own padding under it, so the reserve reads as the card
   failing to close up — and there's nothing below it for an error to shift
   anyway. Daily rejects almost nothing besides: the pool the suggestions come
   from is already the legal set. */
.daily-rung .error-text:empty {
  display: none;
}

/* The board's card reads at 0.8rem rather than the shared 0.75, and takes the
   extra rem the larger type needs — the fade and the padding it falls on are
   .node-detail-text's own.

   8rem, not 16: at 16 a long TMDB biography pushed "Extend the chain from
   here" off the bottom of the phone, and the one control that opens new
   content is not allowed to live below the fold. This is the only one of the
   three cards with a control under it, which is why it is the only one that
   had to pick its cap against the fold rather than against the reading. */
#daily-node-detail .node-detail-text {
  max-height: 8rem;
  font-size: 0.8rem;
}
/* Quiet, and last, inside the flyout: backing out of a guess is a real need
   — there is no undo history — but it should not read as the point of opening
   a node. */
.daily-clear-link {
  display: block;
  margin-top: 0.7rem;
  padding: 0;
  border: none;
  background: none;
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: 600;
  cursor: pointer;
}

.daily-clear-link:hover {
  color: var(--danger);
}

/* The choice at the end of a chain. Two real offers, stacked on a phone so
   neither is a stretch for a thumb and neither reads as the small print.

   A mark, a headline and two offers, spaced a little wider as they go down.
   **Nothing here is prose, so nothing here has to be read** — which is the
   right amount of work to ask of a celebration. Narrow, because three centred
   things want a column rather than a page. */
.chain-done-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: 21rem;
  padding: var(--space-6) var(--space-5) var(--space-5);
  text-align: center;
}

/* The mark carries the card now that the paragraph does not, so it is sized
   as a subject rather than as decoration. */
.chain-done-mark {
  display: block;
  width: 10rem;
  height: auto;
}

.chain-done-link {
  fill: none;
  stroke: var(--accent-bright);
  stroke-width: 3;
  stroke-linecap: round;
  filter: drop-shadow(0 0 4px var(--accent-bright));
}

.chain-done-film {
  rx: 5;
  fill: var(--surface-2);
  stroke: var(--accent-bright);
  stroke-width: 2;
}

/* Quieter than the films it sits between, the way an actor node is on the
   board — --accent-2 is the score's gold and means something else here. */
.chain-done-actor {
  fill: var(--surface-2);
  stroke: rgb(var(--accent-bright-rgb) / 0.5);
  stroke-width: 2;
}

/* The one line of type on the card, so it takes the accent the recap's own
   headline wears — the two are the same moment seen twice. */
.chain-done-title {
  margin: var(--space-4) 0 0;
  font-size: 1.6rem;
  font-weight: bold;
  color: var(--accent-bright);
  line-height: 1.15;
}

.chain-done-actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: 100%;
  margin-top: var(--space-5);
}

/* Full width and the same height, so the pair reads as one choice with a
   preferred half rather than a button and a footnote. The margin is cleared
   because the shared button style's own would make an uneven gap in a
   stack. */
.chain-done-actions .btn {
  width: 100%;
  margin: 0;
  border-radius: var(--radius-pill);
  padding-block: 0.85rem;
}

/* Growing a new chain. Full width and the loudest weight, because it is not a
   tidying action beside Clear — it is the one move that makes the board bigger,
   and it only ever appears on a chain just finished. */
.daily-grow {
  display: block;
  width: 100%;
  margin-top: var(--space-3);
}

.daily-grow:disabled {
  opacity: 0.7;
  cursor: progress;
}

.modal-actions {
  display: flex;
  gap: 0.75rem;
  margin-top: 1rem;
}

.modal-actions .btn {
  flex: 1;
}

/* --- How to play ------------------------------------------------------------
   The rules, and the only place a player can read them. It opens on the way in
   to the board and is reachable afterwards from the ? beside the date. */

/* Sized to the small line beside it rather than to a finger, then given the
   room back as an invisible hit area: a big ? next to a small label would read
   as the more important of the two. */
.help-btn {
  position: relative;
  flex: none;
  width: 20px;
  height: 20px;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
}

/* The visible circle stays 20px; the tappable one is 44px. */
.help-btn::after {
  content: '';
  position: absolute;
  inset: -12px;
}

.help-btn:hover {
  border-color: var(--accent-bright);
  color: var(--accent-bright);
}

.help-btn:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.howto-title {
  margin: 0 0 var(--space-4);
  font-size: var(--text-xl);
}

/* The diagram in #howto-modal. Capped well short of the card's width: it is a
   thumbnail of the board, not a poster, and every pixel it takes is a pixel
   the card needs to clear the fold on a phone without scrolling. Everything
   inside is painted from the tokens the rail itself uses, so the sketch and
   the real board are recognisably the same object. */
.howto-diagram {
  display: block;
  width: 100%;
  max-width: 200px;
  height: auto;
  margin: 0 auto var(--space-4);
}

/* .chain-connector and .chain-connector-active, restated on SVG elements that
   aren't in the rail. Kept as literal copies of those two rules rather than
   shared: the rail's are applied to lines drawn by measurement at runtime, and
   tying the two together would make either one hard to move. */
.howto-edge {
  fill: none;
  stroke: var(--border);
  stroke-width: 2;
  stroke-dasharray: 4 5;
}

.howto-edge-live {
  fill: none;
  stroke: var(--accent-bright);
  stroke-width: 3;
  filter: drop-shadow(0 0 4px var(--accent-bright));
  /* pathLength="1" on the line makes one unit the whole edge, so a single
     dashoffset sweeps it whatever its length. Offset 0 — fully lit — is the
     resting state, which is also what a reduced-motion player sees. */
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
}

/* The two sweep in turn, then both go out and the cycle repeats, so a player
   who opens the card mid-cycle still sees the whole gesture within a few
   seconds. Held lit for most of the cycle: the finished chain is the point,
   the travel is just how it gets there. */
@media (prefers-reduced-motion: no-preference) {
  .howto-edge-a {
    animation: howto-edge-a 4.4s linear infinite;
  }

  .howto-edge-b {
    animation: howto-edge-b 4.4s linear infinite;
  }
}

@keyframes howto-edge-a {
  0% {
    stroke-dashoffset: 1;
    opacity: 1;
  }
  16% {
    stroke-dashoffset: 0;
  }
  82% {
    stroke-dashoffset: 0;
    opacity: 1;
  }
  93% {
    stroke-dashoffset: 0;
    opacity: 0;
  }
  100% {
    stroke-dashoffset: 1;
    opacity: 0;
  }
}

@keyframes howto-edge-b {
  0%,
  20% {
    stroke-dashoffset: 1;
    opacity: 1;
  }
  36% {
    stroke-dashoffset: 0;
  }
  82% {
    stroke-dashoffset: 0;
    opacity: 1;
  }
  93% {
    stroke-dashoffset: 0;
    opacity: 0;
  }
  100% {
    stroke-dashoffset: 1;
    opacity: 0;
  }
}

/* A blank node's thumb, as .chain-node-placeholder .node-thumb-placeholder
   draws one: dashed, filled with the surface behind it so the edge passing
   underneath is hidden, glyph in muted. */
.howto-thumb {
  fill: var(--surface-2);
  stroke: var(--border);
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
}

.howto-glyph-film {
  fill: none;
  stroke: var(--muted);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.howto-glyph-actor {
  fill: var(--muted);
}

/* Muted italic, flowing inward from the thumb — the placeholder label's own
   treatment, and the vertical rail's own layout. */
.howto-node-label {
  fill: var(--muted);
  font-size: 12px;
  font-style: italic;
}

.howto-label-end {
  text-anchor: end;
}

/* Numbered because the content genuinely is ordered — this is the sequence a
   player moves through, not a pair of unranked facts. */
.howto-steps {
  margin: 0;
  padding-left: 1.35rem;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  color: var(--muted);
  line-height: 1.55;
}

.howto-steps strong {
  color: var(--text);
  font-weight: 700;
}

.howto-steps li::marker {
  color: var(--accent-2);
  font-weight: 700;
}

.howto-footer {
  margin: var(--space-4) 0 0;
  color: var(--muted);
  font-size: var(--text-sm);
}

.howto-suppress {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: var(--text-sm);
}

/* On a phone the card meets the bottom edge and reads as a sheet, which is
   the idiom the board's own controls use — not a second modal language. */
@media (max-width: 639px) {
  /* Padding to zero as well as the alignment change: the overlay's 1.5rem
     gutter is what keeps a centred card off the edges, and a sheet that stops
     24px short of the bottom reads as a card that failed to land. */
  .modal-overlay {
    align-items: flex-end;
    padding: 0;
  }

  .modal-card {
    max-height: 100%;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    border-bottom: none;
    padding: var(--space-6) var(--space-5);
    /* Clears the home indicator on a phone without a notch paying for it. */
    padding-bottom: max(var(--space-6), env(safe-area-inset-bottom));
  }
}

/* …with an opt-out. The sheet idiom above is for surfaces that act on the
   board, which is why they rise from the edge you act with. A surface you only
   *read* — the how-to before you start, a node's full summary while you're
   deciding — isn't reaching for your thumb, and pinning it to the bottom edge
   both fights the content and puts it under the keyboard's old ground. Those
   carry .modal-centered and stay cards, centred, with the board dimmed evenly
   around them. */
@media (max-width: 639px) {
  .modal-overlay.modal-centered {
    align-items: center;
    padding: var(--space-4);
  }

  .modal-centered .modal-card {
    border-radius: var(--radius-lg);
    border-bottom: 1px solid var(--accent-2);
    padding-bottom: var(--space-6);
    max-height: 100%;
  }
}

/* The shared .error-text reserves a line so a message doesn't shove the form
   below it; on the landing there is nothing below to shove, and an empty
   reserved line just opens a gap above the day rule. */
#landing-error {
  min-height: 0;
  margin: 0;
}

#landing-error:empty {
  display: none;
}

/* --- The hint ---------------------------------------------------------------
   A blank's legal answers are bought and not given, and these are the two
   states that follow: a button before, and an accented box with a count once it
   has been pressed. */

/* The button shares the box's row, so it costs no height. Pinned to the bottom
   of that row rather than stretched, which keeps it the box's own height
   without growing with the list of suggestions below. */
.daily-input-row {
  display: flex;
  align-items: stretch;
  gap: var(--space-2);
}

.daily-input-row input {
  flex: 1;
  min-width: 0;
}

/* .btn's own vertical padding would make this taller than the input it sits
   beside; height comes from the row instead. No glyph: the label is one short
   word in a field that already has an icon-free input, and a funnel (what it
   does) next to "Hint" (what it is) reads as two names for one control. */
.daily-hint-btn {
  margin: 0;
  padding: 0 var(--space-4);
  flex: 0 0 auto;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
}

/* Bought. The frame takes the accent colour, so a box now offering only legal
   answers is distinguishable at a glance from one offering everything. */
.autocomplete-field.is-hinted input {
  border-color: var(--accent);
}

.daily-hint-count {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--muted);
}

/* The lightbulb, in the button. **Nothing marks the node itself**: a peek
   leaves nothing behind on the board, and the chain already spends three
   different borders saying other things. */
.daily-hint-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.daily-hint-btn-icon {
  display: flex;
}

.daily-hint-btn-icon svg {
  width: 15px;
  height: 15px;
}

/* The moment between chains: a full-screen ground the run is painted onto,
   faded over the board and then away again. **Solid rather than see-through**,
   because the two pictures run at right angles to each other and letting them
   overlap mid-fade reads as a glitch rather than a transition. */
.run-cut {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: var(--bg);
  opacity: 0;
  /* The timings are set from the same numbers the animation itself waits on,
     so retuning the pace in one place cannot leave the other behind. What is
     written here is only a fallback. */
  transition: opacity var(--run-cut-fade-out, 600ms) ease;
}

.run-cut-shown {
  opacity: 1;
  transition-duration: var(--run-cut-fade-in, 600ms);
}

.run-cut canvas {
  display: block;
  width: 100%;
  height: 100%;
}

@media (prefers-reduced-motion: reduce) {
  .run-cut {
    transition: none;
  }
}

/* --- What an ending brings with it ------------------------------------------ */

/* The celebration when a chain takes the day's best. Over everything and part
   of nothing: it never takes up space, and it cannot swallow a tap on the
   buttons underneath. It removes itself when it finishes. */
.recap-burst {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 2;
}

/* What a player with motion turned off gets instead of the burst, and what
   everyone keeps once it has gone. */
.recap-best-note {
  margin: var(--space-1) 0 0;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-2);
}

/* The figure is already gold, so a best is marked by lift rather than by
   colour — there is no second gold to promote it to. */
.recap-score.is-best .recap-score-value {
  text-shadow: 0 0 18px rgb(253 185 10 / 0.55);
}

/* D61: the ending's write did not land. Deliberately not a toast — this one
   has to stay put, because it is the only thing telling the player the chain
   they are looking at is still theirs to go back to. */
.recap-unsaved {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  max-width: 20rem;
  padding: var(--space-3);
  border: 1px solid rgb(239 68 68 / 0.35);
  border-radius: var(--radius-md);
  background: rgb(239 68 68 / 0.08);
  text-align: center;
}

.recap-unsaved-line {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text);
}

/* --- The share tray ---------------------------------------------------------
   A sheet from the bottom, which the app had none of before. The phone is the
   design target and a sheet is where a phone expects a list of destinations; on
   a wide screen it settles into the middle like every other dialog, because
   there is no thumb to be near. */
.modal-overlay.modal-sheet {
  align-items: flex-end;
  padding: 0;
}

.modal-card.share-tray-card {
  max-width: 520px;
  border-radius: 16px 16px 0 0;
  border-bottom: 0;
  padding: 1.5rem 1.25rem calc(1.25rem + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

@media (min-width: 640px) {
  .modal-overlay.modal-sheet {
    align-items: center;
    padding: 1.5rem;
  }
  .modal-card.share-tray-card {
    border-radius: 12px;
    border-bottom: 1px solid var(--accent-2);
  }
}

.share-tray-title {
  margin: 0;
  font-size: 1.15rem;
}

.share-tray-rows {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

/* Full-width rows with a real target height: this is a list to be thumbed,
   not a row of pills. 48px is the floor the rest of the app's tap targets
   already keep. */
.share-row {
  appearance: none;
  width: 100%;
  min-height: 48px;
  /* Vertical padding for the armed label, which is a sentence rather than a
     word and wraps on a narrow phone. A one-word row is still 48px. */
  padding: var(--space-2) var(--space-3);
  text-align: left;
  font: inherit;
  font-weight: 600;
  color: var(--text);
  background: var(--surface-2);
  /* --border, not --accent-2. Six gold-edged rows read as six primary actions,
     and none of these is one — the destination is the player's choice and the
     tray has no recommendation to make. The card keeps the accent edge every
     other modal wears. */
  border: 1px solid var(--border);
  border-radius: var(--radius-md, 10px);
  cursor: pointer;
}

.share-row:hover {
  border-color: var(--accent);
}

/* Same idiom as .daily-finish.is-confirming and #btn-delete-account: --danger
   for the armed state rather than a dialog on top of a sheet. The mark stays
   put — it is what says which row is armed — and only the label changes. */
.share-row.is-confirming {
  color: var(--danger);
  border-color: var(--danger);
}

.share-row[disabled] {
  opacity: 0.55;
  cursor: default;
}

.share-tray-close {
  width: 100%;
}

/* --- A shared run, seen by a stranger -------------------------------------- */
.shared-layout {
  display: flex;
  flex-direction: column;
  /* Tighter than the usual gap, and that tightening is what pays for the two
     small labels above the posters. This screen is the one place on a phone
     where the head, a picture and the buttons all have to fit together. */
  gap: 10px;
  width: 100%;
  max-width: 34rem;
  margin: 0 auto;
  padding: var(--space-4) var(--space-3) var(--space-6);
}

.shared-header {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  text-align: center;
}

.shared-title {
  margin: 0;
  font-size: var(--text-2xl, 1.6rem);
}

/* The card's endpoint band. Two films and their titles is the whole of what a
   stranger can read in a chain, which is why both the image and the page open
   with them — but the page is not the card's canvas and does not get the
   card's 84x126.

   **The caption is wider than the poster it sits under.** Locked to the
   poster's own width the titles wrapped to three lines, and the wrapping —
   not the artwork — was most of the band: 126px of poster against 47px of
   label. A 56x84 poster in a 140px column fits "Once Upon a Time in America"
   on two lines with room to spare, and takes the band from 181px to 123px.
   Two lines and then an ellipsis, which is D84's rule on the card for the
   same reason: those two films are what this is for. Clamping also makes the
   band a fixed height rather than one that depends on how long a title
   happens to be. */
.shared-ends {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: var(--space-4);
}

.shared-end {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  /* The caption's width, not the poster's — see above. Two of these plus the
     arrow and the gaps come to 332px, which clears a 375px phone's 343px of
     content box. */
  width: 140px;
}

.shared-end-poster {
  width: 56px;
  height: 84px;
  object-fit: cover;
  border-radius: 6px;
  background: var(--surface-2);
  border: 1px solid rgb(31 143 214 / 0.55);
}

/* Above the poster, in the landing's small-caps voice — a column header for
   the film under it rather than a caption on it. */
.shared-end-role {
  font-size: 0.62rem;
  line-height: 1.2;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted, #8b94a8);
}

.shared-end-label {
  font-size: var(--text-xs, 0.8rem);
  line-height: 1.3;
  color: var(--muted, #8b94a8);
  /* The column is wider than the poster (see .shared-end), so a title that
     wraps has slack to sit in and a left edge would put its second line under
     nothing. Centred on the poster it names. */
  text-align: center;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

/* Centred on the posters and not on the pair, which the captions would drag
   it below — they are different lengths and one of them wraps. Re-derived
   with the poster: the role label and its gap, plus half of 84px, less half
   the glyph. */
.shared-end-arrow {
  margin-top: 47px;
  color: var(--accent-2);
  font-size: 1.5rem;
  line-height: 1;
}

/* A run whose head is also its tail — shared with nothing filled — draws one
   film and no arrow. The same poster twice with an arrow between would be a
   claim about a journey that has not started. */
.shared-ends.one-end .shared-end-arrow {
  display: none;
}

.shared-meta {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
}

.shared-chain {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* The rail and the detail panel are the recap's, in the recap's classes, so
   there is nothing to style here beyond giving the pair room. What this page
   deliberately does not have is a list of its own — see renderSharedChain. */
#shared-chain-scroll {
  width: 100%;
}

/* Stacked, not a row: both buttons want the same width and two of them at
   14rem do not fit a 390px phone side by side. */
/* The landing's line, in the head's last slot rather than above a divider: the
   rule and the space it hangs on belong to the landing, where the line closes
   off the mark. Here the poster band above it is separation enough. */
.shared-day {
  margin: 0;
  padding-top: 0;
  border-top: none;
  width: auto;
}

/* Today's best's own pair, on this screen: two pills in a row. Both roles get
   it — the owner's is Copy link and Exit, the visitor's is Build your own
   chain and Copy link — because this is that screen with a different verb on
   the lead action, and a column was a second shape for the same two buttons at
   twice the height. */
.shared-actions {
  display: flex;
  gap: var(--space-2);
  width: 100%;
  max-width: 24rem;
  margin-inline: auto;
}

/* --- My Chains: two slots, and tabs rather than a swipe -------------------- */
.my-chains-tabs {
  display: flex;
  gap: 0;
  /* Sized to its contents and centred by its margins: the block this sits in
     does not lay its children out, so asking to be centred did nothing and it
     spanned
     the screen. */
  width: fit-content;
  margin-inline: auto;
  padding: 3px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
}

.my-chains-tab {
  appearance: none;
  border: 0;
  background: none;
  font: inherit;
  font-weight: 600;
  font-size: var(--text-sm, 0.875rem);
  color: var(--muted, #8b94a8);
  padding: 0.45rem 1.1rem;
  border-radius: 999px;
  cursor: pointer;
}

.my-chains-tab[aria-selected='true'] {
  background: var(--accent);
  color: #0b0f1a;
}

.my-slot {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  text-align: center;
  align-items: center;
}

.my-slot-empty {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  align-items: center;
  width: 100%;
}

.my-slot-line {
  margin: 0;
  font-weight: 600;
}

.my-slot-blurb {
  margin: 0;
  max-width: 26rem;
  color: var(--muted, #8b94a8);
  font-size: var(--text-sm, 0.875rem);
  line-height: 1.5;
}

/* A slot with nothing to draw takes the room over rather than sitting under
   the run's own blocks. By attribute, beside data-recap-mode and
   data-run-view, because applyDailyRecapView already toggles `hidden` on these
   and a second hand doing the same would fight it.

   Written as "not the Best slot" rather than as a list of the two states that
   hide it: the one state that draws a run is the one worth naming. */
.recap-layout:not([data-my-slot='best']) .recap-score,
.recap-layout:not([data-my-slot='best']) .run-overview-group,
.recap-layout:not([data-my-slot='best']) .recap-actions,
.recap-layout:not([data-my-slot='best']) .recap-stamp,
.recap-layout:not([data-my-slot='best']) .chain-rail,
.recap-layout:not([data-my-slot='best']) .final-score {
  display: none;
}

/* --- The Shared slot's list of live links (D104) --------------------------- */

/* The whole slot is centred, so these three set their own width instead of
   stretching. 24rem is the actions row's, which they line up with. */
.my-shared-count,
.my-shared-list,
#my-shared-actions {
  width: 100%;
  max-width: 24rem;
}

.my-shared-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  text-align: left;
}

/* A whole row is the target, so it is one button rather than a box with a
   button in it. */
.my-shared-row {
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2);
  /* 44px of height is the smallest comfortable tap; the posters make it more
     than that on their own, and this holds if one is ever missing. */
  min-height: 44px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--text);
  text-align: left;
  cursor: pointer;
}

.my-shared-posters {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex: none;
}

/* The chain page's poster, at a third of the size. Same border, because these
   are the same two films. */
.my-shared-poster {
  width: 30px;
  height: 45px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid rgb(var(--accent-rgb) / 0.55);
}

.my-shared-arrow {
  color: var(--accent-2);
  line-height: 1;
}

/* The one part that may run out of room, so it is the one allowed to shrink.
   min-width: 0 is what lets the two lines below truncate instead of pushing
   the score off the row. */
.my-shared-text {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.my-shared-films,
.my-shared-meta {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.my-shared-films {
  font-size: var(--text-sm);
  font-weight: 600;
}

.my-shared-films .my-shared-arrow {
  margin-inline: 0.2em;
}

.my-shared-meta {
  font-size: var(--text-xs);
  color: var(--muted);
}

/* Which of these is today's. Only one row can carry it. Centred, so it and the
   chevron read as one group at the row's end rather than as a stagger. */
.my-shared-today {
  flex: none;
  padding: 3px 7px;
  border-radius: var(--radius-pill);
  background: var(--accent-2);
  color: var(--bg);
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.my-shared-chevron {
  flex: none;
  color: var(--muted);
  font-size: var(--text-lg);
  line-height: 1;
}

/* The platform marks, each as supplied. Sized and spaced, never recoloured or
   redrawn — the rule public/tmdb_logo.svg already carries, applied to five more
   trademarks. */
.share-mark {
  width: 22px;
  height: 22px;
  flex: none;
}

/* The app's own glyphs, drawn here rather than borrowed. They wear the accent
   because grey beside five coloured marks reads as a disabled row, and these
   are the app's actions rather than anyone's brand. */
.share-mark-own {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.share-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.share-tray-rule {
  margin: var(--space-1) 0;
  border: 0;
  border-top: 1px solid var(--border);
}

/* The tray's one line of copy, signed out only. Sentence case and not
   .recap-caption: that one is uppercase and tracked, which is right for a
   three-word label and shouty for a sentence somebody is meant to read. */
.share-tray-offer {
  margin: -0.35rem 0 0;
  font-size: var(--text-sm, 0.875rem);
  line-height: 1.5;
  color: var(--muted, #8b94a8);
}
