/* OD-LAYOUT-PRIMITIVES v1 — structure only: display, flex/grid, overflow, wrapping, ratio.
   Put this @layer first; product CSS outside the layer always wins. */
@layer od-layout {
  :where(.od-stack,.od-row,.od-row-top,.od-cluster,.od-grid,.od-field,.od-stat,.od-cell,.od-tile) > :where(*) { min-width: 0; }

  /* containers */
  .od-stack   { display: flex; flex-direction: column; gap: var(--od-gap, 8px); }
  .od-row     { display: flex; align-items: center; gap: var(--od-gap, 8px); }
  .od-row-top { display: flex; align-items: flex-start; gap: var(--od-gap, 8px); }
  .od-cluster { display: flex; flex-wrap: wrap; align-items: center; gap: var(--od-gap, 8px); }
  .od-fill    { flex: 1 1 0; min-width: 0; }
  .od-fixed   { flex: none; }
  .od-grid    { display: grid; gap: var(--od-gap, 12px); grid-template-columns: repeat(var(--od-cols, 3), minmax(0, 1fr)); }

  /* stacked information: each piece is its own line — never sibling inline spans */
  .od-stat, .od-field, .od-cell { display: grid; gap: var(--od-gap, 2px); }
  :where(.od-stat,.od-field,.od-cell) > :where(*) { display: block; }
  .od-tile { display: grid; grid-template-rows: auto 1fr; }
  :where(.od-tile) > :where(*) { display: block; }

  /* media keeps its intrinsic ratio by default; never a distorted or cropped
     cover by accident. height:auto lets the ratio win over an <img height="…">
     attribute. Cropping to a uniform box is an explicit opt-in: set --od-ratio
     AND add .od-media-cover together (per the core rule, only where a crop is
     deliberate — tile thumbs, decorative fills — never on full-frame content). */
  .od-media { display: block; width: 100%; height: auto; aspect-ratio: var(--od-ratio, auto); }
  .od-media-cover { object-fit: cover; }

  /* data text only — authored copy is rewritten to its budget instead */
  .od-truncate { display: block; max-width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
  .od-clamp-2, .od-clamp-3 { display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }
  .od-clamp-2 { -webkit-line-clamp: 2; }
  .od-clamp-3 { -webkit-line-clamp: 3; }
  .od-lines-2 { min-height: calc(2 * 1.4em); }   /* block-level reservation for a shared baseline */
  .od-nowrap  { white-space: nowrap; }           /* 数字+单位 · 价格+起 · 日期+星期 */
  .od-keep    { word-break: keep-all; overflow-wrap: anywhere; } /* CJK labels break at spaces / <wbr> first */

  /* screen skeleton (optional): bars take their own space, the middle scrolls — no padding guessed for chrome */
  .od-screen { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; height: 100%; }
  .od-scroll { overflow-y: auto; overscroll-behavior: contain; min-height: 0; }

  /* horizontal rail: the next item peeks at the edge; trailing room survives overflow */
  .od-rail { display: flex; gap: var(--od-gap, 8px); overflow-x: auto; scroll-snap-type: x proximity; scrollbar-width: none;
             padding-inline: var(--od-rail-pad, 16px); scroll-padding-inline: var(--od-rail-pad, 16px); }
  .od-rail::-webkit-scrollbar { display: none; }
  :where(.od-rail) > :where(*) { flex: none; scroll-snap-align: start; }
  :where(.od-rail) > :where(:last-child) { margin-inline-end: var(--od-rail-pad, 16px); }

  .od-spacer { flex: none; visibility: hidden; pointer-events: none; }
  .od-touch  { min-width: 44px; min-height: 44px; }
}
/* /OD-LAYOUT-PRIMITIVES v1 */

/* =========================================================================
   Lucas Baret — site layout
   Layout only. Every colour, size, space and duration below resolves to a
   token from colors_and_type.css. There is not one colour literal in this
   file, and no rule reads --accent directly: the surface-bound
   --accent-on-light / --accent-on-ground pair is the only accent entry.
   ========================================================================= */

*, *::before, *::after { box-sizing: border-box; }

html { height: 100%; }
body { min-height: 100%; display: flex; flex-direction: column; }

/* Keyboard users land here first. Visible only when focused. */
.skip-link {
  position: absolute; left: var(--space-2); top: calc(var(--space-2) * -6);
  z-index: 10;
  background: var(--bg); color: var(--fg);
  border: var(--border-width) solid var(--fg); border-radius: var(--radius);
  padding: 0 var(--space-2); min-height: var(--control-height);
  display: inline-flex; align-items: center; text-decoration: none;
  transition: top var(--duration) var(--ease);
}
.skip-link:focus { top: var(--space-2); }

/* Shared measure for every full-width band. */
.wrap { width: 100%; max-width: 1040px; margin-inline: auto; padding-inline: var(--space-4); }

/* --- Navigation. Same pattern, same place, on all three pages. ----------- */
.nav { border-bottom: var(--border-width) solid var(--border); }
.nav .wrap {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); flex-wrap: wrap;
  padding-block: var(--space-2);
}
/* The mark sits beside the wordmark rather than replacing it. Shipping the
   letters as an <img> of an SVG would drop them to Georgia, because a
   standalone SVG cannot reach the page's self-hosted Newsreader — so the
   name stays live text and only the glyph is drawn. */
