/**
 * The memorial page's layout and type (T13.02) — the entrance, the Index, and
 * a piece page.
 *
 * PROJECT-AUTHORED, like `components.css`, `scale.css` and `gallery-color.css`,
 * and unlike `tokens/*.css`, which are verbatim upstream and re-imported by
 * overwriting (design/README.md). Nothing here may be moved into a token file.
 *
 * WHY IT IS A FILE RATHER THAN A `<style>` BLOCK
 * ----------------------------------------------
 * `tell.html` and `interview.html` each keep their page CSS inline, and that is
 * right for them: each is one page and its styles are used nowhere else. This
 * page is two documents — `index.html` and `piece.html` — that must not drift
 * apart, so the shared half is a file they both link. It is also cached across
 * the walk from the Index into a piece and back, which is the navigation this
 * page is actually for.
 *
 * COLOUR IS NOT IN THIS FILE
 * --------------------------
 * Every colour here is a token (`--surface-page`, `--text-said`, `--rule`, …),
 * so both rooms are already correct: `tokens/colors.css` defines `:root`
 * (paper) and `[data-theme="night"]`, and the page follows whichever is
 * active. The three things that are genuinely per-room — the contributor's
 * star, the entrance ground, and `--gal-secondary` (the second text tone,
 * which the paper room deliberately does not have) — live in
 * `gallery-color.css`, which loads after this file. Adding a hex literal here
 * is the mistake this split exists to prevent.
 *
 * TAP TARGETS
 * -----------
 * Every link a visitor is meant to hit — the doors, an Index entry, the way
 * back out of a piece — is at least 44px tall (WCAG 2.2 SC 2.5.8 asks 24; 44
 * is the number this project uses, because the reader is holding a phone at a
 * funeral). `--gal-target` is that floor in one place; a control that opts out
 * of it is a bug, not a design.
 */

:root {
  --gal-measure: 34rem;
  --gal-target: 44px;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  background: var(--surface-page);
  color: var(--text-said);
  font-family: var(--font-display);
  font-size: 1.05rem;
  line-height: var(--lh-said);
  -webkit-font-smoothing: antialiased;
  /* The sky is `100vw` and `100vw` INCLUDES a classic scrollbar, so on Windows
     and Linux the edge-to-edge picture is a few pixels wider than the space it
     has and the whole page gains a horizontal scrollbar. macOS overlay
     scrollbars hide this completely, which is why it is worth writing down.
     `clip` rather than `hidden` on purpose: `hidden` would make the body a
     scroll container and quietly break `position: sticky` on anything inside
     it — the test-deployment band, for one. Ignored by browsers that do not
     know `clip`, which leaves them exactly where they are today. */
  overflow-x: clip;
}

.gal-page {
  max-width: var(--gal-measure);
  margin: 0 auto;
  padding: 0 var(--s-5);
}

/* A hairline, and the only rule the page draws. */
.gal-rule {
  width: 3.5rem;
  height: 1px;
  border: 0;
  background: var(--rule);
  margin: var(--s-10) auto;
}

/* ---------- the entrance ---------- */

.gal-entrance {
  min-height: 88vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  text-align: center;
  padding: var(--s-9) 0;
}

.gal-entrance__dates {
  font-family: var(--font-mono);
  font-size: var(--meta);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--gal-secondary);
  margin-bottom: var(--s-6);
}

.gal-entrance__name {
  font-weight: var(--weight-said);
  font-size: clamp(2.1rem, 7vw, 3.2rem);
  line-height: 1.15;
}

.gal-entrance__line {
  margin-top: var(--s-7);
  color: var(--gal-secondary);
  font-size: var(--said-prior);
}

/* The two lines the deployment fills, on a deployment that has filled neither.
   `index.html` is engine now (BUG-011) and carries `{{SUBJECT}}` and
   `{{DATES}}`; a memorial with no `subjectName` and no `dates` in its config
   would otherwise show their margins as a gap above the invitation.

   `:empty` rather than a server-side decision about whether to emit the
   element, because that would put this page's class names in `server.ts` — the
   markup stays in the page, and the page says what an empty line looks like.
   Note `:empty` matches an element with no child nodes at all, which is exactly
   what an unfilled marker leaves behind: the substitution removes the text
   node rather than blanking it. */
