/* ============================================================================
   Story feed — "story squares" battle log, ONE turn per column.

   A single feed starts at the safe TOP-LEFT edge; a two-arrow bar pages
   through turns. Each column holds one turn's beats (both players', colour-
   coded per tile via --side-color) newest on top, with that turn's manga recap
   pinned above. Tiles are square card-art buttons sized from --card-unit so
   they scale with the battle from a 44px touch floor with no desktop ceiling.
   The companion --story-ui-unit preserves the established layout through
   1080p, then scales StoryFeed chrome alongside the cards on larger canvases.
   One global side
   popup (z 9550, above enlarged cards, below spell-announce 9600) carries the
   story text + card preview; the manga zoom browser (99999) opens from a tile.
   ========================================================================== */

:root {
  --story-ui-unit: max(1px, calc(var(--card-unit, 1vmin) / 10.8));
  --story-nav-control: max(
    26px,
    calc(
      3.48 * var(--card-unit, 1vmin)
      + calc(2 * var(--story-ui-unit))
    )
  );
  --story-tile: max(44px, calc(6 * var(--card-unit, 1vmin)));
  --story-tile-manga: max(52px, calc(7 * var(--card-unit, 1vmin)));
  /* Firefox and non-overlay scrollbar platforms consume scrollport width. The
     desktop lane reserves the same thin gutter on both sides: that keeps every
     square physically centred on the previous/next pair even when a scrollbar
     is present, without clipping a fixed-width manga tile. */
  --story-scrollbar-gutter: calc(8 * var(--story-ui-unit));
  --story-lane-scrollbar-space: calc(2 * var(--story-scrollbar-gutter));
  --story-lane-content-w: calc(
    var(--story-tile-manga)
    + calc(12 * var(--story-ui-unit))
    + var(--story-lane-scrollbar-space)
  );
  --story-lane-w: var(--story-lane-content-w);
  --story-nav-w: var(--story-lane-w);
  --story-lane-offset: 0px;
  --story-side-player: #eac761;
  --story-side-opponent: #a06fd6;
  /* Desktop keeps the historical clearance. Tablets use the explicit safe-edge
     command rail below because StoryFeed now occupies the left side only. */
  --end-turn-right: max(calc(var(--story-lane-w) + 2.2vmin), 9.26vmin);
}

@media (orientation: landscape)
  and (min-width: 960px)
  and (min-height: 561px)
  and (max-width: 1560px)
  and (max-aspect-ratio: 17/10) {
  :root {
    --end-turn-right: calc(
      env(safe-area-inset-right, 0px)
      + clamp(12px, 2vmin, 20px)
    );
  }
}

/* ---- feed: one left-edge column with an accessible per-turn arrow bar ---- */

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

#story-feed {
  position: fixed;
  top: calc(
    max(env(safe-area-inset-top, 0px), 0px)
    + calc(8 * var(--story-ui-unit))
  );
  transform: none;
  /* 65% of the inset, not 100%: asked for repeatedly ("move the storyboard
     squares slightly to the left") and never visibly delivered, because every
     fix parked the rail at the SAFE edge — which on a notched phone is 59pt
     of empty-looking glass. The island's physical pill only spans ~11-36pt
     from the edge in landscape, so 65% (≈38pt) hugs the screen visibly while
     staying clear of the glass on the rotation that puts the island here.
     Zero-inset phones are unchanged (0 x 0.65 = 0). */
  left: calc(env(safe-area-inset-left, 0px) * 0.65);
  /* Above persistent battle commands (9450), below the focus backdrop (9540).
     A growing phone story rail must cover Return to Lobby, never vice versa. */
  z-index: 9535;
  width: var(--story-nav-w);
  display: none; /* shown only during battle (body.game-active) */
  flex-direction: column;
  align-items: flex-start;
  gap: calc(6 * var(--story-ui-unit));
  pointer-events: none; /* the bar + column re-enable */
}

body.game-active #story-feed {
  display: flex;
}

/* nothing to show until the first beat lands */
#story-feed.is-empty {
  display: none;
}

/* `display: flex` beats the [hidden] attribute's UA `display: none`, so the
   bar needs this or story-feed.js hiding it does nothing at all. */
.story-feed-turnbar[hidden] {
  display: none;
}

.story-feed-turnbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0;
  width: var(--story-nav-w);
  pointer-events: auto;
}

.story-feed-turn-prev,
.story-feed-turn-next {
  width: var(--story-nav-control);
  height: var(--story-nav-control);
  min-width: var(--story-nav-control);
  padding: 0;
  margin: 0;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  border: calc(1 * var(--story-ui-unit)) solid rgba(238, 219, 241, 0.35);
  border-radius: 50%;
  background: rgba(10, 8, 18, 0.85);
  color: #eedbf1;
  font-size: calc(15 * var(--story-ui-unit));
  line-height: 1;
  cursor: pointer;
  transition: transform 120ms ease, opacity 120ms ease;
}

.story-feed-turn-prev:hover:not(:disabled),
.story-feed-turn-next:hover:not(:disabled),
.story-feed-turn-prev:focus-visible,
.story-feed-turn-next:focus-visible {
  transform: scale(1.12);
  outline: none;
}

.story-feed-turn-prev:disabled,
.story-feed-turn-next:disabled {
  opacity: 0.3;
  cursor: default;
}

.story-feed-column {
  overflow-y: auto;
  overscroll-behavior: contain;
  touch-action: pan-y;
  -webkit-overflow-scrolling: touch;
  display: flex;
  flex-direction: column;
  /* Desktop gives the squares a symbolic inset and centres them on the
     previous/next control pair. The phone tier below deliberately switches
     back to the hard-left alignment its safe-edge rail needs. Safari before
     18.2 ignores both-edges; story-feed.js measures its one-sided classic
     scrollbar and mirrors that width into the opposite logical padding. */
  --story-scrollbar-balance-start: 0px;
  --story-scrollbar-balance-end: 0px;
  align-items: center;
  gap: calc(8 * var(--story-ui-unit));
  padding: calc(6 * var(--story-ui-unit));
  padding-inline-start: calc(
    calc(6 * var(--story-ui-unit))
    + var(--story-scrollbar-balance-start)
  );
  padding-inline-end: calc(
    calc(6 * var(--story-ui-unit))
    + var(--story-scrollbar-balance-end)
  );
  width: var(--story-lane-w);
  margin-inline-start: var(--story-lane-offset);
  max-height: calc(
    100dvh
    - max(env(safe-area-inset-top, 0px), calc(8 * var(--story-ui-unit)))
    - max(env(safe-area-inset-bottom, 0px), calc(8 * var(--story-ui-unit)))
    - calc(44 * var(--story-ui-unit))
  );
  box-sizing: border-box;
  pointer-events: auto;
  scrollbar-width: thin;
  scrollbar-gutter: stable both-edges;
  scrollbar-color: rgba(238, 219, 241, 0.4) transparent;
}