.nav__wordmark {
  font-family: var(--font-display); font-size: var(--text-lg); font-weight: 500;
  letter-spacing: -0.015em; color: var(--fg); text-decoration: none;
  min-height: var(--control-height); display: inline-flex; align-items: center;
  gap: var(--space-1);
}
/* currentColor, so one element serves the Ink pages and the Ground ones. */
.nav__mark { flex: none; }
.nav__links { display: flex; gap: var(--space-3); list-style: none; margin: 0; padding: 0; flex-wrap: wrap; }
.nav__links a {
  color: var(--fg); text-decoration: none; font-size: var(--text-sm);
  min-height: var(--control-height); display: inline-flex; align-items: center;
  border-bottom: var(--border-width) solid transparent;
}
/* Hover darkens the rule. It never lightens the label toward --muted. */
.nav__links a:hover { border-bottom-color: var(--fg); }
/* The current page is marked by weight and a persistent rule, not by colour alone. */
.nav__links a[aria-current="page"] { font-weight: 700; border-bottom-color: var(--fg); }

/* --- The full-bleed Ground band every page carries ----------------------- */
.band-ground { width: 100%; }

.hero .wrap {
  /* minmax(0, …) on both: a bare fr track keeps a min-content floor too. */
  display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: var(--space-5);
  align-items: center;
  padding-block: var(--space-7);
}
.hero__title {
  font-family: var(--font-display); font-size: var(--text-4xl); font-weight: 500;
  line-height: var(--leading-tight); letter-spacing: -0.02em; margin: 0;
  text-wrap: balance;
}
.hero__lead { font-size: var(--text-lg); line-height: 1.45; margin: var(--space-3) 0 0; max-width: 42ch; }
.hero__meta { margin: var(--space-3) 0 0; }

/* Content image: the whole frame at its own 2048x1365 ratio, never cropped. */
.figure { margin: 0; }
.figure img { border-radius: var(--radius); }
.figure figcaption { margin-top: var(--space-1); }

/* Compact Ground head used by the EPK and newsletter pages. */
.pagehead .wrap { padding-block: var(--space-6); }
.pagehead__title {
  font-family: var(--font-display); font-size: var(--text-2xl); font-weight: 500;
  line-height: var(--leading-snug); letter-spacing: -0.015em; margin: 0;
}
.pagehead__lead { font-size: var(--text-lg); line-height: 1.45; margin: var(--space-2) 0 0; }
.pagehead__meta { margin: var(--space-2) 0 0; }

/* --- The light document canvas ------------------------------------------ */
.main { flex: 1 1 auto; }
/* minmax(0, 1fr), not the implicit auto column a bare `display: grid` gives.
   An auto track never shrinks below the min-content of what it holds, so the
   widest child sets the width of every sibling: the photograph deck carries
   about 3100px of cards, which stretched each 16:9 slot to roughly 1030px and
   made the whole page scroll sideways. minmax(0, 1fr) removes that floor, and
   the deck goes back to scrolling inside itself as it was meant to. */
.main > .wrap {
  padding-block: var(--space-6);
  display: grid; grid-template-columns: minmax(0, 1fr);
  gap: var(--space-6);
}
/* A grid item's own automatic minimum is min-content too — the track cap above
   is only half the guard. */
