/* ═══════════════════════════════════════════════════════════════════════════
   MEDIA — the asset treatment layer (B-079)

   A deck needs photographs, portraits, product captures and partner marks. CSS
   cannot make any of them, and the failure mode this layer exists to prevent is
   a deck that tries: a gradient standing in for a photo, a blurred colour field
   where a screenshot belongs, a generated glyph where a partner's mark belongs.
   In an investor deck that last one is a claim you did not earn.

   So the contract here is narrow and enforced:

     - This layer TREATS a real file — ratio-locked well, focal offset, circular
       mask with a ring, figure/caption, full-bleed. It never SYNTHESISES one.
     - A slot whose file is absent renders a LABELLED placeholder naming the slot
       and the path it wants, and the deck reports it (`pnpm verify`) rather than
       quietly looking finished. `_shared/js/media.js` carries no code path that
       draws a substitute; `ci-validate-media.mjs` fails the build if one appears.

   Class-scoped (`.media-*`) rather than gated behind a body attribute, unlike
   motif/folio/fit: an unfilled slot is a correctness problem, not a style, and a
   deck should not be able to opt out of being told about it.

   GEOMETRY OWNERSHIP: `.arch-logo-wall .arch-mark` (archetypes.css) already owns
   the 3:2 logo well and its `object-fit: contain`. This file defers to it — the
   ratio is declared in exactly one place, or the two layers fight over it.

   NO `clip-path`, NO `mask-image`: both are unmeasured against html2canvas 1.4.1,
   the in-deck PDF engine, and a circular mask that exports as a square is worse
   than no mask. The ring is `border-radius` + stacked `box-shadow`, which the
   motif elevation ramp already proves through that engine. No `color-mix()`
   either — same reason the token layer precomputes flat hex.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  /* Overridable per slot by media.js from the deck's assets.json. */
  --media-ratio: 16 / 9;
  --media-focal: 50% 50%;
  --media-ring-width: 3px;
  --media-ring-gap: 4px;
  /* The floor a round portrait may not shrink below. It exists because the
     `block-size: 100%` on `.media-round` needs a definite basis and does not always get
     one: inside `.media-figure` (block-size auto) the percentage resolves to auto, the
     box has no content, and the circle collapses to 0×0. Measured: without this the
     figure placement renders 0×0 and the flex-row placement 70px; with it, 160 and 174. */
  --media-round-min: 160px;
}

/* ─── 1. WELL — a ratio-locked box that crops rather than distorts ──────── */
/* B-080: the height has to be able to yield, and `width: 100%` never lets it.
 *
 * Found by dogfooding the 39-page reproduction: a 16/10 well on a full-width stage
 * resolves to 1200x750, the slide is 810 tall, and the figure escaped it by 102px on
 * three pages in both bundles at every viewport. The fit pass cannot rescue it either —
 * fit scales type and rhythm, and an `aspect-ratio` height derived from width is
 * neither, so the slide reported `overflow` while every element on it "fitted".
 *
 * So the well fills its column and takes a HEIGHT CAP from the space it is given:
 * `max-block-size` bounded by the figure, which is itself bounded by the stage.
 *
 * ROUND-6 CORRECTION. The first version of this also carried `margin-inline: auto`, and
 * that one line undid the whole thing. A cross-axis `auto` margin on a flex item absorbs
 * the free space and defeats `align-self: stretch` (Flexbox §8.1), so `inline-size: auto`
 * resolved to fit-content instead of stretch and the well collapsed to the shrink-to-fit
 * width of the placeholder text. Measured on the shipped gallery decks: 1200x675 became
 * 151x85 in a 1200px column, and the unresolved-slot placeholder — the thing three
 * paragraphs below promise reads as unfinished from the back of a room — became a 151px
 * thumbnail. The overflow it was written to fix was gone, because a 151px box overflows
 * nothing. That is the same trap the round-5 review named: the fix was verified in one
 * dimension and broke another.
 *
 * THE TRADE, stated because it is a real one: when the height cap binds, the declared
 * ratio yields rather than the stage. A 16/10 slot on a short stage renders shorter than
 * 16/10. `object-fit: cover` plus the declared focal point still decide what survives the
 * crop, so the slot degrades to a tighter frame instead of pushing content off the slide.
 * `--media-ratio` is therefore the ratio a slot ASKS for, not a guarantee — and
 * `--media-max-h` is the escape hatch for a deck that wants its own cap.
 */