/* "Is that the whole story?" — the lane has always scrolled, but a 56px-wide
   column with an overlay scrollbar gives a phone player no way to know. Fade
   whichever edge has beats beyond it. story-feed.js sets the attribute. */
.story-feed-column[data-overflow="top"] {
  mask-image: linear-gradient(to bottom, transparent 0, #000 calc(10 * var(--story-ui-unit)), #000 100%);
}

.story-feed-column[data-overflow="bottom"] {
  mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 10 * var(--story-ui-unit)), transparent 100%);
}

.story-feed-column[data-overflow="both"] {
  mask-image: linear-gradient(
    to bottom,
    transparent 0,
    #000 calc(10 * var(--story-ui-unit)),
    #000 calc(100% - 10 * var(--story-ui-unit)),
    transparent 100%
  );
}

.story-feed-column::-webkit-scrollbar {
  width: calc(4 * var(--story-ui-unit));
}

.story-feed-column::-webkit-scrollbar-thumb {
  border-radius: 999px;
  background: rgba(238, 219, 241, 0.4);
}

/* ---- tiles ----------------------------------------------------------------- */

.story-tile {
  width: var(--story-tile);
  height: var(--story-tile);
  min-width: var(--story-tile);
  flex: 0 0 auto;
  padding: 0;
  margin: max(4px, 0.58vmin) 0;
  box-sizing: border-box;
  border: calc(2 * var(--story-ui-unit)) solid var(--side-color, rgba(238, 219, 241, 0.4));
  border-radius: calc(10 * var(--story-ui-unit));
  background: rgba(10, 8, 18, 0.85);
  box-shadow:
    0 0 calc(10 * var(--story-ui-unit)) calc(-2 * var(--story-ui-unit))
    var(--side-color, rgba(123, 104, 238, 0.4));
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: transform 120ms ease, box-shadow 120ms ease;
}

.story-feed-more {
  width: var(--story-tile-manga);
  min-height: max(44px, calc(44 * var(--story-ui-unit)));
  flex: 0 0 auto;
  padding: calc(6 * var(--story-ui-unit));
  margin: max(4px, 0.58vmin) 0;
  border: calc(1 * var(--story-ui-unit)) solid rgba(238, 219, 241, 0.32);
  border-radius: calc(10 * var(--story-ui-unit));
  background: linear-gradient(180deg, rgba(37, 27, 57, 0.94), rgba(14, 10, 25, 0.94));
  color: #e8dff2;
  font: 600 calc(11 * var(--story-ui-unit))/1.2 'Spectral', Georgia, serif;
  cursor: pointer;
}

.story-feed-more:hover,
.story-feed-more:focus-visible {
  border-color: rgba(234, 199, 97, 0.8);
  color: #fff4c9;
  outline: calc(2 * var(--story-ui-unit)) solid rgba(234, 199, 97, 0.38);
  outline-offset: calc(2 * var(--story-ui-unit));
}

.story-tile:hover,
.story-tile:focus-visible {
  transform: scale(1.08);
  box-shadow:
    0 0 calc(16 * var(--story-ui-unit)) 0
    var(--side-color, rgba(123, 104, 238, 0.6));
  outline: none;
}

/* Stake beats — deckout strikes and turn-timeout strikes read the same because
   they are the same thing to the player: a countdown to losing the match.

   AND THAT IS THE SAME SIGNAL THE COUNTDOWNS CARRY, so the amber is the shared
   urgency token rather than a third hand-picked one. It was `#ffe8bd` with a
   `rgba(255, 210, 128, 0.78)` edge, against the turn timer's `#ffe4b5` and
   `rgba(255, 200, 120, 0.6)` — near-identical colours, chosen separately, for
   one meaning. The owner asked for these to be "more unifromized in style/look"
   alongside the two countdowns.

   The GROUND stays local on purpose: a stake beat is a feed tile in a scrolling
   column and a countdown is a pill on the command rail. They share the signal,
   not the object. */
.story-tile--deckout,
.story-tile--timeout {
  --side-color: var(--mf-timer-urgent-border);
  background: linear-gradient(135deg, rgba(104, 52, 28, 0.82), rgba(10, 8, 18, 0.92));
  color: var(--mf-timer-urgent-ink);
}

@media (forced-colors: active) {
  .story-tile--deckout,
  .story-tile--timeout {
    --side-color: currentColor;
    background: Canvas;
    color: CanvasText;
  }
}