.main > .wrap > * { min-width: 0; }
.section__title {
  font-family: var(--font-display); font-size: var(--text-xl); font-weight: 500;
  line-height: var(--leading-snug); margin: 0 0 var(--space-2);
}
.section p { margin: 0; }
.section p + p { margin-top: var(--space-2); }
.section > * + .meta { margin-top: var(--space-3); }

/* In-page index on the EPK. Text links, no arrows, no dot-joined string. */
.index { list-style: none; margin: var(--space-2) 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--space-3); }
.index a { color: var(--accent-on-light); min-height: var(--control-height); display: inline-flex; align-items: center; }
.on-ground .index a { color: var(--accent-on-ground); }

/* Inline links inside running copy. */
.link { color: var(--accent-on-light); }
.on-ground .link { color: var(--accent-on-ground); }

/* --- Actions ------------------------------------------------------------- */
.actions { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-3); }
.actions a:not(.btn) {
  color: var(--accent-on-light);
  min-height: var(--control-height); display: inline-flex; align-items: center;
}
.on-ground .actions a:not(.btn) { color: var(--accent-on-ground); }
a.btn { text-decoration: none; }

/* --- Tables: the mono register. Set lists, durations and keys. ----------- */
.table { width: 100%; border-collapse: collapse; margin-top: var(--space-2); }
.table th, .table td {
  text-align: left; padding: 10px 12px;
  border-bottom: var(--border-width) solid var(--border);
  vertical-align: top;
}
.table thead th { font-size: var(--text-sm); font-weight: 500; }
.table tbody tr:last-child td { border-bottom: 0; }
.table .num, .table .mono { font-family: var(--font-mono); font-size: var(--text-xs); }
/* An em dash is the fallback for an empty value. It is not a missing style. */
.table .empty { font-family: var(--font-mono); font-size: var(--text-xs); opacity: 0.72; }

/* --- Panels. A 1px rule, never a shadow, never nested. ------------------- */
.panel { background: var(--surface); border: var(--border-width) solid var(--border); border-radius: var(--radius); padding: var(--space-3); box-shadow: var(--elevation); }
.panel__title { font-family: var(--font-display); font-size: var(--text-lg); font-weight: 500; line-height: var(--leading-snug); margin: 0 0 var(--space-1); }
/* min(280px, 100%) rather than a bare 280px: a plain pixel floor in minmax()
   is a promise the track will never be narrower than that, so on a container
   under 280px the grid stops shrinking and pushes the page wider instead. */
.panels { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); gap: var(--space-2); margin-top: var(--space-2); }

/* Media slots. Real 16:9 space is reserved so nothing shifts when an embed
   is dropped in; the disabled control states plainly that none is linked. */
.slot__frame {
  aspect-ratio: 16 / 9;
  border: var(--border-width) solid var(--border); border-radius: var(--radius);
  background: var(--surface);
  display: grid; place-items: center; gap: var(--space-2);
  padding: var(--space-3);
  margin-bottom: var(--space-2);
}
.slot__frame .btn { pointer-events: none; }

/* --- The newsletter form ------------------------------------------------- */
.signup { width: 100%; max-width: 30rem; margin-top: var(--space-4); }
.field { display: grid; gap: var(--space-1); }
/* A persistently visible label. Never a placeholder standing in for one. */
.field label { font-size: var(--text-sm); font-weight: 500; }
.field__req { font-weight: 400; opacity: 0.72; }
.on-ground .field__req { opacity: 1; color: var(--muted); }
.field input,
.field textarea {
  width: 100%; min-height: var(--control-height);
  padding: 0 var(--space-2);
  font: inherit; font-size: var(--text-md);
  color: var(--fg); background: var(--bg);
  border: var(--border-width) solid var(--border); border-radius: var(--radius);
  transition: border-color var(--duration) var(--ease);
}
.on-ground .field input,
.on-ground .field textarea { background: var(--surface); }
.field textarea {
  min-height: 8rem;
  padding-block: 12px;
  line-height: var(--leading-body);
  resize: vertical;
}
.field input:hover,
.field textarea:hover { border-color: var(--fg); }
/* Error is carried by the message and a heavier rule, never by colour alone. */
.field :where(input, textarea)[aria-invalid="true"] { border-width: 2px; border-color: var(--fg); }
.field__error { font-size: var(--text-sm); font-weight: 700; margin: 0; }
.field__error:empty { display: none; }
.signup .field + .field { margin-top: var(--space-3); }
.signup .actions { margin-top: var(--space-3); }