.media-well {
  position: relative;
  display: block;
  inline-size: auto;
  block-size: auto;
  max-inline-size: 100%;
  max-block-size: var(--media-max-h, 100%);
  aspect-ratio: var(--media-ratio);
  /* In a flex column a `min-height: auto` item refuses to shrink below its content, and
     an aspect-ratio box counts its resolved height as content. Without this the cap
     above is ignored and the well overflows exactly as before. */
  min-block-size: 0;
  flex: 0 1 auto;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--bg-secondary);
}

.media-well > img,
.media-well > video {
  display: block;
  width: 100%;
  height: 100%;
  /* `cover` fills the well and lets the focal point decide what survives the
     crop — a portrait cropped at the centre loses the face often enough that the
     offset has to be declarable per slot. */
  object-fit: cover;
  object-position: var(--media-focal);
}

/* An author who wants the whole frame visible (a screenshot, a diagram export)
   asks for it; `cover` is the default because a photograph almost always wants
   the crop. */
.media-well[data-fit="contain"] > img,
.media-well[data-fit="contain"] > video {
  object-fit: contain;
  background: var(--bg-secondary);
}

/* ─── 2. ROUND — portrait mask with a ring and a gap ────────────────────── */
.media-round {
  --media-ratio: 1 / 1;
  border-radius: 50%;
  /* Two stacked shadows: the first paints the gap in the page colour, the second
     the ring outside it. Flat tokens only, so html2canvas composites it. */
  box-shadow:
    0 0 0 var(--media-ring-gap) var(--bg),
    0 0 0 calc(var(--media-ring-gap) + var(--media-ring-width)) var(--accent);
  /* The ring lives outside the box, so a round portrait needs room for it. */
  margin: calc(var(--media-ring-gap) + var(--media-ring-width));

  /* ROUND-7: `border-radius: 50%` on a box that is not square paints an ELLIPSE, and
     `aspect-ratio: 1/1` alone does not make it square here. `.slide-content` is a flex
     COLUMN, so the default `align-items: stretch` makes the well's INLINE size definite
     at the full column width; the ratio then asks for an equal height, and
     `max-block-size: 100%` (on `.media-well`) clamps only the block axis — a clamp on
     one axis does not transfer back to the other. Measured on the shipped template at
     1440×810 with a heading: 1186×570, skew 2.08. Crowded at 1280×720: 1138×76,
     skew 14.97. The mask stayed a circle only in the arrangement nobody ships.
     `align-self: center` + `inline-size: auto` removes the stretch so the inline size is
     derived, and `block-size: 100%` makes the BLOCK axis the definite one, which is the
     axis the stage actually bounds. Then the ratio transfers the other way and the box
     is square by construction. Verified across five placements (direct child of
     `.slide-content` — the one `_templates/media/figure.html` mandates — plus figure,
     grid column, block wrapper, flex row) at two viewports. The plain block wrapper is
     a circle but can still overflow a short column: that is pre-existing (the shipped
     rule overflows it identically) and is why `ci-validate-media.mjs` asserts skew in
     all five and containment in the four the shipped CSS already contained. */
  align-self: center;
  inline-size: auto;
  block-size: 100%;
  min-block-size: var(--media-round-min);
}

/* The floor is for parents whose block size is INDEFINITE. A direct child of
   `.slide-content` never has that problem — the column's height is definite — and there
   the floor stops being harmless: on a crowded slide the well can no longer shrink below
   160px, so it overflows the stage and `overflow: hidden` crops the ring off. Measured at
   1280×720 with six paragraphs above it: 160×160 spilling with the floor, 76×76 whole
   without. A cramped-but-complete portrait is an author's content problem, visible on the
   slide; a cropped ring is a rendering defect that looks deliberate. */
.slide-content > .media-round {
  min-block-size: 0;
}

.media-round > img { border-radius: 50%; }

/* ─── 3. FIGURE — the well plus its attribution ─────────────────────────── */
.media-figure {
  /* B-117: inline via the class, block via :where() at zero specificity. This was
     `margin: 0`, and its block-axis zero out-cascaded base.css's column-rhythm rule
     (equal 0,1,0 specificity, later source order) — the ninth site of the exact
     mechanism B-116 swept in diagrams.css, found by round 12: a media figure followed
     by a sibling sat flush at 0px. :where keeps beating the UA's 1em figure margins
     (author origin wins regardless of specificity) while losing to the column rule
     for direct children, which is the order the cascade should have had all along. */
  margin-inline: 0;
  display: flex;
  flex-direction: column;
  gap: 10.8px;
  /* B-080: the figure is what bounds the well, so it has to be bounded itself. Without
     these two the well's `max-block-size: 100%` resolves against an unbounded parent and
     means nothing — which is how a 750px well ended up on an 810px stage under a heading
     and above a caption. */
  min-block-size: 0;
  max-block-size: 100%;
}