.story-tile-art {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.story-tile-glyph {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  font-family: 'Cinzel', serif;
  font-weight: 700;
  font-size: calc(var(--story-tile) * 0.42);
  color: #cdbce0;
}

.story-tile.is-pending .story-tile-art,
.story-tile.is-pending .story-tile-glyph {
  opacity: 0.35;
}

.story-tile.is-pending:not(.story-tile--manga)::after,
.story-tile--manga.is-pending .story-tile-manga-frame::after {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  width: calc(18 * var(--story-ui-unit));
  height: calc(18 * var(--story-ui-unit));
  border-radius: 50%;
  border: calc(2 * var(--story-ui-unit)) solid rgba(238, 219, 241, 0.25);
  border-top-color: #eedbf1;
  animation: story-tile-spin 0.9s linear infinite;
}

.story-tile--manga.is-pending .story-tile-manga-frame::after {
  top: calc(5 * var(--story-ui-unit));
  bottom: auto;
  margin: 0 auto;
}

.story-tile-pending-label {
  position: absolute;
  inset-inline: calc(3 * var(--story-ui-unit));
  bottom: calc(3 * var(--story-ui-unit));
  z-index: 1;
  overflow: hidden;
  color: #f4ecff;
  font-family: 'Spectral', Georgia, serif;
  font-size: calc(11 * var(--story-ui-unit));
  line-height: 1.1;
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@keyframes story-tile-spin {
  to { transform: rotate(360deg); }
}

.story-tile.is-failed {
  border-color: rgba(224, 108, 117, 0.7);
  filter: grayscale(0.6);
}

/* Manga tiles size to the PANEL'S OWN aspect (width fixed, height follows the
   image) so the recap panel shows in full — no square crop ("cut off"), no
   letterbox bars, whatever the generated aspect. A min-height floors the
   pending/failed states (spinner / glyph) that have no image yet. */
.story-tile--manga {
  width: var(--story-tile-manga);
  height: auto;
  min-width: var(--story-tile-manga);
  min-height: max(44px, calc(44 * var(--story-ui-unit)));
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: transparent;
  box-shadow: none;
  overflow: visible;
}

.story-tile-manga-frame {
  position: relative;
  width: 100%;
  min-height: calc(var(--story-tile-manga) * 0.62);
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  border: calc(2 * var(--story-ui-unit)) solid rgba(238, 219, 241, 0.65);
  border-radius: calc(10 * var(--story-ui-unit));
  background: rgba(10, 8, 18, 0.85);
  box-shadow:
    0 0 calc(10 * var(--story-ui-unit)) calc(-2 * var(--story-ui-unit))
    rgba(123, 104, 238, 0.4);
  overflow: hidden;
}

.story-tile--manga.is-pending .story-tile-manga-frame {
  min-height: max(44px, calc(44 * var(--story-ui-unit)));
}

.story-tile--manga:hover,
.story-tile--manga:focus-visible {
  box-shadow: none;
}

.story-tile--manga:hover .story-tile-manga-frame,
.story-tile--manga:focus-visible .story-tile-manga-frame {
  box-shadow: 0 0 calc(16 * var(--story-ui-unit)) 0 rgba(123, 104, 238, 0.6);
}

.story-tile--manga.is-failed .story-tile-manga-frame {
  border-color: rgba(224, 108, 117, 0.7);
}

.story-tile--manga .story-tile-art {
  height: auto;
  object-fit: fill; /* the img is sized to its natural aspect, so no distortion */
}

/* Live pending→complete celebration (ported from the rail's reveal moment). */
.story-tile--reveal {
  animation: story-tile-reveal 0.9s cubic-bezier(0.33, 1, 0.68, 1);
}

@keyframes story-tile-reveal {
  0% { transform: scale(0.7); box-shadow: 0 0 0 0 rgba(238, 219, 241, 0.0); }
  45% {
    transform: scale(1.18);
    box-shadow:
      0 0 calc(26 * var(--story-ui-unit)) calc(4 * var(--story-ui-unit))
      rgba(238, 219, 241, 0.55);
  }
  100% { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .story-tile--reveal {
    animation: none;
  }
}

/* ---- side popup (toast + inspect) ------------------------------------------ */

#story-feed-popup {
  position: fixed;
  z-index: 9550;
  /* The seat this panel WANTS: just outboard of the story lane. */
  --story-popup-left: calc(
    var(--story-lane-offset)
    + var(--story-lane-w)
    + env(safe-area-inset-left, 0px)
    + calc(8 * var(--story-ui-unit))
  );
  /* NEVER OVER THE BATTLEFIELD — AND THE FLOOR IS A MOVE, NOT A WIDTH.
     ------------------------------------------------------------------------
     This panel is vertically centred (`top: 50%` + `translateY(-50%)`) with a
     near-solid background, and `--story-popup-left` above is the DESKTOP
     side-lane formula. Wherever the five-slot row is narrow enough to start
     left of the panel's right edge, every storyteller beat paints this over
     the leftmost cards of BOTH rows for the beat's dwell and then removes it.
     A summon always produces a beat, for either player, which is the reported
     defect verbatim: "if i have summoned cards, and i summon another, their
     card description ... dissapears and then comes back in a second. same
     happens for enemy if they summon."

     Two things about the first attempt at this guard are worth keeping
     written down, because both are easy to write again:

     IT WAS SCOPED TO A BREAKPOINT THAT WAS NOT THE BOARD'S. It lived inside
     `(orientation: landscape) and (max-height: 520px)`; the centred phone
     board is `(max-height: 560px) and (max-width: 932px)`; and the row is
     narrow enough to be reached well outside both. Measured with that guard
     in place: 1024x768 -85px, 900x561 -110px, 900x560 -172px, 900x540 -166px,
     932x540 -150px, 933x540 -149px. The bound is now unconditional and reads
     the board's OWN published edge (`--board-row-left`, game-board.css), so
     there is no second breakpoint left to disagree with.

     ITS `max()` FLOOR FAILED OPEN. `max(96 * --story-ui-unit, …)` is a flat
     96px on a phone — wider than the free strip at several page boxes — so the
     floor put the panel back ON the board (600x280 -10px, 568x260 -20px,
     480x260 -64px). A width floor that overlaps is not a floor. The floor is
     now a RELOCATION: the panel slides left, over its own story lane, until it
     either reaches the comfort width or hits the safe edge. Clearance is
     therefore structural — `max-width` is exactly the gap between the seat and
     the board — and the width the slide buys back is what keeps a 2-3s toast
     from needing a scrollbar (TOAST_MS / MAX_STORY_REVEAL_MS,
     public/scripts/ui/story-feed.js). There is deliberately no `max()` on the
     width: a floor that cannot be honoured must move the panel, not overlap.
     Contract: tests/e2e-playwright/story-popup-board-overlap.spec.js. */
  --story-popup-board-gap: 8px;
  --story-popup-board-clear: calc(
    var(--board-row-left) - var(--story-popup-board-gap)
  );
  /* The narrowest width at which a beat still reads. It is the panel's own
     declared minimum, not a second number: `width` below is built from this
     token, so the width the slide targets and the width the panel wants
     cannot drift apart. */
  --story-popup-comfort: 210px;
  --story-popup-safe-left: max(env(safe-area-inset-left, 0px), 6px);
  --story-popup-seat: clamp(
    var(--story-popup-safe-left),
    calc(var(--story-popup-board-clear) - var(--story-popup-comfort)),
    var(--story-popup-left)
  );
  left: var(--story-popup-seat);
  max-width: calc(var(--story-popup-board-clear) - var(--story-popup-seat));
  top: 50%;
  transform: translateY(-50%);
  width: max(var(--story-popup-comfort), 32vmin);
  max-height: calc(
    100dvh
    - max(env(safe-area-inset-top, 0px), calc(8 * var(--story-ui-unit)))
    - max(env(safe-area-inset-bottom, 0px), calc(8 * var(--story-ui-unit)))
  );
  overflow-y: auto;
  overscroll-behavior: contain;
  padding:
    calc(13 * var(--story-ui-unit))
    calc(17 * var(--story-ui-unit));
  border-radius: calc(12 * var(--story-ui-unit));
  border: calc(1 * var(--story-ui-unit)) solid rgba(238, 219, 241, 0.16);
  border-left:
    calc(3 * var(--story-ui-unit)) solid
    var(--side-color, rgba(200, 180, 255, 0.75));
  /* a refined magical panel, not a flat black box */
  background: linear-gradient(135deg, rgba(40, 30, 62, 0.92), rgba(19, 14, 33, 0.94));
  box-shadow:
    0 calc(14 * var(--story-ui-unit)) calc(44 * var(--story-ui-unit))
      calc(-14 * var(--story-ui-unit)) rgba(0, 0, 0, 0.6),
    0 0 calc(22 * var(--story-ui-unit)) calc(-10 * var(--story-ui-unit))
      var(--side-color, rgba(160, 111, 214, 0.5)),
    inset 0 calc(1 * var(--story-ui-unit)) 0 rgba(255, 255, 255, 0.05);
  color: #f4ecff;
  scrollbar-width: thin;
  scrollbar-color: rgba(238, 219, 241, 0.52) rgba(10, 8, 18, 0.24);
  scrollbar-gutter: stable;
  transition: opacity 200ms cubic-bezier(0.33, 1, 0.68, 1), transform 200ms cubic-bezier(0.33, 1, 0.68, 1);
}

#story-feed-popup::-webkit-scrollbar {
  width: calc(5 * var(--story-ui-unit));
}

#story-feed-popup::-webkit-scrollbar-thumb {
  border-radius: 999px;
  background: rgba(238, 219, 241, 0.52);
}