/* Raised only when more than one field fails at once, so a single error still
   behaves exactly as it does on the newsletter page. */
.form__summary {
  border: var(--border-width) solid var(--fg);
  border-radius: var(--radius);
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
}
.form__summary-title { font-weight: 700; margin: 0; }
.form__summary ul { margin: var(--space-1) 0 0; padding-left: var(--space-3); }
.form__summary a { color: var(--accent-on-light); }
.on-ground .form__summary a { color: var(--accent-on-ground); }
.signup__consent { margin-top: var(--space-3); max-width: 42ch; }
.btn[aria-busy="true"] { opacity: 0.72; }

.confirm { max-width: 30rem; margin-top: var(--space-4); }
.confirm__title { font-family: var(--font-display); font-size: var(--text-xl); font-weight: 500; line-height: var(--leading-snug); margin: 0 0 var(--space-1); }
.confirm p { margin: 0; }
.confirm p + .meta { margin-top: var(--space-2); }
[hidden] { display: none !important; }

/* A section reached by a fragment link lands with air above it rather than
   flush against the top of the viewport, and clears the sticky nav on the
   home page. The section itself is the anchor, so this needs no :target. */
.section[id] { scroll-margin-top: var(--space-4); }
.film-page .section[id] { scroll-margin-top: calc(var(--control-height) + var(--space-4)); }
/* The contact section takes focus when it is the landing target, so keyboard
   and screen-reader users arrive at the form rather than at the document top.
   It is not a control, so it gets no outline of its own. */
.section[tabindex="-1"]:focus { outline: none; }

/* --- Footer -------------------------------------------------------------- */
.foot { border-top: var(--border-width) solid var(--border); }
.foot .wrap {
  padding-block: var(--space-3);
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--space-2);
}
.foot__links { display: flex; gap: var(--space-3); list-style: none; margin: 0; padding: 0; flex-wrap: wrap; }
.foot__links a { color: var(--fg); font-size: var(--text-sm); min-height: var(--control-height); display: inline-flex; align-items: center; }
.on-ground .foot { border-top-color: var(--border); }

/* --- Small screens: reorganised, not scaled down ------------------------- */
@media (max-width: 860px) {
  .wrap { padding-inline: var(--space-3); }
  .hero .wrap { grid-template-columns: minmax(0, 1fr); gap: var(--space-4); padding-block: var(--space-5); }
  /* The title steps down the scale rather than the page being shrunk. */
  .hero__title { font-size: var(--text-2xl); }
  .hero__lead { font-size: var(--text-md); max-width: none; }
  .pagehead .wrap { padding-block: var(--space-5); }
  .pagehead__title { font-size: var(--text-xl); }
  .pagehead__lead { font-size: var(--text-md); }
  .main > .wrap { padding-block: var(--space-5); gap: var(--space-5); }
  .nav .wrap { gap: var(--space-2); }
  .nav__links { gap: var(--space-2); }
  .foot .wrap { flex-direction: column; align-items: flex-start; }
}

/* The repertoire table keeps five columns; below 560px it scrolls on its own
   axis inside a labelled region rather than forcing the page sideways. */
.table-scroll { overflow-x: auto; }

/* --- Full-page film ------------------------------------------------------
   The 128-frame sequence is the whole page rather than one section: a fixed
   full-viewport canvas behind every block, with frame index mapped to scroll
   progress across the entire document. Everything here is scoped to
   .film-page, which only index.html carries — the EPK and newsletter pages
   are untouched by this block.

   Without JavaScript, and under prefers-reduced-motion, the layer stays as
   authored: the midpoint still as a fixed background, content scrolling over
   it normally, and none of the other 127 frames requested. */

.film {
  position: fixed;
  inset: 0;
  z-index: 0;
  background: var(--bg);   /* Ground underlay while the first frame loads */
}

