/*
 * Media-lightbox stylesheet — the shared viewer core.
 *
 * Served as a static asset and pulled in by `<LightboxStyles>` rather than
 * `import`ed, for exactly the reasons given at the top of chrome.css,
 * category.css and delivery.css: a bundled import attaches the chunk to the
 * shared `[brand]` layout segment and Next then emits a <link> to it on every
 * brand's every page. A hoisted <link> to a public asset is rendered only by the
 * surfaces that can open a viewer.
 *
 * ─── THE BOARD'S FIXED FRAME IS RETIRED (Ryan's walkthrough, 2026-08-26) ─────
 *
 * design/produnk/PhotoViewers.dc.html drew this viewer at a 1040px frame around
 * a 726x520 stage and a 320px panel, and the build shipped those numbers to the
 * pixel. Ryan's ruling replaces the fixed stage with one that FITS THE VIEWPORT:
 * the panel keeps its readable 320, the control band and the strip take the
 * height they need, and the stage takes everything left over — so a photograph
 * grows until whichever axis constrains first, with its aspect kept and its
 * magnification capped at twice the source's own resolution (`stageCap()` in
 * media-list.ts writes that cap as a `max-width`/`max-height` pair).
 *
 * What survives from the board, because none of it was about the frame's size:
 * the dark stage and its darker strip, the 84x64 tiles and the phone's 54x42,
 * the 5px/9px chip padding, the 40px nav discs at 14px from the edge, the
 * 12px/16px strip padding, the accent selection outline, the panel's type. The
 * board remains the reference for APPEARANCE and is no longer the reference for
 * GEOMETRY — `tools/lbgate.mjs` was re-recorded to match, and its board-fixed
 * crops are retired with a note that says so.
 *
 * ─── THE RESERVED CONTROL BAND (Ryan's design, 2026-08-26) ──────────────────
 *
 * The top strip of the viewer is permanent control chrome ON ITS OWN GROUND:
 * the counter, the two chips and the close cross live in a band that spans the
 * frame, and a photograph never paints under any of them. It is the delivery
 * modal's anatomy — `.sf-dlv__head` is a full-width tinted band with the close
 * at its right — in this viewer's dark palette rather than the modal's well
 * tint. It also SETTLES the flagged white-chips-on-white question by
 * construction: the chips no longer sit on the photograph, so there is no
 * photograph they can vanish into, and being on a known ground they take the
 * platform focus ring rather than the on-media halo pair.
 *
 * Class prefix `sf-lb-`.
 */

/* ------------------------------------------------------- the focus ring -- */

/*
 * EVERY INTERACTIVE CONTROL IN THIS SHEET (Ryan's ruling, 2026-08-26 — the
 * platform pattern, its three grounds and the contrast arithmetic are
 * documented once, at the top of chrome.css).
 *
 * Written here rather than beside each component, the discipline delivery.css
 * sets out: the ring is a site-wide rule, and a rule spread across a sheet is a
 * rule the next control gets missed by. New control in this viewer: add its
 * class to the group whose ground it lands on.
 *
 * The `var()` fallbacks repeat the platform values exactly once each, so this
 * sheet still draws a compliant ring if it is ever loaded without chrome.css.
 *
 * --- ON THE BAND AND THE STRIP: the dark ground ---
 *
 * The band is `#23262E` and the strip is `#0B0C0E`; accent-on-dark `#FFB27A`
 * reads 9.1:1 and 13.9:1 against them. This is the group the control band buys:
 * a chip on a KNOWN ground takes the ordinary ring, and only the controls still
 * sitting on the photograph need the halo pair below.
 */