#story-feed-popup[hidden] {
  display: none;
}

#story-feed-popup[data-side='player'] {
  --side-color: var(--story-side-player);
}

#story-feed-popup[data-side='opponent'] {
  --side-color: var(--story-side-opponent);
}

.story-feed-popup-text {
  font-family: 'Spectral', Georgia, serif;
  font-size: calc(15 * var(--story-ui-unit));
  line-height: 1.62;
  color: #f4ecff;
  overflow-wrap: anywhere;
  hyphens: auto;
  -webkit-user-select: text;
  user-select: text;
}

/* The full relationship viewer and manga reveal share one focus surface.
   Automatic card narration is prose-only; deliberate click/hover inspection
   receives the stronger manual gradient below, while dismissal remains an
   independent interaction policy. */
#story-feed-focus-backdrop {
  position: fixed;
  inset: 0;
  z-index: 9540;
  pointer-events: auto;
  /* NOTHING BY DEFAULT — no dim, no blur. On a phone `backdrop-filter` is
     already banned below (it forces a per-frame repaint of the live board), so
     the treatment iOS actually showed on a self-expand was this GRADIENT, and
     it is what the report describes. The popup carries a near-solid background
     of its own, so the prose stays perfectly legible without it. */
  background: transparent;
  /* A manga panel that expands BY ITSELF is not something the player asked to
     inspect, so pulling the board out of focus behind it reads as the game
     seizing attention: "self expand manga blurs board around it. shouldn't
     happen. only on manual mouse hover it should highlight, but when self
     expands its distracting to do it."
     Both the dim and the blur now belong to the manual branch only. */
}

#story-feed-focus-backdrop[data-dimmed='false'] {
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* DELIBERATE inspection — a click or a hover the player performed. This is the
   only state that takes the board out of focus. */
#story-feed-focus-backdrop[data-manual='true'] {
  background: radial-gradient(
    ellipse at 58% 50%,
    rgba(8, 6, 24, 0.16) 0%,
    rgba(8, 6, 24, 0.54) 52%,
    rgba(5, 4, 14, 0.8) 100%
  );
  -webkit-backdrop-filter: blur(calc(2 * var(--story-ui-unit)));
  backdrop-filter: blur(calc(2 * var(--story-ui-unit)));
}

#story-feed-focus-backdrop.is-manual,
#story-feed-focus-backdrop[data-dismissible='false'] {
  pointer-events: none;
}

#story-feed-focus-backdrop[hidden] {
  display: none;
}

#story-feed-action-stage {
  position: fixed;
  /* Above tutorial tips, Skip, audio, and the in-game Lobby control. */
  z-index: 12000;
  left: 58%;
  top: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(
    calc(10 * var(--story-ui-unit)),
    2vmin,
    calc(24 * var(--story-ui-unit))
  );
  pointer-events: none;
  /* Capture the ambient unit for the enlarged faces below to multiply. A custom
     property cannot reference itself, so the capture lives one level up on the
     stage — which also keeps the :root phone floor and the 4K counter-scale
     reaching these cards instead of being re-derived per surface. */
  --story-card-base-unit: var(--card-unit);
}

#story-feed-action-stage[hidden] {
  display: none;
}

/* The enlargement scales --card-unit, never --card-width alone. Every internal
   of a card — title, description, cost, and the status overlay in particular —
   is derived from the unit, so re-declaring only the frame draws a 2x border
   around 1x contents. That is what made the storyboard's card status
   unreadable: the card was large while its status text stayed at board scale. */
.story-feed-action-card {
  --card-unit: calc(var(--story-card-base-unit, 1vmin) * 2);
  --card-width: calc(12.29 * var(--card-unit));
  --card-height: calc(22.2 * var(--card-unit));
  position: relative;
  flex: 0 0 auto;
  pointer-events: none;
}

/* Relationship cards are a read-only diagram. They deliberately reuse the
   enemy-spell reveal dimensions, but not that surface's pointer-driven 1.6x
   inspection zoom: a diagram that materializes beneath a resting cursor must
   not grow over its arrow or make source and target look unrelated. */
.story-feed-action-card,
.story-feed-action-card * {
  pointer-events: none;
}

/* ---- status rail beside the relationship cards -----------------------------
   On the board a card's status is a strip across its own bottom edge, which is
   the right call when a status must stay welded to a card among eleven others.
   Here there are exactly two cards and the whole point of the stage is reading
   what happened, so the status becomes a legible rail on the card's OUTER side:
   the source's to its left, the target's to its right, leaving the arrow
   between them clear. Chips match the summoner rail so a status is one
   recognisable object wherever it appears. */
.story-feed-action-card > .card {
  /* The rail lives outside the card box, which normally clips its children. */
  overflow: visible;
}