/* The backing store stays at the frames' measured 1600x900 and object-fit
   does the cover crop, so drawImage is always a 1:1 blit and the layer needs
   no resize maths. Cover is right here: this is a deliberate full-bleed
   atmospheric fill, not a content image being cropped. */
.film__still,
.film__canvas {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Every real element rides above the film, in normal document flow.
   The skip link is deliberately absent: it is already position: absolute with
   z-index 10, well clear of the film at z-index 0. Listing it here re-declared
   position: relative at equal specificity (:where() adds none) and, being the
   later rule, won it — which put the link back in flow, where its left offset
   of var(--space-2) pushed the document 16px past the viewport. */
.film-page > :where(.nav, .hero, .main, .foot) { position: relative; z-index: 1; }

/* The nav stays reachable for the whole scroll, not only at the top. Sticky
   keeps it in flow, so it never covers the hero and no compensating padding
   is owed to the content beneath it. */
.nav--sticky { position: sticky; top: 0; z-index: 2; }

.film-page {
  /* Scroll room for the scrub. The page is the timeline, so the air between
     blocks is what gives 128 frames a readable pace — one value to change. */
  --film-air: 28svh;

  /* The scrim: flat, never a gradient. This is a contrast floor, not
     decoration — stage lighting makes frame brightness vary, and body text
     has to hold its ratio across all 128 of them. The literal is the
     fallback; color-mix ties the same value back to the token where it is
     supported. Both resolve to Ground at 55%, because .film-page is also
     .on-ground, where --bg is Ground. */
  --scrim: rgba(24, 20, 15, 0.55);
  --scrim: color-mix(in srgb, var(--bg) 55%, transparent);
}

.scrim {
  background: var(--scrim);
  border-radius: var(--radius);
  padding: var(--space-4);
}

/* The two full-bleed bars take the same fill without the radius, so they read
   as chrome rather than as another panel. */
.film-page .nav--sticky,
.film-page .foot { background: var(--scrim); }

/* Viewport-scale air between blocks: over a running film, the gaps are part
   of the composition rather than dead space. */
.film-page .hero .wrap { padding-block: var(--film-air); }
.film-page .main > .wrap { padding-block: var(--film-air); gap: var(--film-air); }

/* One column: the hero is a single statement block on the film, held to a
   readable measure rather than stretched across the full wrap. */
.film-page .hero .wrap { grid-template-columns: minmax(0, 36rem); }

@media (max-width: 860px) {
  .film-page { --film-air: 18svh; }
  .scrim { padding: var(--space-3); }
}

/* --- Press photographs ---------------------------------------------------
   Full-frame figures at their own measured ratios. Cropping a press photo to
   tidy the grid is the one thing this section must not do, so cells are
   top-aligned: a portrait frame makes its own cell taller instead of
   stretching or cropping its neighbours. Separation is the 1px rule, as
   everywhere else — no shadow under any of them. */
/* --- The press set as a deck you scroll through --------------------------
   It was a grid. It is a fanned deck now: the frames overlap and lean like
   prints spread across a table, and whichever one reaches the middle of the
   rail straightens and lifts clear.

   Scroll position drives it through a per-frame view timeline, so there is no
   scroll listener and no JavaScript — the compositor runs it. Every frame
   keeps its measured ratio: a fixed height with automatic width turns mixed
   portrait and landscape into different widths, which is what a real pile of
   prints looks like, and never a crop. */
.fan {
  /* The height every frame shares. It scales with the viewport: held at a
     flat 20rem the deck was taller than a phone screen while each print was
     too narrow to read. */
  --fan-h: clamp(11rem, 30vw, 20rem);
  /* The widest real card: a 3:2 frame at --fan-h. The end padding is derived
     from it so the first and last prints can actually reach the centre. */
  --fan-w: calc(var(--fan-h) * 1.5);
  --fan-overlap: 5rem;     /* how far each print tucks under the next */
  --fan-lean: 7deg;        /* how far the deck opens */
  --fan-lift: var(--space-3);

  display: flex;
  gap: var(--space-3);
  overflow-x: auto;
  overscroll-behavior-inline: contain;
  scroll-snap-type: x mandatory;
  margin-top: var(--space-3);
  /* Room for the lean, not a round number: rotating the widest card (a 3:2
     frame, so 1.5 x --fan-h) by --fan-lean about its bottom edge lifts its top
     corners by about (1.5 / 2) * sin(7deg) = 0.11 of the height. Setting
     overflow-x above forces overflow-y to compute to auto as well, so anything
     that pokes out is not merely clipped — it adds a scrollbar inside the rail. */
  padding-block: calc(var(--space-2) + var(--fan-h) * 0.11);
  /* Enough room either side that the first and last frame can reach centre. */
  padding-inline: max(var(--space-2), calc(50% - var(--fan-w) / 2));
  scroll-padding-inline: max(var(--space-2), calc(50% - var(--fan-w) / 2));
}
.fan > .photo {
  flex: none;
  scroll-snap-align: center;
  position: relative;
  /* Width comes from the frame's own ratio, so the caption can never widen
     the card past its picture. */
  width: calc(var(--fan-h) * var(--ratio));
  transform-origin: bottom center;
}
.fan > .photo img { width: 100%; height: var(--fan-h); }
/* The fan is drawn from three measurements, written per frame by
   assets/fan.js: --p, how far the frame sits from the centre of the rail
   (0 centred, 1 at the edge); --dir, which side it sits on; and --z, its
   place in the pile. The script only measures. Every angle, lift and
   stacking rule still lives here.

   The tidier way to do this is a per-frame view-timeline, which needs no
   script at all, and that is what shipped first behind
   @supports (animation-timeline: view()). Scroll-driven animations only
   reached Safari in 26 and are still behind a flag in Firefox, so for a real
   share of readers the guard correctly did nothing and the deck arrived as a
   flat rail. Measuring by hand works everywhere.

   With reduced motion asked for the script never runs, and the deck stays a
   plain spaced rail that still snaps and still scrolls. */
.fan[data-fan="on"] { gap: 0; }
.fan[data-fan="on"] > .photo {
  margin-inline-start: calc(var(--fan-overlap) * -1);
  /* Pivoting on the bottom edge is what makes this read as cards laid on a
     table rather than tiles being rotated. */
  transform:
    rotate(calc(var(--fan-lean) * var(--dir, 0) * var(--p, 1)))
    translateY(calc(var(--fan-lift) * (var(--p, 1) - 1)))
    scale(calc(1 - 0.03 * var(--p, 1)));
  /* z rides the same measurement, so the upright frame is the one in front. */
  z-index: var(--z, 1);
}
.fan[data-fan="on"] > .photo:first-child { margin-inline-start: 0; }

/* Declared after the fan so it outranks the measured z on equal specificity:
   a frame reached by keyboard comes to the front whatever the scroll says. */
.fan > .photo:focus-within { z-index: 200; }

.photo { margin: 0; }
.photo img {
  display: block;
  width: 100%;
  height: auto;              /* the width/height attributes carry the ratio */
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius);
}
.photo figcaption { margin-top: var(--space-1); }
.photo__credit { display: block; }