.sf-lb__chip:focus-visible,
.sf-lb__close:focus-visible,
.sf-lb__thumb:focus-visible,
.sf-lb__tray-toggle:focus-visible {
  outline: var(--sf-focus-ring-on-dark, 2px solid var(--brand-color-accent-on-dark, #ffb27a));
  outline-offset: var(--sf-focus-ring-offset, 2px);
}

/*
 * --- ON THE PHOTOGRAPH: the halo pair ---
 *
 * The two nav discs are the only controls left over the picture, and a control
 * on a photograph has no known adjacent colour — so it takes the pair chrome.css
 * documents: a white halo for dark grounds, an ink ring for light ones, and the
 * two bands overlap so whatever the photograph is doing under the disc, one of
 * them contrasts.
 */
.sf-lb__nav:focus-visible {
  box-shadow: var(--sf-focus-halo, 0 0 0 4px var(--brand-color-surface, #ffffff));
  outline: var(--sf-focus-ring-on-media, 2px solid var(--brand-color-ink, #17181a));
  outline-offset: var(--sf-focus-ring-offset, 2px);
}

/*
 * --- ON THE PANEL'S LIGHT GROUND ---
 *
 * Whatever a caller puts in the context panel, plus the preview harness's own
 * openers on the page's paper. Accent clears 3:1 on all of them.
 */
.sf-lb__panel a:focus-visible,
.sf-lb__panel button:focus-visible,
.sf-lb__panel input:focus-visible,
.sf-lb__panel select:focus-visible,
.sf-lb__panel textarea:focus-visible,
.sf-lb-preview__open:focus-visible {
  outline: var(--sf-focus-ring, 2px solid var(--brand-color-accent, #c4530a));
  outline-offset: var(--sf-focus-ring-offset, 2px);
}

/* ----------------------------------------------------------- the frame -- */

/*
 * THE PEEK, DECLARED ONCE — how much of the phone's metadata tray shows while
 * it is shut, and how tall the control that opens it is. The component reads
 * this value back with `getComputedStyle` to interpolate a drag, so the
 * stylesheet is the single source for it and the two can never disagree: the
 * tray's 22px top pad (which is the handle and its lead) plus one 15px title
 * line — MEASURED, not guessed: at 44px the caption's first pixel row sits
 * exactly on the cut, so the shut tray shows the handle and the title and
 * nothing else, whatever the caption says.
 */
:root {
  --sf-lb-peek: 44px;
}

/*
 * THE DIALOG ELEMENT ITSELF is only a transparent, full-viewport host. Every
 * pixel the board draws is painted by `__scrim` and the boxes inside it, which
 * is what lets the wash be read back by computed style — `::backdrop` cannot be
 * queried, and a gate that cannot read a value cannot hold it. Same reset, same
 * reasons, as `.sf-dlv`.
 */
.sf-lb {
  background: transparent;
  border: 0;
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-body, system-ui, sans-serif);
  height: 100%;
  inset: 0;
  /* globals.css sets `line-height: 1.65` on `body` and the board sets no
   * leading at all, so every line box that does not name one has to be
   * `normal` or every block in the panel deepens. */
  line-height: normal;
  margin: 0;
  max-height: none;
  max-width: none;
  overflow: hidden;
  padding: 0;
  /* A viewer INHERITS FROM WHEREVER IT IS MOUNTED — a centred hero, a
   * right-aligned toolbar, a table cell — and `text-align` inherits. Without
   * this the panel's every left-aligned line follows the alignment of the
   * thumbnail that opened it. */
  text-align: left;
  width: 100%;
}

/*
 * The scrim carries the wash so that ONE element is responsible for it. A
 * `::backdrop` left at the UA default would darken underneath this and the two
 * would compound — and when this viewer opens OVER the delivery modal there are
 * already two washes on screen, which is exactly the case a third would ruin.
 */
.sf-lb::backdrop {
  background: transparent;
}

/*
 * The board's viewer scrim: `rgba(23,24,26,0.62)` — ink at 62%, DEEPER than the
 * modal scrim's 55% (delivery.css) and deliberately so: a photo viewer takes
 * the screen, a modal sits on a page. Written as a literal for the same reason
 * the modal's is — a colour token cannot carry an alpha channel, and the scrim
 * is a platform constant like the radii.
 *
 * IT IS ALSO THE FULLSCREEN ELEMENT (see `.sf-lb__scrim:fullscreen` below and
 * the header of media-lightbox.tsx): the dialog itself cannot be fullscreened —
 * Chrome throws `TypeError: Dialog elements are invalid` — so the request goes
 * to the box that carries the wash and holds every other box inside it.
 */
.sf-lb__scrim {
  align-items: center;
  background: rgba(23, 24, 26, 0.62);
  display: flex;
  height: 100%;
  justify-content: center;
  /* Board: 30px around the frame. */
  padding: 30px;
  /* A scroll container, and therefore a containing block: an absolutely
   * positioned descendant whose ancestors are all static resolves against the
   * INITIAL containing block and extends the DOCUMENT's scrollable overflow —
   * the sr-span overflow bug (playbook §5). Unconditional, so nothing added
   * later can escape either. */
  overflow-y: auto;
  position: relative;
  -webkit-overflow-scrolling: touch;
}

/*
 * FULL SCREEN. The scrim is the element that goes into the fullscreen top
 * layer, so it must keep painting its own wash: Chrome's UA sheet gives a
 * fullscreen element a `background: black` it would otherwise inherit, and the
 * frame's shadow would then sit on a ground the rest of the viewer never wears.
 * `::backdrop` behind it is the same ink, so the corners the frame does not
 * reach are the scrim's colour rather than the UA's black.
 */
.sf-lb__scrim:fullscreen {
  background: rgb(23, 24, 26);
  height: 100%;
  width: 100%;
}

.sf-lb__scrim:fullscreen::backdrop {
  background: rgb(23, 24, 26);
}

/*
 * THE FRAME FILLS WHAT IT IS GIVEN (Ryan's ruling — see the header). It is the
 * scrim's 30px margins subtracted from the viewport, and the grid inside it
 * hands every pixel the band, the panel and the strip do not need to the stage.
 *
 * `100dvh` rather than `100vh`: on a phone the dynamic viewport is what is
 * actually visible once the browser's own chrome has collapsed, and a stage
 * sized to the large viewport hides its own strip behind the address bar.
 */
.sf-lb__frame {
  background: var(--brand-color-surface, #ffffff);
  border-radius: var(--radius-control);
  box-shadow: 0 24px 60px rgba(23, 24, 26, 0.35);
  display: grid;
  /* THE BAND SPANS THE FRAME — the delivery modal's anatomy: one bar of
   * control chrome across the top, the close at its right, everything else
   * underneath it. */
  grid-template-areas:
    "band  band"
    "stage panel"
    "strip panel";
  /* The stage column is `1fr` with its floor removed: without `minmax(0, 1fr)`
   * the strip's own min-content (the tiles plus the hint) becomes the column's
   * minimum, and the number of tiles the strip draws is then decided by the
   * number of tiles the strip draws. */
  grid-template-columns: minmax(0, 1fr) 320px;
  grid-template-rows: auto minmax(0, 1fr) auto;
  height: 100%;
  max-height: 100%;
  max-width: 100%;
  overflow: hidden;
  position: relative;
  width: 100%;
}

/*
 * The open transition. The boards carry appearance and never motion (playbook
 * §2), so this is the smallest honest thing: the frame fades up over 160ms.
 * Reduced motion gets the end state with no animation at all — not a shorter
 * one.
 */
@media (prefers-reduced-motion: no-preference) {
  .sf-lb[open] .sf-lb__frame {
    animation: sf-lb-rise 160ms ease-out;
  }
}

@keyframes sf-lb-rise {
  from {
    opacity: 0;
    transform: scale(0.99);
  }

  to {
    opacity: 1;
    transform: scale(1);
  }
}

/* ---------------------------------------------------- the control band -- */

/*
 * PERMANENT CONTROL CHROME, ON ITS OWN GROUND (Ryan's design, 2026-08-26).
 * `--brand-color-media-well-a` is the palette's lighter dark — twelve per
 * channel above the stage's ink, which is the same separation the strip already
 * uses below it, so the three horizontal bands of the viewer read as three
 * surfaces without a rule between them.
 */
.sf-lb__band {
  align-items: center;
  background: var(--brand-color-media-well-a, #23262e);
  display: flex;
  gap: 10px;
  grid-area: band;
  min-height: 48px;
  padding: 8px 8px 8px 16px;
  position: relative;
  /* Above the stage in the paint order, so nothing the stage draws — a zoomed
   * photograph, the drag ghost — can ever cross into the band. */
  z-index: 3;
}

/* The chip pair, held together at the band's left. */
.sf-lb__band-controls {
  display: flex;
  gap: 8px;
}

/*
 * The board's two chips. They are `<span>`s on the board and `<button>`s here —
 * a control that does something has to be operable — so the type face, the
 * leading and the padding are all restated to keep the board's box: a UA button
 * brings its own font, its own centred text and its own 1px/6px padding.
 */
.sf-lb__chip {
  /*
   * INLINE-FLEX, TOP-ALIGNED — and that is the difference between a `<button>`
   * and the board's `<span>`, not a preference. A UA button CENTRES its content
   * in its content box; a span that is a flex item puts its line box at the
   * top.
   */
  align-items: flex-start;
  background: none;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: var(--radius-control);
  color: var(--brand-color-surface, #ffffff);
  cursor: pointer;
  display: inline-flex;
  font-family: inherit;
  font-size: var(--text-caption);
  line-height: normal;
  padding: 5px 9px;
  white-space: nowrap;
}

.sf-lb__chip:hover {
  background: rgba(255, 255, 255, 0.14);
}

/* Both chips are toggles, and a pressed toggle has to look pressed. */
.sf-lb__chip[aria-pressed="true"] {
  background: rgba(255, 255, 255, 0.22);
  border-color: rgba(255, 255, 255, 0.6);
}

/*
 * The counter, pushed to the band's right and standing just left of the close.
 * `margin-left: auto` rather than a spacer element, so the day a third control
 * joins the left group nothing has to be re-ordered.
 */
.sf-lb__counter {
  color: var(--brand-color-line-mid, #d8d6d0);
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: var(--text-micro);
  letter-spacing: 0.1em;
  margin-left: auto;
}

/* ----------------------------------------------------------- the stage -- */

.sf-lb__stage {
  align-items: center;
  background: var(--brand-color-ink, #17181a);
  display: flex;
  grid-area: stage;
  justify-content: center;
  overflow: hidden;
  position: relative;
  /*
   * THE GESTURE SURFACE OWNS ITS OWN TOUCHES. Without this the browser claims a
   * vertical drag for page scrolling and a two-finger spread for its own pinch,
   * and the pointer stream this viewer reads simply stops arriving mid-gesture
   * — `pointercancel`, no `pointermove`, and a photograph frozen half-panned.
   */
  touch-action: none;
  /* A native drag on an <img> is the other thief: it takes over the pointer and
   * hands the reader a drag-and-drop ghost of the photograph. */
  -webkit-user-select: none;
  user-select: none;
}

/*
 * The transform target — one box the magnifier scales and the drag translates,
 * so a photograph, a placeholder line and the film's media well all zoom the
 * same way and none of them can drag itself off its own stage (the offset is
 * clamped to the overhang the scale creates — `clampPan()` in media-list.ts).
 *
 * `translate` and `scale` ONLY, and nothing else animated: both are composited,
 * so a drag never touches layout or paint. The transform is written straight
 * onto this node from a rAF rather than through React state — see
 * media-lightbox.tsx.
 */
.sf-lb__figure {
  align-items: center;
  display: flex;
  /*
   * OUT OF FLOW, ON PURPOSE. A photograph taller than the stage would otherwise
   * SET the stage's height: the stage's own height is decided by the grid, so
   * `max-height: 100%` on the image needs a definite box to resolve against and
   * an in-flow figure does not give it one — measured, the 900x1088 portrait
   * grew a 608px frame to 840 and then had it clipped.
   */
  inset: 0;
  justify-content: center;
  position: absolute;
  will-change: transform;
}

/* Only while a pointer is actually down: a `grabbing` cursor on a stage nobody
 * is holding is a lie, and a `grab` cursor on a stage that cannot pan is worse. */
.sf-lb__stage[data-zoomed="true"] .sf-lb__figure {
  cursor: grab;
}

.sf-lb__stage[data-dragging="true"] .sf-lb__figure {
  cursor: grabbing;
}

/*
 * The settle. A gesture that ends inside its threshold puts the picture back,
 * and it does that over 200ms rather than instantly — the one piece of motion
 * a direct-manipulation surface owes the reader, because an instant snap reads
 * as the viewer having lost the drag. It is applied ONLY between gestures
 * (`data-dragging` removes it), so nothing is ever interpolated while a finger
 * is down, and reduced motion drops it entirely.
 */
@media (prefers-reduced-motion: no-preference) {
  .sf-lb__stage:not([data-dragging="true"]) .sf-lb__figure {
    transition: transform 200ms cubic-bezier(0.22, 0.61, 0.36, 1);
  }
}

/*
 * THE FIT (Ryan's ruling): the photograph grows until whichever axis constrains
 * first, keeping its aspect. `object-fit: contain` is what keeps the aspect;
 * `width`/`height: 100%` is what lets it GROW to the stage rather than stopping
 * at its intrinsic size; and the `max-width`/`max-height` pair the component
 * writes inline is the magnification cap — never more than twice the source's
 * own pixels, so a small render centres at its cap instead of going mushy.
 *
 * CONTAIN, never cover: a viewer that crops the photograph it was opened to
 * show is the one thing it must not do.
 */
.sf-lb__photo {
  display: block;
  height: 100%;
  object-fit: contain;
  width: 100%;
}

/*
 * The board's own stage line for a photograph that does not exist yet — mono,
 * 10.5px, in the light grey the board picks for it (`#D8D6D0`, the platform's
 * `line-mid`, doing a text job here on a dark ground where it is legible).
 */
.sf-lb__placeholder {
  color: var(--brand-color-line-mid, #d8d6d0);
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: var(--text-micro);
  text-align: center;
}

/*
 * THE EMBEDDED FILM (Ryan's ruling, 2026-08-27 — YouTube, as little branding as
 * possible). It fills the figure exactly as a photograph does, and it is the one
 * thing on this stage the viewer does NOT get to transform: the player owns its
 * own controls, so a magnifier that scaled it would only push its scrub bar off
 * the stage. `border: 0` because a bare <iframe> still draws the platform's
 * inset frame around a dark player.
 *
 * 16:9 IS DECLARED, not inherited. The figure is `position: absolute; inset: 0`
 * with no intrinsic content to size against once the film is an iframe, so
 * without an aspect the player would stretch to the whole stage box and letterbox
 * itself inside its own frame — two sets of black bars. `max-*: 100%` keeps the
 * declared aspect inside whichever axis constrains first, which is the same fit
 * rule `.sf-lb__photo` obeys.
 */
.sf-lb__film {
  aspect-ratio: 16 / 9;
  border: 0;
  display: block;
  height: auto;
  max-height: 100%;
  max-width: 100%;
  width: 100%;
}

/*
 * THE FILM WE SERVE OURSELVES (Ryan's ruling FA, 2026-08-27) — same class, one
 * modifier, because everything above it is still right and exactly two things
 * are not.
 *
 * THE RATIO IS THE FILE'S, NOT A DECLARATION. The 16:9 above exists because an
 * <iframe> has no intrinsic size to fit against; a `<video>` has one, and this
 * film's is 3:2 (720 x 480, square pixels). Declaring 16:9 over it would put
 * pillarbox bars inside the element — the same class of bug F3 root-caused on
 * the hero slot, arriving from the other direction. (The hero's box IS 16:9 by
 * ruling and crops to it with `object-fit: cover`; the VIEWER is where the film
 * is watched, so the viewer shows all of it.)
 *
 * AND IT IS CAPPED AT TWICE ITS OWN RESOLUTION, which is not a new rule: it is
 * `MAX_MAGNIFICATION` from `media-list.ts`, the ruling that a photograph never
 * renders at more than twice its source (Ryan, 2026-08-26). 1440 x 960 is
 * 2 x 720 x 480. A stage bigger than that centres the film rather than smearing
 * it across the whole width, and the scrim behind it is the same dark ground
 * the rest of the viewer paints.
 */
.sf-lb__film--native {
  aspect-ratio: auto;
  height: auto;
  max-height: min(100%, 960px);
  max-width: min(100%, 1440px);
  width: auto;
}

/*
 * THE FILM STAGE. No film has been re-hosted, so this is the platform's media
 * well — the `linear-gradient(160deg, a, b)` every video and photo placeholder
 * wears — carrying the position the caller cued, in the mono placeholder ink.
 * Same pattern, same wording, as the delivery modal's `__player`.
 */
.sf-lb__well {
  align-items: center;
  background: linear-gradient(
    160deg,
    var(--brand-color-media-well-a, #23262e),
    var(--brand-color-media-well-b, #0b0c0e)
  );
  display: flex;
  flex-direction: column;
  gap: 6px;
  height: 100%;
  justify-content: center;
  padding: 20px;
  text-align: center;
  width: 100%;
}

.sf-lb__well-cue {
  color: var(--brand-color-surface, #ffffff);
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: var(--text-support);
  letter-spacing: 0.06em;
}

.sf-lb__well-note {
  color: var(--brand-color-placeholder-on-dark, #5a5d66);
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: var(--text-micro);
  letter-spacing: 0.12em;
}

/* --------------------------------------------------- the stage controls -- */

/* The board's 40px translucent discs, vertically centred, 14px in. */
.sf-lb__nav {
  align-items: center;
  background: rgba(255, 255, 255, 0.12);
  border: 0;
  border-radius: var(--radius-pill);
  color: var(--brand-color-surface, #ffffff);
  cursor: pointer;
  display: flex;
  height: 40px;
  justify-content: center;
  padding: 0;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 40px;
  /* Above the figure, and clear of the band. */
  z-index: 2;
}

.sf-lb__nav:hover {
  background: rgba(255, 255, 255, 0.24);
}

.sf-lb__nav--prev {
  left: 14px;
}

.sf-lb__nav--next {
  right: 14px;
}

/* ------------------------------------------------------------ the strip -- */

.sf-lb__strip {
  align-items: center;
  background: var(--brand-color-media-well-b, #0b0c0e);
  display: flex;
  gap: 8px;
  grid-area: strip;
  /* Board verbatim. It is also what makes the strip a WINDOW rather than a
   * scroller: the tiles that do not fit are counted by the "+N" tile, never
   * hidden behind a scrollbar nobody looks for (Baymard, on the board). */
  overflow: hidden;
  padding: 12px 16px;
}

.sf-lb__thumb {
  align-items: center;
  background: var(--brand-color-well, #f1f0ec);
  border: 0;
  border-radius: var(--radius-inner);
  cursor: pointer;
  display: flex;
  flex: none;
  height: 64px;
  justify-content: center;
  overflow: hidden;
  padding: 0;
  position: relative;
  width: 84px;
}

/* The selection language, on a tile whose own edges are the photograph's: an
 * OUTLINE rather than a border, so the tile does not change size when it
 * becomes current and the strip does not shuffle. */
.sf-lb__thumb--current {
  outline: 2px solid var(--brand-color-accent, #c4530a);
  outline-offset: 1px;
}

/*
 * THE TILE SHOWS THE WHOLE SHOT (Ryan's ruling, 2026-08-26 — "thumbnail aspect:
 * fix"). It was `object-fit: cover`, which fills the tile by cropping to it: a
 * 1800x586 panorama arrived as a 84x64 slice of its middle and a 760x432 truck
 * as a zoom into one corner, so the strip stopped being a map of the gallery.
 * `contain` keeps every source's own aspect inside the board's tile and lets the
 * tile's well tint show around it, which is what makes two shots of different
 * shapes readable side by side.
 */
.sf-lb__thumb-photo {
  display: block;
  height: 100%;
  object-fit: contain;
  width: 100%;
}

/* The board's `.cgi` label at its own size — 9px mono, muted. Below the type
 * scale's floor on purpose: it is a placeholder marker inside a 64px tile, the
 * same exemption the parts-diagram micro-text carries. */
.sf-lb__thumb-label {
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: 9px;
  padding: 0 4px;
  text-align: center;
}

/* Baymard's signpost: the tile that says how many are not on screen. */
.sf-lb__thumb--more {
  background: var(--brand-color-media-well-a, #23262e);
  color: var(--brand-color-surface, #ffffff);
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: var(--text-small);
  font-weight: 700;
}

.sf-lb__thumb-badge {
  align-items: center;
  background: var(--brand-color-accent, #c4530a);
  border-radius: var(--radius-pill);
  bottom: 5px;
  color: var(--brand-color-surface, #ffffff);
  display: flex;
  height: 18px;
  justify-content: center;
  position: absolute;
  right: 5px;
  width: 18px;
}

/*
 * The keyboard hint. `flex: none` is this build's, not the board's: the board's
 * span happens never to be squeezed, and a shrinking hint would change the room
 * the strip measures for tiles and make the tile count depend on the width of a
 * label.
 */
.sf-lb__hint {
  color: var(--brand-color-steel, #7387a6);
  flex: none;
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: var(--text-micro);
  letter-spacing: 0.1em;
  margin-left: auto;
  white-space: nowrap;
}

/* --------------------------------------------- the shared position bar -- */

/*
 * THE PROGRESSION DOTS ARE RETIRED (Ryan's ruling G5 v2, 2026-08-27).
 * `.sf-lb__dots`, `.sf-lb__dot`, `.sf-lb__dot--on` and `.sf-lb__dot--edge` are
 * gone by name. They drew a windowed rail of seven pills, and a window over a
 * long album SLIDES ONLY FAR ENOUGH to keep the current pill inside it — so a
 * run of swipes through the middle of a twenty-four frame gallery drew an
 * identical rail every time. Ryan read the dead zone off the live product page
 * and ruled: a bar, filled by fraction, which has no dead zone at any count.
 *
 * ─── AND IT IS THE ONE BAR THE WHOLE SITE DRAWS ─────────────────────────────
 *
 * The other half of the ruling is that this treatment WON. The product page's
 * phone stage drew its own dots in its own palette; there is now ONE chrome —
 * `.sf-mprog`, below, drawn by `media-progress.tsx` — consumed by this viewer
 * and by that stage, so the two are the same object in two places rather than
 * two objects that resemble each other.
 *
 * ─── WHY A FOREIGN PREFIX LIVES IN THIS SHEET ───────────────────────────────
 *
 * `sf-mprog` is not `sf-lb`, and this sheet otherwise owns its prefix strictly
 * (`tests/media-lightbox.test.ts` sweeps for exactly that). The exception is
 * deliberate and is recorded in the test beside the sweep: the shared bar is
 * THIS viewer's treatment, promoted, and this is the sheet every surface that
 * can draw it already loads — the component pulls it in itself with
 * `<LightboxStyles>`. A second stylesheet for twenty lines of shared chrome
 * would be a second `<link>` on every page that already has this one.
 *
 * THE MEASUREMENTS ARE THE DOTS' OWN, so the bar has the mass the rail had:
 * 4px tall, a 2px pill radius, and 162px of track — which is what seven 18px
 * dots and six 6px gaps came to. Nothing about the phone's foot moved.
 */
.sf-mprog {
  background: var(--sf-mprog-track, rgba(255, 255, 255, 0.34));
  border-radius: 2px;
  display: block;
  height: 4px;
  /* The fill is a percentage of this box, and a percentage of `auto` is
   * nothing: the track is a stated width, not a stretched one. */
  overflow: hidden;
  width: 162px;
}

/*
 * THE TWO COLOURS ARE READ AS FALLBACKS, NEVER DECLARED (2026-08-27).
 *
 * A surface whose ground cannot take white ink overrides `--sf-mprog-track` and
 * `--sf-mprog-ink` on its own box and lets inheritance carry them in. Written
 * as `var(--x, default)` here, that override wins wherever it is set; written
 * as a declaration ON this class it would shadow the inherited value and the
 * override would silently do nothing — the two rules would be on the same
 * element and the winner would be whichever sheet loaded last.
 *
 * The viewer sets neither and gets the ruled look: a solid white fill in a
 * white-at-34% track, on the scrim's near-black. The product page's phone stage
 * sets both, because its bar sits on a photograph shot on white (its own sheet
 * carries the arithmetic).
 */
.sf-mprog__fill {
  background: var(--sf-mprog-ink, var(--brand-color-surface, #ffffff));
  border-radius: inherit;
  display: block;
  height: 100%;
}

/*
 * WHERE THIS VIEWER PUTS IT: exactly where the dot rail sat — centred, above
 * the tray's closed peek and out from under it. Hidden until the phone query
 * below, because the desktop layout draws the thumbnail strip instead.
 */
.sf-lb__progress {
  display: none;
  left: 50%;
  position: absolute;
  transform: translateX(-50%);
  bottom: calc(var(--sf-lb-peek) + 12px);
  z-index: 2;
}

/* --------------------------------------------------- the first-use hint -- */

/*
 * SHOWN ONCE, THEN NEVER (Ryan's ruling, 2026-08-26). A phone reader has no
 * arrows to look at any more, so the first viewer of a session says how to move
 * — and then gets out of the way, because a hint that keeps appearing is an
 * instruction the reader has already followed.
 *
 * `aria-hidden`, and for the same reason as the dots: it is a pointer
 * affordance being explained to a pointer, and the keyboard/screen-reader path
 * (the arrows, which stay focusable) never needed it.
 */
.sf-lb__swipe-hint {
  align-items: center;
  background: rgba(11, 12, 14, 0.72);
  border-radius: var(--radius-pill);
  color: var(--brand-color-surface, #ffffff);
  display: none;
  font-size: var(--text-caption);
  gap: 8px;
  left: 50%;
  padding: 9px 16px;
  pointer-events: none;
  position: absolute;
  top: 50%;
  transform: translate(-50%, -50%);
  white-space: nowrap;
  z-index: 2;
}

@media (prefers-reduced-motion: no-preference) {
  .sf-lb__swipe-hint {
    animation: sf-lb-hint 2600ms ease-out forwards;
  }
}

/* Reduced motion gets the same one showing and the same disappearance, without
 * the fade: the component removes the node on the same timer either way. */
@keyframes sf-lb-hint {
  0% {
    opacity: 0;
  }

  14% {
    opacity: 1;
  }

  72% {
    opacity: 1;
  }

  100% {
    opacity: 0;
  }
}

/* ------------------------------------------------------------ the panel -- */

.sf-lb__panel {
  display: flex;
  flex-direction: column;
  gap: 14px;
  grid-area: panel;
  /* A panel whose caller fills it with an album record and a CTA must scroll
   * inside the frame rather than growing it past the viewport. */
  overflow-y: auto;
  padding: 22px 24px;
  position: relative;
}

/* Desktop has no bottom sheet, so it has neither a drag handle nor the button
 * that opens one. */
.sf-lb__handle,
.sf-lb__tray-toggle {
  display: none;
}

.sf-lb__eyebrow {
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: var(--text-micro);
  letter-spacing: 0.12em;
  margin-bottom: 6px;
}

.sf-lb__title {
  color: var(--brand-color-ink, #17181a);
  /* globals.css styles every h1-h3 with the heading face at weight 500, a
   * -0.015em track, 1.12 leading and `text-wrap: balance`. The board draws
   * none of that, so all five are restated.
   *
   * `wrap`, not `normal`: `normal` is NOT a value of `text-wrap` and is dropped
   * as invalid, which leaves `balance` from globals.css still in force — the
   * defect the homepage verify round found in nine places on 2026-08-26. This
   * was the ninth. */
  font-family: var(--brand-font-body, system-ui, sans-serif);
  font-size: var(--text-product);
  font-weight: 700;
  letter-spacing: normal;
  line-height: normal;
  margin: 0;
  text-wrap: wrap;
}

.sf-lb__caption {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-support);
  margin: 3px 0 0;
}

/* ------------------------------------------------------------ the close -- */

/*
 * IN THE BAND, at its right — the delivery modal's anatomy, and one control
 * rather than the two board positions the old build reconciled with an
 * absolutely positioned button. The mark keeps the board's 20px at the board's
 * 1.6 stroke; the 8px of padding around it is what makes a 36px target.
 */
.sf-lb__close {
  align-items: center;
  background: none;
  border: 0;
  border-radius: var(--radius-inner);
  color: var(--brand-color-line-mid, #d8d6d0);
  cursor: pointer;
  display: flex;
  flex: none;
  justify-content: center;
  padding: 8px;
}

.sf-lb__close:hover {
  background: rgba(255, 255, 255, 0.14);
  color: var(--brand-color-surface, #ffffff);
}

/*
 * Accessible names for controls whose whole content is a decorative SVG — the
 * two chevrons and the cross — and the live region that says what the stage
 * changed to.
 *
 * `position: absolute` on a clipped span is the pattern that caused the page
 * overflow bug (playbook §5). Every one of these sits inside a positioned
 * ancestor: the nav buttons position themselves, the close sits in a positioned
 * band, and the live region's containing block is `.sf-lb__frame`.
 */
.sf-lb__sr {
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

/* ------------------------------------------------------------ the phone -- */

/*
 * "Mobile — full-screen stage, context as a bottom sheet", transplanted from
 * the board's four phone frames and then revised by Ryan's walkthrough. The cut
 * is the site's own mobile boundary (767.98, wider-side convention) rather than
 * a number of this viewer's: a viewer that reflows at a different width from the
 * page behind it reads as a bug. The component asks `matchMedia` for THE SAME
 * STRING, so the gestures and the layout can never disagree about which one is
 * in force.
 *
 * The band stays exactly where it is. Under it the stage fills the frame, the
 * strip is replaced by the progression dots, and the panel becomes a TRAY that
 * floats over the stage's bottom edge — closed to a peek, opened by a tap or a
 * swipe up.
 */
@media (max-width: 767.98px) {
  .sf-lb__scrim {
    padding: 0;
  }

  .sf-lb__frame {
    border-radius: 0;
    grid-template-areas:
      "band"
      "stage";
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
  }

  /* THE BAND'S PHONE ORDER (Ryan's ruling): zoom and full screen at the
   * top-left, close at the right, and NO COUNTER — the dots carry position now,
   * and the live region carries it for anyone not looking at them. */
  .sf-lb__counter {
    display: none;
  }

  .sf-lb__band {
    min-height: 44px;
    padding: 6px 6px 6px 12px;
  }

  /* The close is the band's right-hand end whether or not a counter pushed it
   * there. */
  .sf-lb__close {
    margin-left: auto;
  }

  /*
   * THE ARROWS COME OFF THE GLASS AND STAY IN THE TAB ORDER — the ruled
   * pushback against the board, kept through this revision. The board's phone
   * frames draw swipe and no arrows, and swipe is now really implemented, so
   * the discs are no longer DRAWN. They are not removed: they are the only path
   * through the gallery for a keyboard and the only pair of named controls a
   * screen reader can operate, and a gesture is not an affordance either can
   * reach. So they take the clipped-span treatment the accessible names take —
   * still focusable, still named, still first in the stage's tab order, and
   * visible again the moment they are focused so a sighted keyboard reader can
   * see where they are.
   */
  .sf-lb__nav {
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    width: 1px;
  }

  .sf-lb__nav:focus-visible {
    clip: auto;
    clip-path: none;
    height: 40px;
    overflow: visible;
    width: 40px;
  }

  /* The position bar instead of a strip. */
  .sf-lb__strip {
    display: none;
  }

  .sf-lb__progress {
    display: block;
  }

  .sf-lb__swipe-hint {
    display: flex;
  }

  /*
   * THE METADATA TRAY (Ryan's ruling, 2026-08-26). Out of the grid and over the
   * stage, so the picture keeps the whole frame; translated down to its peek
   * and back up by `--sf-lb-tray`, which is a TRANSFORM and therefore
   * composited — a drag interpolates it directly and a settle transitions it.
   *
   * The closed position is written as a percentage of the tray's OWN height, so
   * nothing has to measure the panel to park it: whatever a caller puts in the
   * panel, the peek is the same handle and the same title line.
   */
  .sf-lb__panel {
    background: var(--brand-color-surface, #ffffff);
    border-radius: var(--radius-panel) var(--radius-panel) 0 0;
    bottom: 0;
    box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.35);
    /* Tighter than the desktop column's 14, because the peek's height is a
     * constant and every pixel of it belongs to the title — but not ZERO, which
     * is what the board's phone frames implied when the head was the panel's
     * only child. A caller's block runs straight into the caption without it. */
    gap: 12px;
    left: 0;
    max-height: 72%;
    overflow-y: auto;
    /* The top pad clears the handle the peek's control draws over it: 8px of
     * air, the 4px bar, and 10px before the title — the board's own lead,
     * measured off its phone frames. */
    padding: 22px 16px 16px;
    position: absolute;
    right: 0;
    /*
     * THE TRAY'S OWN TOUCHES. Same reasoning as the stage: without this the
     * browser claims the vertical drag for the panel's own scroller and the
     * tray never moves. `pan-y` is deliberately NOT the closed value — the
     * drag has to be readable in both directions before the axis lock has
     * decided which one it is.
     */
    touch-action: none;
    transform: translateY(var(--sf-lb-tray, calc(100% - var(--sf-lb-peek))));
    /* It is the sheet's top edge, and the stage is behind it. */
    z-index: 2;
  }

  @media (prefers-reduced-motion: no-preference) {
    .sf-lb__frame:not([data-tray-dragging="true"]) .sf-lb__panel {
      transition: transform 220ms cubic-bezier(0.22, 0.61, 0.36, 1);
    }
  }

  /*
   * OPEN IS `--sf-lb-tray: 0`, which is the value the fallback in the transform
   * above is a fallback FOR. Without this rule the state flag flipped, the
   * `aria-expanded` flipped, and the sheet never moved — a tray that said it
   * was open and was not.
   */
  .sf-lb__frame[data-tray="open"] .sf-lb__panel {
    --sf-lb-tray: 0px;
  }

  /* A closed tray must not scroll: it would swallow the swipe that opens it. */
  .sf-lb__frame:not([data-tray="open"]) .sf-lb__panel {
    overflow-y: hidden;
  }

  .sf-lb__frame[data-tray="open"] .sf-lb__panel {
    touch-action: pan-y;
  }

  /*
   * THE HANDLE AND THE CONTROL UNDER IT. The board draws a bare 36x4 handle,
   * and a tray that opens on a tap needs that tap to land on a CONTROL — so the
   * handle is the face of a button that covers the WHOLE PEEK, which is what
   * makes the shut tray tappable anywhere rather than only on a 4px bar.
   *
   * It is laid OVER the head rather than wrapped around it: a `<button>` may
   * only hold phrasing content and the head carries an `<h2>`, so a wrapper
   * would be invalid markup and would fold the photograph's title into the
   * control's accessible name. The button therefore carries the handle and a
   * clipped name of its own, and the head keeps its heading.
   */
  .sf-lb__tray-toggle {
    -webkit-appearance: none;
    appearance: none;
    background: none;
    border: 0;
    color: inherit;
    cursor: pointer;
    /*
     * FLEX, TOP-ALIGNED — the same UA-button gotcha the chips carry, and the
     * same fix. A `<button>` CENTRES its content vertically in its content box
     * whatever its `display` says, so a `block` button 52px tall put the 4px
     * handle 28px down instead of 8: measured, it landed on the title line.
     */
    align-items: center;
    display: flex;
    flex-direction: column;
    font: inherit;
    height: var(--sf-lb-peek);
    justify-content: flex-start;
    left: 0;
    padding: 0;
    position: absolute;
    right: 0;
    text-align: left;
    top: 0;
    /* Over the head's text, under nothing: the peek has no other control in it. */
    z-index: 1;
  }

  .sf-lb__handle {
    background: var(--brand-color-line-mid, #d8d6d0);
    border-radius: 2px;
    display: block;
    flex: none;
    height: 4px;
    margin: 8px 0 0;
    width: 36px;
  }

  /* The peek is one line and its own action: the board drops the eyebrow and
   * shrinks the title to 15/700 with a 12.5px caption. */
  .sf-lb__eyebrow {
    display: none;
  }

  .sf-lb__title {
    font-size: var(--text-body);
    /* THE PEEK IS ONE LINE, whatever the title's length — the closed height is
     * a constant (`--sf-lb-peek`) and a two-line title would push the caption
     * into view under a tray that is supposed to be shut. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Opened, the title is free to use as many lines as it needs. */
  .sf-lb__frame[data-tray="open"] .sf-lb__title {
    overflow: visible;
    white-space: normal;
  }

  .sf-lb__caption {
    font-size: var(--text-caption);
  }

  /* The board's phone close: a translucent circle. It is in the band now rather
   * than over the stage, so it keeps the circle and loses the offset. */
  .sf-lb__close {
    background: rgba(255, 255, 255, 0.14);
    border-radius: var(--radius-pill);
    height: 34px;
    padding: 0;
    width: 34px;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
 * TEMPORARY — THE PREVIEW HARNESS. DELETE THIS BLOCK.
 *
 * `src/app/[brand]/preview/lightbox/` is scaffolding: buttons on one page so
 * the viewer can be opened before the grids that will open it exist. Its rules
 * live here rather than inline so the page stays inside the CSP that forbids
 * everything this site does not need, and so a sweep can see them.
 *
 * DELETE TOGETHER: this block, the preview route, and its `_components/`.
 * Nothing else in this sheet is reachable from that page.
 * ═══════════════════════════════════════════════════════════════════════════ */

.sf-lb-preview {
  padding: 64px 28px;
  text-align: center;
}

.sf-lb-preview__note {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-support);
  line-height: 1.55;
  margin: 0 auto 24px;
  max-width: 620px;
}

.sf-lb-preview__row {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  justify-content: center;
}

/*
 * The stand-in context panel. On a desktop it fills the 320px column the core
 * leaves for a caller; on a phone it is what gives the metadata tray something
 * to open to.
 */
.sf-lb-preview__panel {
  display: flex;
  flex-direction: column;
  gap: 14px;
  text-align: left;
}

.sf-lb-preview__panel-note {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-caption);
  line-height: 1.5;
  margin: 0;
}

.sf-lb-preview__facts {
  border-top: 1px solid var(--brand-color-line-mid, #d8d6d0);
  display: flex;
  flex-direction: column;
  margin: 0;
}

.sf-lb-preview__facts > div {
  border-bottom: 1px solid var(--brand-color-line-mid, #d8d6d0);
  display: flex;
  gap: 12px;
  justify-content: space-between;
  padding: 9px 0;
}

.sf-lb-preview__facts dt {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-caption);
}

.sf-lb-preview__facts dd {
  color: var(--brand-color-ink, #17181a);
  font-size: var(--text-caption);
  font-weight: 600;
  margin: 0;
}

.sf-lb-preview__open {
  background: var(--brand-color-ink, #17181a);
  border: 0;
  border-radius: var(--radius-control);
  color: var(--brand-color-surface, #ffffff);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--text-support);
  font-weight: 600;
  line-height: normal;
  padding: 14px 24px;
}