.story-feed-action-card .card-status {
  --story-status-gap: calc(0.6 * var(--card-unit));
  top: 0;
  bottom: auto;
  left: auto;
  right: auto;
  width: max-content;
  max-width: calc(9 * var(--card-unit));
  height: auto;
  /* Chip rail beside the card, not a plate on it — the card-height ceiling
     cards.css puts on the resting plate would only truncate this. */
  max-height: none;
  padding: 0;
  background: none;
  border-radius: 0;
  overflow: visible;
  text-align: left;
  font-size: calc(0.92 * var(--card-unit));
  line-height: 1.18;
  gap: calc(0.34 * var(--card-unit));
  align-items: flex-start;
  /* Settles in a beat after the card so the eye reads card, then state. */
  animation: story-status-rail-in 0.34s cubic-bezier(0.22, 1, 0.36, 1) 0.12s backwards;
}

.story-feed-action-source .card-status {
  right: calc(100% + var(--story-status-gap));
  align-items: flex-end;
  text-align: right;
}

.story-feed-action-target .card-status {
  left: calc(100% + var(--story-status-gap));
}

/* The rail shows the WHOLE ledger: it is a chip column beside a large preview
   card, not the 37px board card the `+N` fold exists for. */
.story-feed-action-card .card-status .card-status-overflow {
  display: block;
}

.story-feed-action-card .card-status .card-status-more {
  display: none;
}

.story-feed-action-card .card-status span {
  display: block;
  box-sizing: border-box;
  max-width: 100%;
  padding: calc(0.18 * var(--card-unit)) calc(0.42 * var(--card-unit));
  border-left: calc(0.14 * var(--card-unit)) solid currentColor;
  border-radius: calc(0.3 * var(--card-unit));
  background: rgba(14, 9, 22, 0.94);
  overflow-wrap: break-word;
  hyphens: auto;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.98);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.08),
    0 2px 6px rgba(0, 0, 0, 0.62);
}

.story-feed-action-source .card-status span {
  border-left: none;
  border-right: calc(0.14 * var(--card-unit)) solid currentColor;
}

@keyframes story-status-rail-in {
  from {
    opacity: 0;
    transform: translateY(calc(-0.4 * var(--card-unit)));
  }

  to {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .story-feed-action-card .card-status {
    animation: none;
  }
}

.story-feed-action-card .spell-announce-cardface {
  --card-hover-transform: scale(1);
}

.story-feed-action-card .spell-announce-cardface:hover {
  --card-hover-transform: scale(1);
}

.story-feed-action-arrow {
  color: #ffe9a9;
  font-size: clamp(
    calc(28 * var(--story-ui-unit)),
    5vmin,
    calc(58 * var(--story-ui-unit))
  );
  line-height: 1;
  text-shadow:
    0 0 calc(16 * var(--story-ui-unit)) rgba(234, 199, 97, 0.75),
    0 calc(3 * var(--story-ui-unit)) calc(8 * var(--story-ui-unit))
      rgba(0, 0, 0, 0.9);
}

.story-feed-player-target,
.story-feed-hidden-target,
.story-feed-zone-target {
  --target-color: rgba(234, 199, 97, 0.78);
  --target-ink: #fff4c9;
  position: relative;
  box-sizing: border-box;
  border: calc(2 * var(--story-ui-unit)) solid var(--target-color);
  border-radius: calc(14 * var(--story-ui-unit));
  display: flex;
  flex-direction: column;
  flex: 0 0 auto;
  align-items: center;
  overflow: hidden;
  background: linear-gradient(150deg, rgba(72, 54, 102, 0.96), rgba(14, 10, 26, 0.98));
  box-shadow:
    0 1.2vmin 3vmin rgba(0, 0, 0, 0.75),
    0 0 2.4vmin color-mix(in srgb, var(--target-color) 42%, transparent);
}

#story-feed-action-stage [data-target-side='player'] {
  --target-color: var(--story-side-player);
  --target-ink: #fff4c9;
}

#story-feed-action-stage [data-target-side='opponent'] {
  --target-color: var(--story-side-opponent);
  --target-ink: #f2e3ff;
}

.story-feed-action-target[data-target-side] .card {
  outline:
    calc(2 * var(--story-ui-unit)) solid
    color-mix(in srgb, var(--target-color) 76%, transparent);
  outline-offset: calc(2 * var(--story-ui-unit));
  box-shadow:
    0 1.2vmin 3vmin rgba(0, 0, 0, 0.75),
    0 0 2.4vmin color-mix(in srgb, var(--target-color) 54%, transparent);
}

.story-feed-player-target {
  /* Resolve large dimensions directly from --card-unit. Firefox quantizes a
     small inherited length before multiplying it, which otherwise turns a
     subpixel chrome rounding into a visible multi-pixel portrait mismatch.
     The 10.8px base retains --story-ui-unit's 1px floor on smaller screens. */
  width: clamp(
    calc(16.6666666667 * max(10.8px, var(--card-unit, 1vmin))),
    36vmin,
    calc(35.1851851852 * max(10.8px, var(--card-unit, 1vmin)))
  );
  height: auto;
  min-width: calc(16.6666666667 * max(10.8px, var(--card-unit, 1vmin)));
  min-height: calc(9.3518518519 * max(10.8px, var(--card-unit, 1vmin)));
  aspect-ratio: 16 / 9;
  justify-content: flex-end;
  background-position: center 24%;
  background-size: cover;
}

.story-feed-hidden-target,
.story-feed-zone-target {
  width: 24.6vmin;
  height: 44.4vmin;
  min-width: calc(88 * var(--story-ui-unit));
  min-height: calc(158 * var(--story-ui-unit));
  justify-content: center;
  gap: calc(12 * var(--story-ui-unit));
  padding:
    calc(18 * var(--story-ui-unit))
    calc(12 * var(--story-ui-unit))
    calc(12 * var(--story-ui-unit));
  overflow: visible;
}

.story-feed-player-target > span {
  width: 100%;
  padding:
    calc(9 * var(--story-ui-unit))
    calc(6 * var(--story-ui-unit));
  box-sizing: border-box;
  background: rgba(8, 6, 18, 0.88);
  color: var(--target-ink);
  font:
    700 clamp(
      calc(12 * var(--story-ui-unit)),
      1.8vmin,
      calc(18 * var(--story-ui-unit))
    )/1.2 'Cinzel', serif;
  text-align: center;
}