.gal-entrance__dates:empty,
.gal-entrance__name:empty {
  display: none;
}

/* A 44px target, and a link that says where it goes to a screen reader. */
.gal-entrance__down {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--gal-target);
  min-width: var(--gal-target);
  margin: var(--s-9) auto 0;
  color: var(--gal-secondary);
  text-decoration: none;
  font-size: 1.3rem;
}

/* ---------- the stage: the sky behind, the invitation on top ----------
 *
 * `.gal-eulogy`, `.gal-invite__q`, `.gal-shuffle` and `.gal-invite__later` were
 * all here. The page between the entrance and the stories is now one screen —
 * a sentence and two doors over the sky — and the rules are DELETED rather
 * than left unused. The shuffle's whole design was "hidden until the script
 * that drives it says it works", and a rule like that outliving its script is
 * exactly how a control that does nothing comes back.
 *
 * ONE FULL SCREEN, EDGE TO EDGE, WITHOUT A TRANSFORM. `margin-inline:
 * calc(50% - 50vw)` breaks the measure and lands the box on the viewport edges
 * — and it is chosen over the `left: 50%; transform: translateX(-50%)` trick
 * this file used before for one specific reason: **a transformed ancestor
 * becomes the containing block for `position: fixed` descendants.** The live
 * sky below is fixed to the viewport, and under a transform it would silently
 * be fixed to the stage instead. Margins have no such side effect.
 *
 * `svh`, NOT `vh`: `100vh` on a phone is the height with the browser chrome
 * RETRACTED, so a `vh`-tall stage is taller than the screen the arrow lands you
 * on and its bottom is cut off on arrival. `svh` is the smallest viewport — the
 * one you actually get. The `vh` line beneath is the fallback where `svh` is
 * unknown, which is the old behaviour and not a failure. */

.gal-stage {
  position: relative;
  margin-inline: calc(50% - 50vw);
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: var(--s-9) var(--s-5);
  /* T13.11: the sky takes taps and horizontal drags, and the PAGE keeps the
     vertical one. The single most important line for the visitor this page is
     written for — a full-screen picture that swallowed the scroll gesture
     would trap a thumb on its way down to the stories, on a phone, in a car
     park. `pan-y` gives the browser every vertical drag before a script sees
     it, so scrolling can never be something the sky has to remember not to
     break. */
  touch-action: pan-y;
}

/* THE BACKDROP, AND IT IS INERT.
 *
 * Arriving here the sky is scenery: dimmed so the sentence on top of it is the
 * only thing asking to be read, and taking NO pointer at all. `pointer-events:
 * none` is the load-bearing half. A dimmed background that quietly accepted
 * taps would be the worst of both — somebody reaching for a button gets a
 * story card they never asked for, and nothing on screen ever said the
 * background was clickable. The picture becomes the subject only when a person
 * says so, which is what `sky-live` is.
 *
 * The opacity is a room decision and lives in `gallery-color.css`; the default
 * here is the neutral one, so a missing colour file dims rather than blinds. */
.gal-stage__sky {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: var(--gal-backdrop, .5);
  transition: opacity var(--dur-fade) var(--ease-calm);
}

.gal-stage__over {
  position: relative;
  z-index: 1;
  max-width: var(--gal-measure);
  text-align: center;
  transition: opacity var(--dur-settle) var(--ease-calm);
}

.gal-invite__lead {
  color: var(--text-said);
  font-size: var(--said-prior);
  margin-bottom: var(--s-8);
}

.gal-doors {
  display: flex;
  gap: var(--s-4);
  justify-content: center;
  flex-wrap: wrap;
}

.gal-door {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--gal-target);
  padding: 0 var(--s-7);
  border: var(--hairline) solid var(--control-border);
  color: var(--text-said);
  text-decoration: none;
  font-family: var(--font-ui);
  font-size: var(--note-sm);
  letter-spacing: .04em;
}

/* THE TELLING DOOR IS THE EMPHASIZED ONE, and `[data-door]` is what identifies
   it — no modifier class. That attribute is already on exactly the doors the
   server fills with a card-aware URL, so it is the same fact stated once
   instead of a second class that has to be kept in step with it. It is also
   what several tests match on, and adding a class beside `gal-door` in the
   attribute is enough to break a `class="gal-door"` regex — which is precisely
   the silent-guard failure T13.11 already paid for once. */