/* The picture sits beside the text and stays there: the two columns shrink
   together rather than the picture dropping below at the first squeeze.

   Flex rather than grid, because grid tracks never wrap — they overflow — so a
   grid needs a breakpoint to decide when to stack, and any number picked for it
   is a guess. Here the two min-widths decide: 18rem is the narrowest the prose
   or a form stays readable, 12rem the narrowest the photograph stays a
   photograph. Below their sum plus the gap — about 512px — the line wraps and
   the picture goes underneath, which on a phone is the only usable answer. */
.with-photo {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  align-items: flex-start;
}
.with-photo > :first-child { flex: 1 1 22rem; min-width: min(18rem, 100%); }
.with-photo > :last-child  { flex: 0 1 var(--photo-col, 24rem); min-width: min(12rem, 100%); }
/* Neither side stretches to match the other: a photograph stretched to a text
   column's height is a cropped photograph. Same reason the press deck starts
   its frames rather than stretching them. */
@media (max-width: 860px) {
  .with-photo { gap: var(--space-3); }
}

/* Pixel dimensions are data, so they take the mono register. */
.photo__file {
  display: inline-flex; align-items: center;
  min-height: var(--control-height);
  font-family: var(--font-mono); font-size: var(--text-xs);
  color: var(--accent-on-light);
}
.on-ground .photo__file { color: var(--accent-on-ground); }