.story-feed-target-badge {
  position: absolute;
  z-index: 5;
  top: calc(-11 * var(--story-ui-unit));
  left: 50%;
  width: max-content;
  max-width: calc(100% - calc(8 * var(--story-ui-unit)));
  min-height: calc(24 * var(--story-ui-unit));
  padding:
    calc(5 * var(--story-ui-unit))
    calc(9 * var(--story-ui-unit));
  box-sizing: border-box;
  transform: translateX(-50%);
  overflow: visible;
  border:
    calc(1 * var(--story-ui-unit)) solid
    color-mix(in srgb, var(--target-color) 78%, white 12%);
  border-radius: 999px;
  background: rgba(10, 7, 22, 0.96);
  box-shadow:
    0 calc(5 * var(--story-ui-unit)) calc(14 * var(--story-ui-unit))
      rgba(0, 0, 0, 0.58),
    0 0 calc(12 * var(--story-ui-unit)) var(--target-color);
  color: var(--target-ink);
  font:
    700 clamp(
      calc(10 * var(--story-ui-unit)),
      1.25vmin,
      calc(13 * var(--story-ui-unit))
    )/1.15 'Cinzel', serif;
  letter-spacing: 0.04em;
  text-align: center;
  text-transform: uppercase;
  overflow-wrap: normal;
  white-space: normal;
}

.story-feed-hidden-target > .story-feed-target-badge,
.story-feed-zone-target > .story-feed-target-badge {
  top: calc(8 * var(--story-ui-unit));
}

.story-feed-hidden-target .card-back,
.story-feed-zone-target .card-back {
  position: relative;
  width: 74%;
  aspect-ratio: 0.56;
  flex: 0 1 auto;
  border:
    calc(2 * var(--story-ui-unit)) solid
    color-mix(in srgb, var(--target-color) 60%, white 22%);
  border-radius: calc(9 * var(--story-ui-unit));
  background-image: var(
    --cardback-image,
    radial-gradient(circle at 50% 44%, rgba(234, 199, 97, 0.42), transparent 18%),
    linear-gradient(145deg, #3d235f, #171023 64%, #5c3b72)
  );
  background-position: center;
  background-size: cover;
  box-shadow:
    inset 0 0 0 calc(4 * var(--story-ui-unit)) rgba(8, 6, 18, 0.55),
    0 calc(8 * var(--story-ui-unit)) calc(22 * var(--story-ui-unit))
      rgba(0, 0, 0, 0.5);
}

.story-feed-hidden-target[data-target-zone='hand'] .card-back {
  transform: rotate(-3deg);
  box-shadow:
    calc(-7 * var(--story-ui-unit)) calc(3 * var(--story-ui-unit)) 0
      calc(-3 * var(--story-ui-unit))
      color-mix(in srgb, var(--target-color) 38%, #171023),
    calc(7 * var(--story-ui-unit)) calc(3 * var(--story-ui-unit)) 0
      calc(-3 * var(--story-ui-unit))
      color-mix(in srgb, var(--target-color) 38%, #171023),
    inset 0 0 0 calc(4 * var(--story-ui-unit)) rgba(8, 6, 18, 0.55),
    0 calc(8 * var(--story-ui-unit)) calc(22 * var(--story-ui-unit))
      rgba(0, 0, 0, 0.5);
}

.story-feed-zone-target[data-target-zone='deck'] .card-back {
  box-shadow:
    calc(4 * var(--story-ui-unit)) calc(4 * var(--story-ui-unit)) 0
      color-mix(in srgb, var(--target-color) 28%, #171023),
    calc(8 * var(--story-ui-unit)) calc(8 * var(--story-ui-unit)) 0
      color-mix(in srgb, var(--target-color) 18%, #0b0714),
    inset 0 0 0 calc(4 * var(--story-ui-unit)) rgba(8, 6, 18, 0.55),
    0 calc(10 * var(--story-ui-unit)) calc(24 * var(--story-ui-unit))
      rgba(0, 0, 0, 0.58);
}

/* ---- coarse pointers (touch): no backdrop blur over the live board ---------- */
/* backdrop-filter over the animated battle board forces a per-frame repaint of
   everything behind it — the biggest GPU cost on phones. The popup already uses
   a near-solid background, so blur stays banned here even if a future restyle
   is tempted to add it (contract: coarseBackdropSwap.test.js). */

@media (pointer: coarse) {
  #story-feed-popup,
  #story-feed-focus-backdrop {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* ---- phone landscape -------------------------------------------------------- */

/* The explicit navigation box reproduces the historical rendered control size
   without inheriting global button padding. Touch devices and phone-sized
   DevTools emulation retain an honest 44px floor. The arrows span the narrow
   tile lane, while an extra safe-edge inset keeps the full target on-screen. */
@media (pointer: coarse), (orientation: landscape) and (max-height: 560px) {
  :root {
    --story-nav-w: var(--story-lane-w);
    --story-lane-offset: 0px;
  }

  #story-feed {
    left: calc(
      env(safe-area-inset-left, 0px)
      + calc(8 * var(--story-ui-unit))
    );
  }

  .story-feed-turnbar {
    overflow: visible;
  }

  .story-feed-turn-prev,
  .story-feed-turn-next {
    width: max(44px, var(--story-nav-control));
    height: max(44px, var(--story-nav-control));
    min-width: max(44px, var(--story-nav-control));
    font-size: max(20px, calc(15 * var(--story-ui-unit)));
  }
}

/* A landscape tablet/touch laptop gets 44px controls but is not the compact
   phone rail. Its lane must therefore fit both arrows before `space-between`
   can centre their pair on the symmetrically inset squares. */
@media (pointer: coarse)
  and (orientation: landscape)
  and (min-height: 561px) {
  :root {
    --story-lane-w: max(
      var(--story-lane-content-w),
      calc(2 * max(44px, var(--story-nav-control)))
    );
  }
}

/* A short desktop receives the same 44px navigation controls as a phone.
   Its content-width lane can be narrower than the two non-shrinking arrows,
   shifting their pair away from the inset square. Keep the compact phone
   concession separate and widen only the fine-pointer lane here. */
@media (pointer: fine)
  and (orientation: landscape)
  and (max-height: 560px) {
  :root {
    --story-lane-w: max(
      var(--story-lane-content-w),
      calc(2 * max(44px, var(--story-nav-control)))
    );
  }
}

/* The hard-left lane is a compact phone concession, not a generic touch
   layout. Coarse tablets and touch laptops have room for the same symmetric
   inset and arrow-pair centering as desktop. */
@media (hover: none)
  and (pointer: coarse)
  and (orientation: landscape)
  and (max-height: 560px) {
  :root {
    /* Compact phones keep the one-sided scrollbar budget: their tiles start
       at the safe edge rather than centring on the navigation pair. */
    --story-lane-scrollbar-space: var(--story-scrollbar-gutter);
  }

  .story-feed-column {
    align-items: flex-start;
    padding-inline-start: 0;
    scrollbar-gutter: stable;
  }
}

/* No phone override for the feed's own box: the decks no longer live in this
   rail (indicators.css moves them to desktop's right-side inset), so the
   narration starts at the top like every other canvas and the base column cap
   — which already subtracts both safe insets and the nav footprint — is
   correct. A previous version reserved a deck footprint above AND below,
   which on an 844x288 page box left the column
     288 - 21 - 100 - 100 - 66 = 1px
   of lane: the feed rendered with nowhere to render into. */

/* The compact relationship stage follows viewport geometry, not pointer
   reporting. Desktop browser responsive mode and hybrid laptops often retain a
   fine pointer while presenting the exact same short landscape canvas. */
@media (orientation: landscape) and (max-height: 520px) {
  #story-feed-popup {
    /* The board-clearance seat and `max-width` are unconditional now (they
       live on the base rule above and read `--board-row-left`), because the
       overlap was never confined to this breakpoint. All that is left here is
       the short-landscape TYPOGRAPHY: a tighter box and smaller padding. */
    width: min(42vw, 300px);
    padding: 12px;
  }

  #story-feed-popup[data-inspecting-card] {
    width: min(31vw, 220px);
  }

  #story-feed-action-stage {
    left: auto;
    right: max(env(safe-area-inset-right, 0px), 6px);
    transform: translateY(-50%);
  }

  .story-feed-action-card,
  .story-feed-hidden-target,
  .story-feed-zone-target {
    /* The 160x288 floor is folded into the unit rather than clamped on the box,
       so a phone that hits the floor scales the card's contents with it. */
    --card-unit: max(calc(var(--story-card-base-unit, 1vmin) * 3.25), 12.97px);
    --card-width: calc(12.29 * var(--card-unit));
    --card-height: calc(22.2 * var(--card-unit));
    width: var(--card-width);
    height: var(--card-height);
  }

  .story-feed-player-target {
    width: clamp(152px, 38vmin, 260px);
    min-width: 152px;
    min-height: 86px;
    height: auto;
  }

  /* The stage hugs the right edge here, so the target's outer side is off the
     page. Both rails move inboard-left, where the whole width is available. */
  .story-feed-action-target .card-status {
    left: auto;
    right: calc(100% + var(--story-status-gap));
    align-items: flex-end;
    text-align: right;
  }

  .story-feed-action-target .card-status span {
    border-left: none;
    border-right: calc(0.14 * var(--card-unit)) solid currentColor;
  }

  .story-feed-popup-text {
    font-size: 14px;
  }
}

@media (orientation: landscape) and (max-height: 520px) and (max-width: 700px) {
  #story-feed-popup[data-inspecting-card] {
    width: min(24vw, 180px);
  }

  #story-feed-action-stage {
    gap: 6px;
  }

  .story-feed-action-arrow {
    font-size: 24px;
  }
}