.gal-door[data-door] { border-color: var(--text-said); }

/* NOTHING TO EXPLORE IS NOT A DOOR THAT GOES NOWHERE. `renderIndex()` emits
   nothing at 0 pieces, so on day one at the printed address `#stories` does not
   exist and this anchor would scroll to exactly where it already is. Written
   as `:not(:has())` rather than as the positive rule on purpose: a browser
   without `:has()` drops the whole selector and shows the door, which is
   today's behaviour, where the positive form would hide a working control on
   every page that has stories. */
.gal-page:not(:has(.gal-index)) #explore { display: none; }

/* ---------- the live sky (the "explore" state) ----------
 *
 * One class on the root, set by the enhancement script, and everything below
 * follows from it. Without scripting the class is never set, `#explore` is a
 * plain anchor to the Index, and none of this exists — which is the whole of
 * BUG-004's rule: the page may put the sky first and may never need JavaScript
 * to reach a story. */

/* The picture, promoted. Fixed to the VIEWPORT, which is only true because
   `.gal-stage` carries no transform (see above). */
:root.sky-live .gal-stage__sky {
  position: fixed;
  inset: 0;
  z-index: 40;
  pointer-events: auto;
  opacity: 1;
  touch-action: none;
}

/* Everything else steps back: the entrance, the invitation over the sky, the
   Index and the footer. `visibility` rides along with `opacity` so a faded
   control is not still tabbable — a link nobody can see is a worse trap on a
   keyboard than on a thumb. */
:root.sky-live .gal-entrance,
:root.sky-live .gal-stage__over,
:root.sky-live .gal-index,
:root.sky-live .gal-rule,
:root.sky-live .gal-foot {
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-settle) var(--ease-calm), visibility 0s linear var(--dur-settle);
}

/* The page does not scroll behind a full-screen picture. */
:root.sky-live body { overflow: hidden; }

/* `[hidden]` is the accessible truth and this rule is what makes it visual:
   `display: inline-flex` below would otherwise beat the user-agent's
   `[hidden] { display: none }`, and the way out of a mode nobody is in would
   sit on the entrance. Written as its own rule rather than relying on the
   attribute alone, because that is exactly the failure it exists to stop. */
.gal-stage__exit[hidden] { display: none; }

.gal-stage__exit {
  position: fixed;
  top: var(--s-5);
  right: var(--s-5);
  z-index: 41;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--gal-target);
  min-width: var(--gal-target);
  padding: 0 var(--s-5);
  background: var(--surface-page);
  border: var(--hairline) solid var(--control-border);
  color: var(--text-said);
  font-family: var(--font-ui);
  font-size: var(--note-sm);
  letter-spacing: .04em;
  cursor: pointer;
}

/* ---------- the sky (T13.04) ----------
 *
 * A box for a canvas, and nothing else — the picture inside it is drawn by
 * `web/sky.js` and its colour comes from `gallery-color.css` and from the
 * artifact.
 *
 * `:root.has-sky` IS THE WHOLE RULE. The class is added by `index.html`'s head
 * script, before first paint, if and only if a 2D canvas can actually be
 * obtained:
 *
 *   no class  ── scripting off, or a browser that cannot draw. `display: none`:
 *                no empty pane behind the invitation, and no canvas element
 *                sitting there absorbing nothing.
 *   class     ── the box fills its backdrop from the first paint.
 *
 * WHAT THIS CLASS NO LONGER HAS TO DO, and it is worth recording because the
 * old rule was written almost entirely about it: it used to reserve HEIGHT.
 * The sky was a 26rem band in the page flow, so unhiding it late shoved the
 * Index down under the thumb of somebody already reading, and the class existed
 * to buy that space up front. It cannot happen now — `.gal-stage` owns a full
 * screen because the INVITATION needs one, and the sky is absolutely positioned
 * inside it. A sky that arrives late, or never, moves nothing. The layout-shift
 * argument is gone; the "don't paint an empty pane" one remains, which is why
 * the class does. `sky.js` still takes it back off if it decides not to draw.
 *
 * IT BREAKS THE PAGE'S MEASURE ON PURPOSE, AND NOW IT BREAKS IT COMPLETELY.
 * `--gal-measure` is 34rem because a column of text is unreadable wider than
 * that; the sky is not text. It was a 68rem-clamped band under the invitation
 * and is now the full screen behind it. The clamp is what actually went:
 * capping the width made the sky a letterboxed panel framed by page ground on
 * both sides, which reads as a widget embedded in a document. Unclamped, the
 * picture is the room and the document is what you scroll into.
 *
 * `min(width, height)` IS THE CONSTELLATION'S RADIUS (`sky-layout.js` projects
 * into a circle inscribed in the box), so the two dimensions do not contribute
 * equally. On a phone the width has always been the binding one and the extra
 * height buys breathing room rather than size. On a desktop the old 26rem
 * height was the cap, and lifting it is where the picture actually grows —
 * which is also why `sky-draw.js` had to raise `starPx` and `lineWidth` in the
 * same pass: the same marks spread over more screen read as a thinner picture,
 * not a bigger one. */