/* B-117: the block-axis zero, at (0,0,0) — see the note in .media-figure above. */
:where(.media-figure) { margin-block: 0; }

.media-figure > figcaption {
  font-size: var(--small-size);
  color: var(--muted);
  line-height: var(--leading-body);
}

/* ─── 4. BLEED — the well spanning the stage, ignoring content inset ────── */
/* ROUND-7: this used to be the `margin-left: 50%` + `transform: translateX(-50%)` pair,
   and only the first half of it survived. base.css owns `transform` on every direct child
   of `.slide-content` for the reveal — `.slide .slide-content > *` (0,0,2) and
   `.slide[data-active="true"] .slide-content > *:not([data-fragment])` (0,0,4) both beat
   `.media-bleed` (0,0,1), and the second one resolves to `translateY(0)`. So the push-out
   applied and the pull-back never did: measured at 1440×810, the bleed box ran
   720..2160 on a 0..1440 stage — it began at the centre of the slide and half of it was
   off the edge. The one-property form needs no transform at all, so nothing can outrank
   it, and it survives html2canvas, which composites margins but is unreliable on
   transforms. A rule that depends on winning the cascade against another layer's
   animation is a rule that stops working the day that animation is written.

   B-106 (ROUND-9): the ROUND-7 fix corrected the layout box (0..1440) but never
   reached the screen. `.slide-content` carries `overflow: hidden` + `max-width:
   min(1200px, 90vw)` (base.css:256-260); a bleed well that is a child of
   `.slide-content` has layout 0..1440 and paint 120..1320 — identical to a plain
   `.media-well`. `getBoundingClientRect()` returns the layout box, so the ROUND-7
   gate reported exit 0 while the display showed a crop.

   The fix is a placement rule, not a CSS rule: `.media-bleed` must be a DIRECT
   CHILD OF `.slide`, not of `.slide-content`. `.slide` has no `overflow: hidden`,
   so nothing clips the 100vw box. `margin-inline: calc(50% - 960px)` still works
   relative to `.slide`'s own padding box — measured 1440x810 / 1280x720 / 1920x1080
   / 768x432: paint 0..1439, 0..1279, 0..1919, 0..767 respectively. The ROUND-7
   gate has been replaced with a paint-sweep assertion (`elementFromPoint` along
   the bleed's vertical centre) that actually sees the screen — layout numbers are
   no longer enough. */
.media-bleed {
  --media-ratio: 21 / 9;
  width: 1920px;
  max-width: 1920px;
  margin-inline: calc(50% - 960px);
  border-radius: 0;
}

/* ─── 5. MISSING — the labelled placeholder ─────────────────────────────────
   Deliberately plain and deliberately legible. It is not a design element: it is
   a message to the author, and it should read as unfinished at a glance from the
   back of a room. Nothing here paints an image, a gradient or a blur — the gate
   asserts that, because the temptation to make this "look nicer" is exactly how
   a placeholder turns into a substitute. */
.media-slot-missing {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  width: 100%;
  height: 100%;
  padding: 16px;
  text-align: center;
  /* A logo well is 3:2 and can be small. Without this the three lines push past the
     dashed border and the placeholder stops reading as a contained, deliberate box —
     measured on the shipped logo wall, where the slot id sat on the border. */
  overflow: hidden;
  border: 2px dashed var(--card-border);
  border-radius: inherit;
  background: var(--bg-secondary);
  color: var(--muted);
  font-size: var(--small-size);
  line-height: 1.35;
}

.media-slot-missing .media-slot-label {
  font-weight: 700;
  color: var(--fg);
}

.media-slot-missing .media-slot-path {
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  /* `anywhere` rather than `break-all`: a long path still breaks, but ordinary prose
     breaks at word boundaries. break-all rendered "(no file declared in assets.json)"
     as "declare / d in assets.jso / n)". */
  overflow-wrap: anywhere;
  font-size: 0.86em;
}

.media-slot-missing .media-slot-id {
  font-size: 0.8em;
  /* No opacity here. `opacity: 0.85` on this line measured 4.01:1 in the light theme —
     the archetype gate's opacity-aware pass caught it, which is the same trap that
     shipped a cover label at 4.14:1 once already. `--muted` is measured; dimming a
     measured token un-measures it. */
}

/* A round slot's placeholder keeps the circle so the layout does not jump when
   the real portrait lands. */
.media-round .media-slot-missing { border-radius: 50%; }

@media (prefers-reduced-motion: reduce) {
  .media-well > img,
  .media-well > video { transition: none; }
}