/* ---- reduced motion ---------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .story-tile,
  .story-feed-turn-prev,
  .story-feed-turn-next,
  #story-feed-popup {
    transition: opacity 100ms linear;
  }

  .story-tile:hover,
  .story-tile:focus-visible,
  .story-feed-turn-prev:hover,
  .story-feed-turn-next:hover {
    transform: none;
  }

  .story-tile.is-pending::after,
  .story-tile--manga.is-pending .story-tile-manga-frame::after {
    animation: none;
  }
}

/* ---- shared character-name highlight (used by the popup) -------------------- */

.story-highlight {
  font-weight: bold;
  color: #ffdf5c;
}

/* ---- manga auto-reveal side panel ----------------------------------------
   A generated panel expands beside the left feed for a few seconds (readable,
   Storyteller-sized), overlapping the board but never the feed; activation
   promotes that illustration into the fullscreen viewer. Later chronology
   remains queued until the viewer closes; the durable tile opens it too.
   Story beats and completed manga share one FIFO reveal queue, so bursts
   remain sequential and never stack over one another. */
#story-feed-manga-panel {
  position: fixed;
  left: calc(
    var(--story-lane-offset)
    + var(--story-lane-w)
    + env(safe-area-inset-left, 0px)
    + calc(14 * var(--story-ui-unit))
  );
  top: 50%;
  transform: translateY(-50%);
  z-index: 9560; /* above the story popup (9550), below spell-announce (9600) */
  display: flex;
  flex-direction: column;
  /* BIG ENOUGH TO ACTUALLY READ. The old ceiling was a PIXEL cap that did not
     grow with the page, so the panel shrank in proportion as the viewport got
     bigger — 320px of a 1400px page, 23%. 36vw holds one proportion at every
     size and the ceiling is now the same lane-derived expression as
     `max-width`, which is the real constraint. Portrait phones are untouched:
     they get their own edge-to-edge treatment further down.
     Contract: tests/e2e-playwright/manga-auto-panel.spec.js */
  width: clamp(
    200px,
    36vw,
    calc(
      100vw
      - var(--story-lane-offset)
      - var(--story-lane-w)
      - calc(180 * var(--story-ui-unit))
    )
  );
  max-width: calc(
    100vw
    - var(--story-lane-offset)
    - var(--story-lane-w)
    - calc(180 * var(--story-ui-unit))
  );
  max-height: calc(
    100dvh
    - max(env(safe-area-inset-top, 0px), calc(8 * var(--story-ui-unit)))
    - max(env(safe-area-inset-bottom, 0px), calc(8 * var(--story-ui-unit)))
  );
  padding: calc(8 * var(--story-ui-unit));
  box-sizing: border-box;
  overflow: hidden;
  border-radius: calc(14 * var(--story-ui-unit));
  border: calc(2 * var(--story-ui-unit)) solid rgba(238, 219, 241, 0.55);
  background: linear-gradient(135deg, rgba(40, 30, 62, 0.96), rgba(19, 14, 33, 0.97));
  box-shadow:
    0 calc(16 * var(--story-ui-unit)) calc(46 * var(--story-ui-unit))
    rgba(0, 0, 0, 0.6);
  cursor: pointer;
  -webkit-backdrop-filter: blur(calc(6 * var(--story-ui-unit)));
  backdrop-filter: blur(calc(6 * var(--story-ui-unit)));
}