.gal-sky { display: none; }

:root.has-sky .gal-sky {
  display: block;
  position: absolute;
  inset: 0;
}

.gal-sky canvas {
  display: block;
  width: 100%;
  height: 100%;
  /* THE RESTING CURSOR, and it is the stylesheet's rather than the script's on
     purpose: it is true the moment the canvas exists, before any pointer has
     moved over it to tell `sky.js` what to say. The script overrides inline
     with `pointer` over a light and `grabbing` mid-drag; this is the third
     state and the only one nothing has to happen to reach.

     It costs nothing in the backdrop state, where the sky is `pointer-events:
     none` and never hit-tested — so "you can pick this up" is offered exactly
     where it is true. */
  cursor: grab;
  /* A drag across a canvas otherwise starts selecting the page's text, which
     leaves a blue smear over the invitation and ends the gesture early. */
  user-select: none;
  -webkit-user-select: none;
}

/* ---------- the Index ---------- */

.gal-index__title {
  font-family: var(--font-mono);
  font-size: var(--label);
  font-weight: 400;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--gal-secondary);
  text-align: center;
  margin-bottom: var(--s-8);
}

.gal-idx { list-style: none; }

.gal-idx__entry + .gal-idx__entry { border-top: var(--hairline) solid var(--rule); }

.gal-idx__link {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: var(--s-4);
  row-gap: var(--s-1);
  align-items: baseline;
  min-height: var(--gal-target);
  padding: var(--s-5) 0;
  text-decoration: none;
  color: inherit;
}

/* The contributor's mark. One hue per person everywhere they appear; the room
   chooses which of the two resolved colours is drawn (gallery-color.css). */
.gal-star {
  width: 9px;
  height: 9px;
  border-radius: var(--r-pill);
  align-self: center;
  flex: none;
}

.gal-idx__title {
  grid-column: 2;
  font-size: var(--said-sm);
  line-height: 1.25;
}

.gal-idx__who {
  grid-column: 2;
  color: var(--gal-secondary);
  font-size: var(--said-prior);
}

.gal-idx__rel::before { content: " · "; }

.gal-idx__meta {
  grid-column: 2;
  font-family: var(--font-mono);
  font-size: var(--meta);
  letter-spacing: var(--track-meta);
  text-transform: uppercase;
  color: var(--gal-secondary);
}

.gal-idx__dot { padding: 0 .4em; }

/* ---------- a piece ---------- */

.gal-nav { padding-top: var(--s-5); }

.gal-back {
  display: inline-flex;
  align-items: center;
  min-height: var(--gal-target);
  font-family: var(--font-mono);
  font-size: var(--meta);
  letter-spacing: var(--track-meta);
  text-transform: uppercase;
  color: var(--gal-secondary);
  text-decoration: none;
}

.gal-piece { padding: var(--s-6) 0 var(--s-9); }

.gal-piece__title {
  font-weight: var(--weight-said);
  font-size: clamp(1.7rem, 6vw, 2.4rem);
  line-height: 1.2;
}

.gal-piece__who {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-top: var(--s-5);
  font-size: var(--said-prior);
  color: var(--gal-secondary);
}

/* The flex gap already sets the space; the separator adds only the mark. */
.gal-piece__rel::before { content: "·"; padding-right: .35em; }

.gal-piece__meta {
  margin-top: var(--s-2);
  font-family: var(--font-mono);
  font-size: var(--meta);
  letter-spacing: var(--track-meta);
  text-transform: uppercase;
  color: var(--gal-secondary);
}

.gal-piece__synopsis {
  margin-top: var(--s-6);
  color: var(--gal-secondary);
  font-size: var(--said-prior);
}

.gal-piece__audio {
  display: block;
  width: 100%;
  margin-top: var(--s-7);
}

.gal-piece__said { margin-top: var(--s-9); }

.gal-piece__said-title {
  font-family: var(--font-mono);
  font-size: var(--label);
  font-weight: 400;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--gal-secondary);
  margin-bottom: var(--s-6);
}

.gal-said-list { list-style: none; }

/* The largest thing on the page, because it is what somebody said. */
.gal-said {
  font-size: var(--said-sm);
  line-height: var(--lh-said);
  margin-bottom: var(--s-6);
}

/* ---------- the synced player (T13.03) ----------
   `<story-audio>` (web/story-audio.js) renders its own <audio> and its own
   <ol class="story-audio__turns">, and its doc comment leaves what
   "highlighted" looks like to the host page. This is that decision, and it is
   the ONLY thing this page contributes to playback — there is no second
   player and no second sync rule anywhere in this repo.

   The element replaces the server's word list, so these rules restate
   `.gal-said`'s type rather than inheriting it: the same words, the same size,
   in either room. */

story-audio { display: block; }

/* The control stays in reach while a long story scrolls. `--surface-page`
   behind it, or the words would run underneath it. */
story-audio > audio {
  display: block;
  width: 100%;
  position: sticky;
  top: 0;
  z-index: 1;
  padding: var(--s-3) 0;
  background: var(--surface-page);
}

.story-audio__turns {
  list-style: none;
  margin-top: var(--s-6);
}

/* A turn is a 44px target like every other control here, and the left rule is
   the slot the highlight arrives in — reserved at rest, so nothing shifts when
   playback reaches it. */
.story-audio__turn {
  font-size: var(--said-sm);
  line-height: var(--lh-said);
  margin-bottom: var(--s-6);
  padding: var(--s-2) 0 var(--s-2) var(--s-5);
  min-height: var(--gal-target);
  border-left: 2px solid transparent;
  cursor: pointer;
}

/* No timing behind it, so it is text and not a control (`data-unsynced` is set
   by the element for a span the artifact could not time). */
.story-audio__turn[data-unsynced] { cursor: default; }

/* The sentence being said right now: it sits on the raised ground, and its
   left rule takes the narrator's own colour. INK AND GROUND, never a glow —
   the paper room is an engraving. The colour half is in `gallery-color.css`
   with the star it matches, because only that file knows which of a
   contributor's two resolved values the room is in. */
.story-audio__turn[data-playing] { background: var(--surface-raised); }

/* The focusable control is the inner span, not the `<li>` — the row keeps its
   `listitem` role so the transcript still announces how many sentences it has
   (`web/story-audio.js`). So the ring goes on the span, and the 44px target
   above stays on the row that is actually clicked. */
.story-audio__text:focus-visible {
  outline: 1px solid var(--accent-new);
  outline-offset: 3px;
}

/* ---------- follow the thread ---------- */

.gal-thread { margin-top: var(--s-9); }

.gal-thread__heading {
  font-family: var(--font-mono);
  font-size: var(--label);
  font-weight: 400;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--gal-secondary);
  margin-bottom: var(--s-6);
}

.gal-thread__list { list-style: none; }

.gal-thread__item + .gal-thread__item { border-top: var(--hairline) solid var(--rule); }

/* The Index entry's grid, deliberately: a thread row and a catalog row lead to
   the same place and should not look like two different kinds of thing. */
.gal-thread__link {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: var(--s-4);
  row-gap: var(--s-1);
  align-items: baseline;
  min-height: var(--gal-target);
  padding: var(--s-5) 0;
  text-decoration: none;
  color: inherit;
}

.gal-thread__title {
  grid-column: 2;
  font-size: var(--said-sm);
  line-height: 1.25;
}