#story-feed-manga-panel[hidden] {
  display: none;
}

#story-feed-manga-panel:focus-visible {
  outline: calc(3 * var(--story-ui-unit)) solid rgba(234, 199, 97, 0.92);
  outline-offset: calc(3 * var(--story-ui-unit));
}

.story-feed-manga-panel-img {
  display: block;
  flex: 1 1 auto;
  align-self: center;
  width: auto;
  max-width: 100%;
  height: auto;
  min-height: 0;
  max-height: calc(
    100dvh
    - max(env(safe-area-inset-top, 0px), calc(8 * var(--story-ui-unit)))
    - max(env(safe-area-inset-bottom, 0px), calc(8 * var(--story-ui-unit)))
    - calc(58 * var(--story-ui-unit))
  );
  object-fit: contain;
  border-radius: calc(9 * var(--story-ui-unit));
}

.story-feed-manga-panel-cap {
  margin-top: calc(6 * var(--story-ui-unit));
  text-align: center;
  font-family: 'Spectral', Georgia, serif;
  font-size: calc(12 * var(--story-ui-unit));
  color: #cdbce0;
}

.story-feed-manga-panel--in {
  animation: story-manga-panel-in 0.35s cubic-bezier(0.2, 0.9, 0.3, 1.05);
}

@keyframes story-manga-panel-in {
  from { opacity: 0; transform: translateY(-50%) scale(0.9); }
  to { opacity: 1; transform: translateY(-50%) scale(1); }
}

/* THE LANE STANDS DOWN WHILE ITS OWN PANEL IS ON SCREEN.
   With the panel up, three lane controls were still painting beside it:
   `.story-feed-turn-prev`, `.story-feed-turn-next`, and a `.story-tile--manga`
   thumbnail of the exact page the panel was already showing full size. Two ways
   to page, one of them a postage stamp of the other.

   Fade rather than remove: the lane keeps its box, so nothing reflows when the
   panel closes and the controls are where the hand left them. `:has()` is the
   whole mechanism and its failure mode is the previous behaviour (chrome stays
   visible), never a missing control.
   Contract: tests/e2e-playwright/manga-auto-panel.spec.js */
body:has(#story-feed-manga-panel:not([hidden])) :is(
  .story-feed-turnbar,
  .story-feed-column
) {
  opacity: 0;
  pointer-events: none;
}

/* Blurring the animated board behind a simultaneously animated loading spinner
   causes a full-panel repaint on every frame. The opaque panel already carries
   enough contrast, so phone viewports use the cheaper compositing path. */
@media (orientation: portrait) and (max-width: 768px),
       (orientation: landscape) and (max-height: 560px) {
  #story-feed-manga-panel {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

@media (orientation: portrait) and (max-width: 768px) {
  #story-feed-manga-panel {
    top: auto;
    right: max(12px, env(safe-area-inset-right, 0px));
    bottom: max(12px, env(safe-area-inset-bottom, 0px));
    left: max(12px, env(safe-area-inset-left, 0px));
    width: auto;
    max-width: none;
    max-height: min(56dvh, 480px);
    transform: none;
  }

  #story-feed-manga-panel.story-feed-manga-panel--in {
    animation: story-manga-panel-in-portrait 0.3s cubic-bezier(0.2, 0.9, 0.3, 1.05);
  }

  .story-feed-manga-panel-img {
    max-height: calc(min(56dvh, 480px) - 58px);
  }
}

@keyframes story-manga-panel-in-portrait {
  from { opacity: 0; transform: translateY(10px) scale(0.96); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .story-feed-manga-panel--in { animation: none; }
}

/* Visible manga failure state (was ARIA-only) */
.story-tile-failed-label {
  color: #ff9e9e;
}

/* The rail sat a further 6 story-units inboard of the notch inset, which on an
   iPhone 13 Pro is visible dead space: env(safe-area-inset-left) already clears
   the Dynamic Island, so the extra pad only narrows the lane. Spend it on the
   narration instead — the notch assertion in mobile-page-box.real.spec.js keeps
   this honest. */
@media (orientation: landscape) and (max-height: 560px) and (max-width: 932px) {
  :root {
    /* HOW FAR THE RAIL REACHES, published for whatever has to sit beside it.

       The rail's box is `--story-lane-w` wide, but its turn bar is two
       `max(44px, --story-nav-control)` arrows in a lane that is narrower than
       both of them together (72px against 88px at every phone page box), and
       the bar is `overflow: visible`, so the "next" arrow's box runs past the
       lane's edge whenever the story spans two or more turns. A control seated
       off the lane's own width would sit under that arrow. This token is the
       rail's TRUE right edge — the feed's `left` below plus the wider of the
       two — so the guest tutorial's Skip (tutorial.css) clears the arrow
       without re-typing any of these terms. */
    --story-rail-reach: calc(
      env(safe-area-inset-left, 0px)
      + calc(2 * var(--story-ui-unit))
      + max(
        var(--story-lane-w),
        calc(2 * max(44px, var(--story-nav-control)))
      )
    );
  }

  #story-feed {
    left: calc(
      env(safe-area-inset-left, 0px)
      + calc(2 * var(--story-ui-unit))
    );
  }
}

/* The inspection highlight: a full-viewport darkening painted by the panel's
   own spread shadow, so it needs no extra element and cannot intercept the
   click that opens the fullscreen viewer. Present only while the panel is
   being touched or hovered — an auto-revealed panel stays as quiet as a story
   beat, which is the contract the owner asked for. */
#story-feed-manga-panel.story-feed-manga-panel--inspecting {
  border-color: rgba(238, 219, 241, 0.92);
  box-shadow:
    0 calc(16 * var(--story-ui-unit)) calc(46 * var(--story-ui-unit)) rgba(0, 0, 0, 0.6),
    0 0 calc(18 * var(--story-ui-unit)) rgba(234, 199, 97, 0.35),
    0 0 0 100vmax rgba(4, 2, 10, 0.62);
}

@media (prefers-reduced-motion: no-preference) {
  #story-feed-manga-panel {
    transition: box-shadow 0.18s ease, border-color 0.18s ease;
  }
}