.gal-thread__who {
  grid-column: 2;
  color: var(--gal-secondary);
  font-size: var(--said-prior);
}

.gal-thread__rel::before { content: " · "; }
.gal-thread__dot { padding: 0 .4em; }

/* WHY this piece is being offered. Every row carries one, because a link that
   does not say what it shares is an assertion that two stories belong
   together — and nothing here may assert anything. */
/* THE REASON IS THE ACCENT, and it is the same amber as `linked through` for
   the same meaning: this is the way through. A row that named its reason in the
   secondary tone made "the smart fridge" look like more metadata beside a
   duration, when it is the only part of the row that says why the row exists.
   One colour, one meaning — and it stays a LABEL, never a star, so it cannot be
   read as a contributor's own light (T13.01 reserves amber for exactly that
   distinction). */
.gal-thread__why {
  grid-column: 2;
  font-family: var(--font-mono);
  font-size: var(--meta);
  letter-spacing: var(--track-meta);
  color: var(--accent-new);
}

.gal-thread__why::before { content: "— "; }

/* ---------- the footer bar ----------
 *
 * The building the memorial is in, not the memorial. Small, quiet, one hairline
 * away from the page, and carrying nothing about him — which is what lets it be
 * identical on every deployment.
 *
 * `flex-wrap` and `column-gap`/`row-gap` rather than a fixed row: three items
 * fit on one line at a phone width and the signature drops below on a narrow
 * one, without a media query and without a separator character that would be
 * left dangling at the end of a wrapped line. */

.gal-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  column-gap: var(--s-7);
  row-gap: var(--s-2);
  /* Out of the 34rem measure, like the stage, so the rule above it runs the
     width of the screen and the three items sit on one line. A bar clipped to
     a text column is a paragraph with a border. Margins rather than a
     transform, for the reason `.gal-stage` gives. */
  margin-inline: calc(50% - 50vw);
  margin-top: var(--s-9);
  padding: var(--s-4) var(--s-5) var(--s-6);
  border-top: var(--hairline) solid var(--rule);
  font-family: var(--font-ui);
  font-size: var(--note-sm);
  color: var(--gal-secondary);
}

/* 44px, like every other target on this page (`--gal-target`) — the reader is
   still holding a phone down here. The height comes from the line box rather
   than from a `min-height`, because a 44px-tall link in a 40px-tall bar makes
   the bar tall; `padding` on the block gives the same reachable box. */
.gal-foot__link {
  display: inline-flex;
  align-items: center;
  min-height: var(--gal-target);
  color: var(--gal-secondary);
  text-decoration: none;
  border-bottom: var(--hairline) solid var(--rule);
}

.gal-foot__link:hover { color: var(--text-said); }

.gal-foot__mark {
  font-family: var(--font-mono);
  font-size: var(--meta);
  letter-spacing: var(--track-meta);
  opacity: .75;
}

/* ---------- the card over the sky ----------
 *
 * A `<dialog>`, so most of this is colour and measure rather than mechanics:
 * the top layer, the focus trap, Escape and `display: none` when closed are the
 * user agent's. What is left is what the card should look like standing in
 * front of a constellation.
 *
 * THE BLUR IS ON THE BACKDROP, WHICH IS WHY IT WORKS AT ALL. The sky is a fixed
 * canvas at `z-index: 40`; a blur applied to *it* would blur the picture the
 * card stands in front of and would have to be undone on close. `::backdrop`
 * sits in the top layer between the page and the dialog, so one
 * `backdrop-filter` softens everything behind the card — canvas included —
 * costs nothing when the card is shut, and needs no z-index argument.
 *
 * A browser without `backdrop-filter` gets the tint and no blur, which is the
 * same design one step quieter and not worth a fallback. */

.gal-card {
  /* The user agent centres a modal dialog with `margin: auto`, and the
     `* { margin: 0 }` reset at the top of this file takes that away — so the
     card lands in the top-left corner and nothing says why. Put back
     explicitly, because it is a rule this stylesheet removed rather than one
     the browser lacks. */
  margin: auto;
  width: min(46rem, 100% - 2rem);
  max-height: min(82svh, 44rem);
  padding: var(--s-7) var(--s-7) var(--s-6);
  border: var(--hairline) solid var(--rule);
  background: var(--surface-raised);
  color: var(--text-said);
  overflow-y: auto;
  /* Its own scroll container, so a long telling scrolls INSIDE the card and the
     page beneath never moves. `overscroll-behavior` is what stops a thumb that
     reaches the end of a transcript from scrolling the document behind the
     dialog — the same class of trap `touch-action: pan-y` avoids on the stage,
     one layer up. */
  overscroll-behavior: contain;
}

.gal-card::backdrop {
  background: color-mix(in oklab, var(--surface-page) 72%, transparent);
  backdrop-filter: blur(14px);
}

/* The card's markup is the piece page's own, so everything inside it is already
   styled. The focus ring is the exception: on a page the card is a landing
   point, and in a dialog it is the whole content. */
.gal-card__body .gal-piece { outline: none; }

.gal-card__hint {
  margin-top: var(--s-6);
  padding-top: var(--s-5);
  border-top: var(--hairline) solid var(--rule);
  font-family: var(--font-mono);
  font-size: var(--meta);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--gal-secondary);
  text-align: center;
}

/* `linked through …` — the named things this telling shares with another one.
   The labels carry the page's one accent, and this is the only place it is
   used: `--accent-new` is amber, and T13.01 reserves amber and moss as the
   design's two signal colours specifically so that no contributor's generated
   hue can be mistaken for one. A shared entity is exactly that kind of signal —
   it means "there is a way through here" — and it is a LABEL rather than a
   star, so it cannot be read as a light. Freshness still gets none of it
   (T13.05: brightness within the narrator's own hue, never a shift toward
   amber). */
.gal-piece__through {
  margin-bottom: var(--s-6);
  font-size: var(--note-sm);
  color: var(--gal-secondary);
}

.gal-through { color: var(--accent-new); }

/* ---------- the Index, and where it is ----------
 *
 * Hidden when scripting works, and ONLY then. `.js` is added by the head script
 * before first paint, so a browser with scripting off never matches this and
 * simply gets the list.
 *
 * THAT IS BUG-004 KEPT, NOT WORKED AROUND. With scripting the stories are
 * reached by pressing "explore the stories", which puts the sky in the
 * foreground; without it, "explore" is the plain `#stories` anchor it is in the
 * markup and the list is right there. The page may put the sky first and may
 * never NEED JavaScript to reach a story.
 *
 * `display: none` rather than a removal, because the anchors are load-bearing:
 * T13.11's tap-to-open finds this section's `[data-piece]` anchor and follows
 * it, so the destination, the card token and the history entry are the Index's
 * by construction rather than by a second URL builder. A hidden element's
 * `.click()` still navigates.
 *
 * **The cost, named rather than left to be discovered:** `display: none` takes
 * the list out of the accessibility tree too, so a screen-reader user WITH
 * scripting loses the list and has only the canvas. That is the open edge of
 * this design (`D71`) and the fix is a real one — an accessible
 * list inside the live sky — not a CSS trick here. */
.js .gal-index { display: none; }

/* ---------- focus, always visible ---------- */

a:focus-visible,
button:focus-visible,
audio:focus-visible {
  outline: 1px solid var(--accent-new);
  outline-offset: 3px;
}

/* …except the card itself, which takes focus on arrival so a walk down the
   thread continues at the story rather than at the top of the document. It is
   a landing point and not a control — nothing is operated on it — so it draws
   no ring; every control inside it still draws its own. A ring around the
   whole article reads as "this is selected", which is not what happened. */
.gal-piece:focus,
.gal-piece:focus-visible { outline: none; }

/* ---------- the reveal ----------
   Gated on `.js`, so with scripting off the page is simply visible. The
   earlier version of this page set `opacity: 0` unconditionally and relied on
   an IntersectionObserver to undo it: with JavaScript disabled the whole
   memorial rendered blank. */
.js .gal-reveal {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity var(--dur-fade) var(--ease-calm), transform var(--dur-fade) var(--ease-calm);
}

.js .gal-reveal.is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .js .gal-reveal { opacity: 1; transform: none; transition: none; }
}

/* ---------- wider than a phone ---------- */

@media (min-width: 861px) {
  .gal-page { padding: 0 var(--s-7); }
  .gal-idx__link { column-gap: var(--s-5); }
}
