/* ============================================================
   CASE STUDY — page-specific layout for project case studies.
   The reusable template; Cynoia is instance #1 (Projects/cynoia.html).

   Loaded AFTER tokens.css + styles.css. Tokens only (Protocol 3),
   self-contained: it borrows the home/About vocabulary (.container,
   .section, .eyebrow, .btn, .split register, .site-footer, [data-reveal])
   but defines its own components so the home + About builds stay
   literally untouched. Same discipline as about.css.

   Narrative spine (Baha's call): a full arc, not just two acts —
   00 Overview · 01 Setup · 02 Confrontation (dark) · 03 Resolution ·
   04 Results · 05 Gallery. The Confrontation act flips to the dark
   zone (the token swap is the site's theming mechanism) so the tension
   reads, then Resolution returns to light and the page resolves.

   Zebrikitou, 2026-06-29.
   ============================================================ */

/* --- Section rhythm ----------------------------------------------------------
   styles.css pads .section a full --space-section top AND bottom, so adjacent
   case acts would sit 2x apart. Halve each side (same fix as about.css) so every
   act-to-act gap resolves to exactly one --space-section, dark band included.
   Case-page scope only; the home build keeps its own rhythm. */
.section { padding-block: calc(var(--space-section) / 2); }

/* A section that CONTINUES the act above it instead of opening a new one. Between two
   .section blocks the gap is the previous section's bottom pad PLUS this one's top pad =
   one full act gap (~--space-section, the widest gap on the page). When a section is the
   PAYOFF of the act above it (Results — "as a result we produced reels consistently" — is
   the result of the templates act), that full act gap disconnects it. Dropping this
   section's top pad leaves only the previous act's bottom pad (~--space-section/2), which
   lands at the within-act flow rhythm, so the two read as one continuous thought.
   Reversible: remove the class to restore the full act gap. */
.section--continues { padding-top: 0; }

/* Vertical rhythm inside an act: one flow gap between every beat. A flex column
   so the gap is uniform (Müller-Brockmann) and survives margin-collapse quirks.
   Put .case-flow on the .container of each act (not the hero). */
.case-flow {
  /* Tokenised in V4.16 so .case-rule can subtract it by name instead of restating
     the formula. One source, two consumers. */
  --case-flow-gap: clamp(var(--space-48), 6vw, var(--space-96));
  display: flex;
  flex-direction: column;
  gap: var(--case-flow-gap);
}

/* ============================================================
   TEXT REGISTERS — three voices, reused everywhere
   beat title (.case-para__title) > statement (.case-statement) > body (.case-body)
   plus the section eyebrow (act label) and .display-sm (act opener).
   ============================================================ */
/* V4.17 (Baha's call): .case-confront__statement JOINS this register. It was set
   to .display-sm — a size of its own, sitting between the hero and everything else
   — which made the 02 act opener a fourth heading level that appeared exactly once
   on the page. A level that appears once is not a level, it is an exception.
   It also used to cap itself at --measure-statement (36ch). That was a fake cap:
   36ch at --fs-display-sm rendered ~684px, WIDER than the half it was supposed to
   be sitting in. .case-half measures it now, from the markup.

   V4.17d shipped this group at --fs-display-sm, collapsing the page to ONE heading
   register, and Baha reverted it on sight. Two registers stay. Do not try it again
   without looking at the page: at display-sm these headings are ~39px against ~31,
   and the case is mostly headings — so every act opener started competing with the
   hero it is supposed to be subordinate to, and the page read as a stack of claims
   rather than one claim and its case. The size was never the complaint it looked
   like; whatever "too small" was about, it was not this register. */
.case-para__title,
.case-beat__title,
.case-confront__statement {
  font-size: var(--fs-h1);
  line-height: var(--lh-heading);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;
}
/* The punchy stance line — same register as .project__desc / .about-decades__statement. */
.case-statement {
  font-size: var(--fs-h2);
  line-height: var(--lh-heading);
  font-weight: var(--weight-regular);
  text-wrap: balance;
  max-width: var(--measure-case-text);   /* shared with .case-body — same rendered width */
}
/* Explanatory paragraph register. Same cap as .case-statement (was 52ch, which is a
   different rendered width because ch tracks font-size and this register is smaller). */
.case-body { font-size: var(--fs-lead); line-height: var(--lh-body); max-width: var(--measure-case-text); }
.case-body + .case-body { margin-top: var(--space-16); }
/* A muted "inspired by" credit set in type, never as a pasted third-party logo. */
.case-credit { font-size: var(--fs-small); color: var(--fg-muted); }
.case-credit strong { color: var(--fg); font-weight: var(--weight-medium); }

/* A generic vertical group (title + body + notes + figure), tighter than .case-flow. */
/* The generic vertical group. V4.17b: the gap is a knob now, defaulting to the 24
   it always was, so nothing that already uses .case-beat moves. The knob exists
   because a beat that stacks two TEXT GROUPS needs more than a beat that stacks a
   title over a figure: inside a .case-para__text everything runs on 16, so a 24
   between two of them is a 1.5:1 step, and a heading that opens a new thought at
   1.5:1 reads as belonging to the paragraph above it as much as to the body under
   it. 48 is 3:1 against that 16 and unambiguous, and it stays at or under the flow
   gap (48-96), so a new thought never out-shouts a new row. Passed inline, same
   convention as --ratio: per-instance knobs live in the markup, defaults live here. */
.case-beat { display: flex; flex-direction: column; gap: var(--case-beat-gap, var(--space-24)); }

/* ============================================================
   THE BIPARTITE ROW (V4.15 — Baha's structural call)
   Every two-pane row on this page is now the SAME row: two equal halves with one
   channel between them. This replaces THREE splits that used to coexist and
   fought each other — the hero's 12-col "5 + void 2 + 5", the golden
   --split-golden / --split-golden-rev pair on .case-para, and .case-para--grid's
   --fig-start knob. One shape, one gap, no exceptions: the reader learns the
   centre line once on the hero and every row down the page confirms it, which is
   the entire argument for a bipartite layout (Müller-Brockmann: the grid is the
   discipline — but only if it is ONE grid).

   The channel is --gap-half, NOT --grid-gutter. See tokens.css: 48px between two
   ~660px panes is a seam, not a split.

   .case-gallery is deliberately NOT in this list — it is an auto-fit row of 3+
   uniform frames (reels, prints), not a two-pane relationship, so it keeps the
   grid gutter and does not pretend to honour the centre line.
   ============================================================ */
.case-hero__grid,
.case-tone,
.case-para,
.case-notes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--gap-half);
}
/* Load-bearing, same reason as .split in styles.css: lets a half shrink below its
   content's intrinsic min-width instead of blowing the row out. */
.case-hero__grid > *,
.case-tone > *,
.case-para > *,
.case-notes > * { min-width: 0; }

/* ---- .case-half — one half, standing alone (V4.16, Baha's call) -------------
   The page is bipartite, so a block that stands alone in the flow has exactly two
   honest widths: the full field, or ONE half. Anything between is a third measure
   the reader has no reference for — it starts at the left margin like a half and
   ends nowhere, so it reads as a half that missed.

   The half is expressed as the row's own arithmetic, never as a number: whatever
   --gap-half becomes, this stays exactly the width of a .case-para slot. A lone
   statement therefore starts on the same left edge and ends on the same centre
   line as every two-pane row above it, which is the only reason the centre line
   is worth having.

   CONSTRAINT: the 100% resolves against the parent, so this belongs on a direct
   child of the full-width .case-flow container. Nest it inside a half and it
   halves again.
   --------------------------------------------------------------------------- */
.case-half {
  display: flex;
  flex-direction: column;
  gap: var(--space-16);
  max-width: calc((100% - var(--gap-half)) / 2);
}

/* ---- .case-rule — the act boundary (V4.16, Baha's call) ---------------------
   A hairline across the full field, under the hero and above the meta stack. It
   closes the header and opens the arc: everything above it is the claim, everything
   below it is the case. That join had no marker, so the hero's banner and the meta
   stack read as one continuous fall of blocks.

   Read the history before you touch this. V4.15 RETIRED a rule in this same
   position, and this is not that rule coming back. The old one spanned cols 1-5
   only, so it was a half-line stopping dead at the channel with nothing over 8-12,
   and the "01 — Setup" eyebrow directly beneath it already marked the act, so it
   said the same thing twice. Both objections are now spent: this one spans the
   whole field, and the "01" eyebrow moved down to head the problem it names, so
   nothing else marks this boundary. Position is not the argument. What a rule
   spans, and whether a label already says it, is the argument.

   <hr> because a thematic break is exactly what this IS, so the element carries
   the meaning and the CSS only removes the browser's default bevel.
   Prefer pure space instead? Delete the <hr>. That is the entire revert.

   The rule HANGS with the block it opens rather than floating one flow gap from
   each side, which made it read as a divider between two equals instead of as the
   head of what follows. V4.16 hung it at 24. V4.17 (Baha's call) opens that to 64:
   24 was right about the hierarchy and wrong about the air. It stapled the label to
   the hairline, and a boundary the reader is meant to CROSS needs room on the far
   side of it. Against the ~128 above it (see .case-hero + .section), 64 still reads
   2:1 as belonging below, which is the only thing that asymmetry has to say.
   The negative is derived, never magic: --case-flow-gap subtracts the flow's own gap
   by name, so this stays exactly 64 no matter what the flow is dialled to. The flow
   gap is a clamp, so a plain margin here would drift at every width.
   Dial 48 ⇄ 96, but keep it well under the number above or the rule floats again.
   --------------------------------------------------------------------------- */
.case-rule {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 0 0 calc(var(--space-64) - var(--case-flow-gap)) 0;
}

/* V4.23 (Baha's call): a MOVEMENT break inside an act. The second challenge (speed vs
   excellence) opens a new movement after the content-registers answer, so it needs a
   section's worth of air above it, not the uniform flow gap, or the two read as one
   continuous fall of beats (Baha: "a space... distinguish the two sections apart"). Pure
   space, no .case-rule hairline: the bold "Then a second challenge" heading claims the gap
   and marks the boundary itself, so a line would say it twice. Derived, never magic: the
   flow already lays a --case-flow-gap above this child, so adding (--space-section minus
   that gap) lands the TOTAL separation at exactly --space-section, the between-sections
   measure, at every width. */
.case-movement { margin-top: calc(var(--space-section) - var(--case-flow-gap)); }

/* ============================================================
   00 HEADER / OVERVIEW (.case-hero) — light
   V4.15: this row no longer rhymes with the home .project row (text 1-5, void
   6-7, banner 8-12). Baha took the case template bipartite, so the hero is a
   straight half-and-half and the page states its centre line here. The home page
   keeps the canon; these are two deliberate structures now, not a drift to
   reconcile. See "THE BIPARTITE ROW" above.
   ============================================================ */
/* V4.16 (Baha's call): was clamp(48px, 9vw, 128px). The banner is the tallest thing
   on the page and .case-hero__grid centres the claim against it, so the kicker already
   starts a long way down the row on its own. Stacking 128px of page padding on top of
   that centring put "Cynoia" near the middle of the screen with nothing over it,
   which reads as a gap rather than as a top margin. Cut to ~72 so the topbar, the
   kicker and the banner's top edge resolve as one opening block.
   The 9vw was the real fault: it grew the void exactly on the wide screens where the
   banner is already tallest. 5vw makes it shrink relative to the row instead.
   V4.20 (Baha's call): pulled from clamp(32,5vw,72) so the whole hero rides higher and
   the meta stack's first item clears the fold. This lift and the tease air below are one
   decision in two places, same as they always were. */
.case-hero { padding-top: clamp(24px, 3vw, 44px); padding-bottom: 0; }

/* The air under the banner. It is the whole tease mechanism, so read this and
   .case-rule's margin together — they are one decision in two places.
   The tease is a FOLD statement: Baha wants the banner, the rule, and "Role /
   Marketing Designer" on the opening screen, and the timeline a scroll away. Which
   means this number is not decoration, it is what pushes the meta stack down until
   only its first item survives the fold.
   V4.20 (Baha's call): pulled to clamp(32, 4vw, 60), was clamp(64, 9vw, 128). The 128
   over-corrected. It did not "under-tease" as the old note here feared, it teased
   NOTHING: it pushed the whole meta stack, ROLE included, clean off a laptop fold, so
   the opening screen was the banner over an empty page with nothing to scroll toward.
   Two faults compounded and both are fixed now. (1) Position: this air plus the banner's
   own height put ROLE at ~793px, below an ~812 fold; 60 lifts it to ~700. (2) Reveal:
   ROLE sat on a data-reveal block that only un-hides once its top crosses 92% of the
   viewport, so a SHORTER laptop hid it harder, which is backwards for a tease. The tease
   elements (the rule and the meta stack) are no longer reveal-gated, they paint on load;
   see cynoia.html. So this number only has to place ROLE, not also out-run the observer.
   Still well under the act gap (--space-section/2 x 2 = 160 at full width): the arc is
   glimpsed here, not arrived at. Every other act keeps the section rhythm, met mid-scroll
   with nothing to prove. Structural, not #setup, so the next case page inherits the tease.
   CAVEAT, unchanged and honest: static CSS cannot lock a fold. This lands ROLE above the
   cut with the timeline peeking on a ~1440x812 window; a much shorter window trims the
   timeline, a much taller one shows more of the stack. Locking it exactly needs JS
   measurement, not built. Dial 48 ⇄ 72. */
.case-hero + .section { padding-top: clamp(32px, 4vw, 60px); }
/* Layout is the shared bipartite row above. Only the vertical relationship is the
   hero's own: the claim sits centred against the banner rather than hanging off
   its top edge. */
.case-hero__grid { align-items: center; }

.case-hero__head { display: flex; align-items: center; gap: var(--space-12); margin-block: var(--space-16) var(--space-24); }
/* Logo mark, same fixed box as .project__mark so marks read as one set. */
.case-hero__mark { width: var(--mark-size); height: var(--mark-size); border-radius: var(--radius-mark); object-fit: contain; flex: 0 0 auto; }
/* Now carries a full claim sentence (the brand name moved to the kicker), so it
   sits at the act-opener register, not billboard display, and balances its rag. */
.case-hero__name { font-size: var(--fs-display-sm); line-height: var(--lh-heading); font-weight: var(--weight-medium); letter-spacing: var(--tracking-tight); text-wrap: balance; }
/* Header description = the home row string, verbatim, at the project-desc register.
   Kept as a template option; Cynoia drops it (the brand name moved to the kicker
   and the platform line carries the H1), but other case pages can still use it. */
.case-hero__desc { font-size: var(--fs-h2); line-height: var(--lh-heading); font-weight: var(--weight-regular); max-width: var(--measure-statement); text-wrap: balance; }

/* ---- Brand kicker -----------------------------------------------------------
   The eyebrow IS the brand: mark + name set at eyebrow size, so "Case study" is
   gone (the reader knows it is one) and the headline can carry the real claim.
   The mark is tied to the eyebrow's own cap height so the lockup reads as one
   small unit. Replaces the old big mark+name head for this template's default. */
.case-hero__kicker { display: inline-flex; align-items: center; gap: var(--space-8); }
/* Radius is a RATIO, not the absolute --radius-mark: this box is 1.5em of a 12px
   eyebrow (=18px), and 8px on 18px is a 44% near-circle that clips into artwork
   the SVG already rounds itself. 20% = the home page's own 8/40 (--radius-mark /
   --mark-size), so the mark reads identically at either size. */
.case-hero__kicker-mark { width: 1.5em; height: 1.5em; border-radius: 20%; object-fit: contain; flex: 0 0 auto; }
/* Re-establish the rhythm the old .case-hero__head margin used to provide. */
.case-hero__kicker + .case-hero__name { margin-top: var(--space-16); }

/* ---- The meta stack (ROLE / TIMELINE / CHANNELS) ----
   V4.15: was three across on a full-width rule hanging under the hero. Now ONE
   vertical column living inside .case-tone's first slot (cols 1-5), facing the
   setup statements. The rule and its margin went with it: a hairline over 1-5
   only, with nothing over 8-12, is an arbitrary half-line — the "01 — Setup"
   eyebrow already marks the act, so the separator was doing the job twice.
   Item gap is the 8pt rhythm, not the grid gutter (this is a stack now, not
   a row): label 8 to its value, 32 between the three sections. */
.case-meta        { display: flex; flex-direction: column; gap: var(--space-32); }
.case-meta__item  { display: flex; flex-direction: column; gap: var(--space-8); }
.case-meta__label { font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: var(--tracking-wide); font-weight: var(--weight-medium); color: var(--fg-muted); }

/* ---- .case-meta__value — the tight-set value (V4.15, Baha's call) ----
   The answer under a label: "Marketing Designer", "1 year, 4 months / (July 2024
   – October 2025)", "LinkedIn, Instagram". Its OWN class with its OWN leading,
   because a value is not a paragraph. It was inheriting --lh-body (1.6), a
   reading leading built to carry the eye across long lines; on a three-word
   answer that opens a gap so wide the timeline read as two unrelated lines with
   a stray date under it rather than as one fact. --lh-tight (1.1) closes it.

   The date range is a <br> inside this element, NOT a second span: a sibling
   would be spaced by .case-meta__item's flex gap (8px) and the leading would
   never reach it, which is the bug this class exists to fix. One value, one
   element, one leading, however many lines it takes.

   The former .case-meta__sub is retired — it said "footnote" when the range is
   simply the value's second line. Documented in KB/03-components.md. */
.case-meta__value {
  font-size: var(--fs-h3);
  font-weight: var(--weight-medium);
  line-height: var(--lh-tight);
}

/* ============================================================
   01 SETUP — "setting the tone" (two blocks) + paragraphs
   ============================================================ */
/* The meta stack | the statements that set the tone. Layout is the shared
   bipartite row above; the row itself needs nothing of its own now. */
/* V4.16 (Baha's call): 48, was 24. These are two separate paragraphs, not two beats of
   one, and 24 ≈ one line of this register (--fs-h2 × --lh-heading ≈ 27px) — a gap the
   width of a line break reads as a line break, so they set as one block of text with a
   pause in it. 48 is two lines: unmistakably a paragraph break, still inside the same
   thought. The 8pt scale has nothing between them, which is the scale doing its job.
   Dial 32 ⇄ 64. */
.case-tone__block { display: flex; flex-direction: column; gap: var(--space-48); }

/* ---- The header-to-arc break (V4.16, Baha's call) ----
   .case-tone is the last beat of the header block: the rule opens it, the meta stack
   and the overview statements fill it, and "01" starts something else entirely. So the
   gap under it is an ACT gap, not a beat gap. .case-flow deliberately sets ONE uniform
   gap (the rhythm within an act), and this is the single join on the page that is not
   within an act — it is an act boundary that happens to live inside one <section>.
   Adding to the flow gap instead of overriding it leaves that rhythm intact underneath,
   so the step up is legible without inventing a second rhythm.
   V4.17 (Baha's call, verbatim: "don't be stingy"): 96, was 48. At 48 the sum came to
   ~144 against the ~96 between beats — a 1.5x step, which is a pause, not a boundary,
   and the reader arrived at "01" still inside the header. At 96 the sum is ~192, which
   makes this the WIDEST gap on the page: wider than the 160 between acts, and it should
   be. Every other gap separates two beats of the case. This one separates the claim
   FROM the case, and that join happens exactly once. Dial 64 ⇄ 128. */
.case-tone { margin-bottom: var(--space-96); }

/* V4.16 (Baha's call): the opening statements set to the HALF, not to
   --measure-case-text. Inside a half, THE HALF IS THE MEASURE — a second, narrower
   cap inside a pane the reader can already see the edges of is a measure with no
   visible reference, and it left ~200px of slack that read as an accident rather
   than as a decision. The half is ~660px at full width, so at --fs-h2 this sets to
   roughly 53 characters: still inside the 45-75 reading band, so nothing is bought
   by capping it twice.
   Scoped to this block deliberately. .case-statement KEEPS its measure everywhere
   else, because elsewhere it is either a full-width flow child (Results: uncapped,
   it would set one ~1400px line) or it sits above a .case-body it has to rag with —
   which is the whole reason --measure-case-text is one rem token and not two.

   text-wrap: pretty, NOT the base class's `balance`. Balance minimises the longest
   line, so it PULLS A BLOCK IN — that is its entire job, and it is why these two
   paragraphs did not match after the cap came off. Same measure, different rendered
   width: the first wraps to 3 lines and balances to ~52ch (essentially the full
   half, so it barely moves), the second wraps to 4 and balances to ~42ch, about
   120px short. Balance is a HEADLINE tool: right on .case-hero__name and
   .case-para__title, where 2 or 3 lines should not hang a stray word. These are
   paragraphs. `pretty` fixes the last-line widow and leaves the block at its
   measure, which is the whole point of setting them to the half. */
.case-tone__block .case-statement { max-width: none; text-wrap: pretty; }

/* ---- The paragraph block — the brief's picture rule made into one component ----
   Layout is the shared bipartite row above; only alignment is this component's own.
   V4.15 retired all three variants, because the row no longer varies:
     .case-para--figured   was the golden split reversed (text major : figures minor)
     .case-para--grid      was a 12-col override placing the figure at --fig-start
     --fig-start           was the per-instance knob for that override
   A half is a half whether it holds a title, a stack of copy, or two figures, so
   the variants had nothing left to say. Restoring any of them means reintroducing
   a second row shape — read the bipartite-row note above before you do. */
/* V4.17c: alignment is a knob, default `start` — unchanged for every row that does not
   ask. `start` is right when the two halves are close in height: both begin on the same
   line and the row has a top edge. It is wrong the moment one half is much taller, which
   is what the 01 row became when the turn joined the problem's column — a picture pinned
   to the top with 200px of void under it reads as a picture that failed to load, not as
   a picture that ended. Centred, the figure has no top edge to miss and no void to
   explain: it sits against the middle of the argument it illustrates. */
/* V4.26: the columns are a knob now, defaulting to the 50/50 bipartite pair so every
   existing .case-para is unchanged. The ONE instance that overrides it is the Results row,
   where the right pane is a reels gallery (a figure cluster, which the bipartite rule above
   already exempts from the centre line) and wants the major share: it passes --split-golden
   (text minor : reels major). The mobile collapse at 810px still linearises this to one
   column, so the knob is desktop-only. */
.case-para { align-items: var(--case-para-align, start); grid-template-columns: var(--case-para-cols, repeat(2, minmax(0, 1fr))); }
.case-para__text    { display: flex; flex-direction: column; gap: var(--space-16); }
.case-para__figures { display: flex; flex-direction: column; gap: var(--space-16); }

/* ---- .case-para--compare — two registers on ONE row (V4.17, Baha's call) ----
   Informative left, humorous right, each carrying its own claim and its own
   selection of work. They used to be two stacked .case-para rows, which meant the
   reader met the second voice from MEMORY of the first. That is fatal here: the
   answer this act reaches is not "we made informative posts" and then separately
   "we made funny ones", it is the RANGE between the two. A range you have to scroll
   through is a list. Side by side it is one comparison, seen in a glance, which is
   the entire reason the page is bipartite in the first place.
   Stretch, not the row's default `start`: a half whose intro rags to a third line
   would otherwise drop its figures below its neighbour's, and two selections of work
   on two different baselines stop reading as a pair. margin-top: auto pins both
   figure stacks to the bottom of the row, so the intros can rag freely and the work
   still lines up. Each half is a plain .case-beat — the generic vertical group, no
   new vocabulary needed for the inside of a half. */
/* V4.23: the compare row drops the --gap-half centre-line channel for the plain
   --grid-gutter. Once each half became a boxed image stack (below) instead of a prose
   pane, it stopped being a centre-line-honouring half and became a grouped module like
   .case-gallery, so the two stacks sit a gutter apart, close (Baha: the two-stack gap
   "way too wide"), not a half-channel apart. */
.case-para--compare { --case-para-align: stretch; gap: var(--grid-gutter); }
.case-para--compare .case-para__figures { margin-top: auto; }

/* V4.23 (Baha's call): the gray STACK PANEL, the aboutmarco/globoplay vocabulary: a
   sunken box wrapping a grid of posts, labelled from the white above it, never with text
   inside the box. It earns being a component because the page uses it twice: the compare
   row seats TWO side by side at --panel-cols 3 (informative, humorous), and the templates
   library seats ONE page-wide at --panel-cols 5. Only the column count differs, so that is
   the one knob. --surface-sunken is the whisper-gray container (grey-100, not the grey-150
   slab that read as dark), --radius-panel its soft corner, gap 16 packs the wall, padding 24
   insets it evenly. In the compare row, .case-para--compare .case-para__figures (above) adds
   margin-top: auto so the two panels still align top and bottom when one intro rags longer. */
.case-stack-panel {
  display: grid;
  grid-template-columns: repeat(var(--panel-cols, 3), minmax(0, 1fr));
  gap: var(--panel-gap, var(--space-16));
  background: var(--surface-sunken);
  border-radius: var(--panel-radius, var(--radius-panel));
  padding: var(--panel-pad, var(--space-24));
}

/* V4.24 (Baha's call): the templates library sits in a FULL page-width gray box. Baha,
   non-negotiable: the wrapping rectangle stays page-width; "breathing room" is the space
   BETWEEN the stack and that rectangle, never a page margin around it. So the 5x4 keeps its
   own small cell (--tpl-cell, the original full-width size scaled ~1.95x down) and is CENTRED
   inside the wide box, and the room between the smaller stack and the box edges is the
   breathing room. Fixed cell, not 1fr: at 1fr the cells refill the width and there is no room
   left between the stack and its wrapper. minmax(0, --tpl-cell) still lets them shrink on a
   narrow screen so they never overflow. Generous padding-block so the box is not a thin
   letterbox; padding-inline is the minimum inset, the centring adds the rest. */
.case-stack-panel--library {
  grid-template-columns: repeat(var(--panel-cols, 5), minmax(0, var(--tpl-cell)));
  justify-content: center;
  padding-block: var(--space-96);
  padding-inline: var(--space-48);
}

/* V4.26 (Baha's call): a panel whose cells keep their SOURCE ratio instead of being
   equalised to the row. The stack grid defaults to align-items:stretch, which forces
   every cell to the tallest in its row — with mixed-ratio media (blog covers: five 16:9,
   three 2:1) that stretch overrides each figure's aspect-ratio and object-fit:cover then
   crops the odd ones out to fill the taller frame. align-items:start lets each figure
   stand at its own aspect-ratio height, so a 2:1 cover reads full-frame, no crop. The
   cost is an honest ragged baseline where a shorter 2:1 sits a few px above its 16:9
   neighbours; that stagger is the price of never cropping a cover. */
.case-stack-panel--fit { align-items: start; }

/* V4.27 (Baha's call): a stack sized SMALLER than its full-width gray box, breathing
   room around it. Same shape Baha approved for the templates library: the box stays
   page-width (non-negotiable — the room is the space BETWEEN the stack and the box, never
   a page margin around the box), the cells take a FIXED --panel-cell instead of 1fr and
   centre, so the freed space IS the room. padding-block carries that room top and bottom
   too, so it is not only side gutters. --library is the dense 224px instance of this same
   idea; GITEX runs it here at half its old fill width. */
.case-stack-panel--inset {
  grid-template-columns: repeat(var(--panel-cols, 3), minmax(0, var(--panel-cell, 200px)));
  justify-content: center;
  padding-block: var(--space-96);
  padding-inline: var(--space-48);
}

/* V4.28 (Baha's call): the print wall as a uniform-height gallery. Every figure shares one
   row height (--row-h) and takes its own native --ratio, so widths differ (a portrait tee
   sits narrow, a landscape card wide) but every image lines up to the same height — the
   honest way to show pieces of mixed shape together. flex-wrap packs them into rows,
   justify-content:center keeps a part-full row balanced instead of ragged-left, and
   align-items:flex-start pins each row's tops. No crop: only the height is pinned, the width
   follows the ratio. This turns the base grid into a flex row, so grid-template-columns is
   inert here; the base gap still applies as the flex row/column gap. */
.case-stack-panel--justified {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-start;
}
.case-stack-panel--justified .case-media {
  height: var(--row-h, 240px);
  width: auto;
}

/* V4.29 (Baha's call): the print section split into THREE separate gray containers
   instead of one justified wall. The tee line sits on top in its own full-width panel;
   below it a two-panel row whose widths echo the 5-up tee rhythm — a wide 3-unit panel
   (the certificate) and, in the space that leaves, a 2-unit panel holding the three
   promo cards as a 1x3 vertical stack. 3fr:2fr so the left panel reads "as wide as the
   first three tees" and the right is "the rest". .case-print is the vertical group that
   keeps the tee panel and the bottom row on one rhythm (same gap as the panels' own).
   The bottom row is a BENTO (Baha's call): the certificate is the hero and the three
   promo cards are a 1x3 stack the SAME height beside it. The 2.6fr:1fr split is chosen
   so that when each piece fills its own cell width, a 1.594 certificate and a stack of
   three 16:9 cards resolve to the same height — the certificate reads large, the cards
   a neat rail. align-items:stretch (grid default) equalises any residual, and each cell
   centres its content so that residual sits as balanced internal room, never an outer
   page margin. All frames carry their native --ratio, so nothing crops. Re-weight the
   two fr values together if the height match ever drifts. */
.case-print {
  display: flex;
  flex-direction: column;
  gap: var(--panel-gap, var(--space-16));
}
.case-print-row {
  display: grid;
  grid-template-columns: 2.6fr 1fr;
  gap: var(--panel-gap, var(--space-16));
}
/* left panel: the hero certificate, filling its cell (centred to absorb any residual). */
.case-print-cert {
  display: flex;
  align-items: center;
  justify-content: center;
}
.case-print-cert .case-media { width: 100%; }
/* right panel: the three cards stacked one per row, each full width at its native ratio,
   the column centred so a hair of residual height splits top and bottom. */
.case-print-stack {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--panel-gap, var(--space-16));
}
.case-print-stack .case-media { width: 100%; }

/* V4.30 (Baha's call): the motion clips as ONE equal-height line. Three frames of
   different shape (a ~2:1 UI walkthrough, a 4:5 badge, a 1:1 review) sit on a single row,
   every frame the SAME height, none cropped. The mechanism is ratio-weighted flex: each
   figure grows in proportion to its own aspect ratio (flex-grow = width/height, read
   straight off --ratio so there is no second source to keep in sync) from a zero basis, so
   the widths come out proportional to the ratios and every height resolves to the same
   number — width_i / ratio_i is constant. The fixed gaps are removed before the grow split,
   so they never distort the match. Fully fluid: the row always fills, always stays one line,
   and the three scale down together instead of wrapping. align-items:flex-start pins the
   tops (identical here since heights match). This turns the base grid into a flex row, so
   grid-template-columns/--panel-cols are inert; the base gap becomes the flex row gap. */
.case-stack-panel--row {
  display: flex;
  align-items: flex-start;
}
.case-stack-panel--row .case-media {
  flex-grow: calc(var(--ratio));   /* width ∝ ratio → every frame the same height */
  flex-shrink: 1;
  flex-basis: 0;
  min-width: 0;
}

/* V4.31 (Baha's call): the flagship explainer embed under the reels. One video in a full
   page-width gray container — the box stays page-width (Baha, non-negotiable), the breathing
   room is the padding INSIDE it, never an outer page margin. The 16/9 frame holds a responsive
   YouTube iframe (--ink backing so the frame reads solid while the player boots), and the
   iframe fills the frame's media cell exactly. */
.case-embed {
  background: var(--surface-sunken);
  border-radius: var(--radius-panel);
  padding: clamp(var(--space-24), 4vw, var(--space-48));
}
.case-media--embed { background: var(--ink); }
.case-media iframe { width: 100%; height: 100%; border: 0; display: block; }

/* ---- .case-diverge — the divergence unit (V4.17, Baha's call) ----------------
   Baha's divergence drawing plus the extremes axis under it are ONE figure, not two.
   The axis is what the drawing is read AGAINST ("Solutions aren't found at the
   extremes" — the two poles, and the line between them where the answer lives), so
   it is tied to the artwork at the figure stack's own 16 and can never be pulled a
   flow gap away from it. A component, not two pieces of markup, because it appears
   twice: management's conflicting visions, and speed vs excellence.
   Both files are drawn to the same width (divergence 435, lines-extremes 438), so
   both take 100% of the unit and the scale relationship Baha drew survives at any
   width. That pairing is why one max-width on the unit is the only size control here.

   --case-diverge-w is the whole knob: 686px, Baha's number, reverting V4.17c's 549
   and 438. Set on the unit, not on the art, so the drawing and the axis can never be
   sized apart.

   HISTORY, because this cost four rounds and the scars are the useful part.
   V4.17b hung the two positions off the axis's dots and derived a 56.7% cap from the
   dot geometry to make them land. All of it is gone: Baha never asked for the poles
   (he asked for the SPEED-VS-EXCELLENCE box to go, and I read "lose the boxes" as
   licence to delete the note cards), the cards are back, and with them the geometry
   has nothing left to justify it. Do not resurrect the 56.7%: it was a real constraint
   derived from a requirement that was never real.
   V4.17c then sized the art to 438, its exported width, arguing the artwork's own
   scale needs no defence. Baha called it comically small and he was right — that
   argument was about the FILE, not the page. A figure is sized by the weight of its
   claim in the layout it sits in.
   The unit is a centred island, wider than a half and narrower than the field. That
   is allowed for a FIGURE and nothing else on this page: a text block between the two
   honest widths is a measure with no reference (see .case-half), but a figure carries
   its own. 686 does not fit a half (~554 at 1440), which is the only reason the copy
   in "A matter of perspective" sits above this rather than beside it. Cap it at 554
   and the row comes back. */
.case-diverge {
  --case-diverge-w: 686px;
  display: flex;
  flex-direction: column;
  gap: var(--space-16);
  max-width: var(--case-diverge-w);
  margin-inline: auto;
}
.case-diverge__art { width: 100%; }
.case-extremes { display: block; width: 100%; height: auto; }

/* ---- The pole-centred composition (V4.19, Baha's call) — beat knob + figure width -----------
   ONE figure-width knob drives the whole thing. --fig-w sizes the divergence figure AND, through
   the same value, the span the two views below centre across, so a view always lands under a pole.
   min(100%, 686px): 686 is Baha's island width (see .case-diverge history above); min() lets it
   shrink to the field on a narrow viewport. Lives on the beat so both children inherit one source.
   The composition is three classes together: .case-beat--poles + .case-diverge--poles + .case-notes--poles. */
.case-beat--poles { --fig-w: min(100%, 686px); }

/* The figure needs a REAL rendered width or it shrink-wraps: as a margin-auto flex child of the
   beat it collapses to the SVG's intrinsic ~438 and --case-diverge-w never engages (measured:
   poles 386 apart, the art "comically small"). Taking --fig-w spreads the poles enough to seat a
   readable measure under each. Scoped here — the speed-vs-excellence figure sits under its own
   notes and needs no spread, so base .case-diverge is left untouched. */
.case-diverge--poles { width: var(--fig-w); }

/* ============================================================
   MEDIA FRAME (.case-media) — greybox until real assets land
   Same family as the home project cards + .about-media: rounded, hairline,
   surface fill, --ratio knob. Drop an <img> in to replace the placeholder
   label; in the dark zone it auto-themes through the token layer. Mirrors the
   about.css recipe (kept local so the page doesn't depend on about.css; a later
   refactor could hoist one shared .media primitive into styles.css).
   ============================================================ */
.case-media {
  aspect-ratio: var(--ratio, 4 / 3);
  border-radius: var(--radius-card);
  border: 1px solid var(--border);
  background: var(--surface);
  background-size: cover;          /* for the optional inline brand-banner backing */
  background-position: center;
  overflow: hidden;
  display: grid;
  grid-template-areas: "media";    /* one cell: the image overlays the placeholder label, which stays as a fallback */
  place-items: center;
}
.case-media > * { grid-area: media; }
.case-media img,
.case-media video { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Named placeholder label — real visible text now, swapped for an alt-bearing img later.
   V4.32 (Baha's call): hidden site-wide. These sit BEHIND each image as a load/fallback
   label, but they bled through the transparent artwork (the GITEX / print / banner PNGs)
   and read as stray annotations ("social-before", "cynoia-banner"). display:none removes
   them from every frame; the markup and its styling stay, so a future frame can re-enable
   one by dropping this line. Real alt text on every <img> carries the accessibility load. */
.case-media__ph {
  display: none;
  font-size: var(--fs-caption);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: var(--fg-muted);
  text-align: center;
  padding: var(--space-16);
}
/* Reels are video — slot sized 16/9 and flagged, video-ready for a later drop-in. */
.case-media--video { aspect-ratio: var(--ratio, 16 / 9); }
.case-media--video .case-media__ph::before { content: "\25B6  "; }   /* ▶ */

/* Diagrams (transparent SVGs) read on a FIXED light plate, identical in the light
   and dark zones, and are contained so the artwork is never cropped. --grey-100 is
   a primitive (it does not theme), so in the dark Confrontation act the diagram
   reads as an inserted light figure card — the honest thing for a diagram. */
.case-media--diagram { background: var(--grey-100); }
.case-media--diagram img { object-fit: contain; padding: clamp(var(--space-24), 3vw, var(--space-48)); }

/* A diagram drawn straight onto the page — no plate, border, radius, or label. The
   divergence artwork carries itself; --ratio still holds its block and the SVG is
   contained so it is never cropped. The plated --diagram frame is the alternative. */
.case-media--plain { background: none; border: none; border-radius: 0; overflow: visible; }
.case-media--plain img { object-fit: contain; }

/* ============================================================
   02 + 03 NOTE CARDS (.note) — the "two axis / two positions" unit
   In-system reading of the brief's "post-it": a flat, bordered surface card,
   NOT a yellow skeuomorphic sticky (that would be the only decorative, colored,
   3D element on the whole site). The only note cue is a faint folded corner in
   the hairline tone. Reused twice: the two axes, and the two opposing positions.
   ============================================================ */
/* Layout is the shared bipartite row above: the two cards sit on the SAME centre
   channel as every other row, so their inner edges line up with the hero's split
   instead of floating on a narrower gutter of their own. Two opposing positions
   held apart by the page's own channel — the gap is the argument.
   Only equal height is this component's own. */
.case-notes { align-items: stretch; }

.note {
  --note-skew: 0deg;        /* knob: dial to ±0.4deg for a hand-placed feel. System default = flat. */
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: clamp(var(--space-24), 2.5vw, var(--space-32));
  transform: rotate(var(--note-skew));
}
/* Dog-ear: a faint folded corner, bottom-right, in the hairline tone. No color,
   no shadow — stays inside the monochrome flat system; auto-themes via --border. */
.note::after {
  content: "";
  position: absolute;
  right: 0; bottom: 0;
  border-style: solid;
  border-width: 0 0 18px 18px;
  border-color: transparent transparent var(--border) transparent;
}
.note__label { font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: var(--tracking-wide); font-weight: var(--weight-medium); color: var(--fg-muted); }
.note__title { font-size: var(--fs-h3); line-height: var(--lh-heading); font-weight: var(--weight-medium); }
.note__body  { font-size: var(--fs-body); line-height: var(--lh-body); }
.note__list  { margin: 0; padding-left: 1.2em; }
.note__list li { font-size: var(--fs-body); line-height: 1.5; }
.note__list li + li { margin-top: var(--space-8); }

/* ---- .case-notes--poles — the two views, seated under the divergence poles (V4.19, Baha's call)
   The two opposing views belong to the axis above, so each sits UN-BOXED and centred directly
   under its purple pole, not in the page's bipartite row. Three moves, all here:
     1. UN-BOXED. Quoted views, not cards: .note's surface, border, radius, padding and dog-ear
        all drop, so the words hang straight off the axis and carry the extremity themselves.
     2. READABLE. --view-measure is a real reading measure (Lupton/Bringhurst put the comfortable
        band at 45-75 characters), replacing the earlier 20ch squeeze that fell below the floor.
     3. CENTRED ON THE POLES. The unit is a centred flex pair. In a centred pair the two items'
        centres sit (measure + gap) apart, so setting the gap to (pole span - measure) lands each
        centre exactly on a pole. Derived, not eyeballed.
   Geometry, so the next pass need not re-derive it:
     - The poles are at cx 25.6 and 411.6 in the 438-wide axis SVG, so their span is
       --pole-span-frac (0.884) of the figure's RENDERED width. Measured from lines-extremes.svg;
       if that art changes, update the fraction.
     - --fig-w is shared with .case-diverge--poles (both inherit it from .case-beat--poles), so the
       span the views centre across and the figure that draws the poles are always one width. This
       ONLY works once the figure renders at --fig-w: at the collapsed 438 the poles are 386 apart
       and a readable measure will not fit twice.
   Wide-field composition: below 1024 it releases to a stacked column (a phone has no poles to sit
   under) — see the 1024 media block. Knobs: --view-measure (reading width), --fig-w on the beat. */
.case-notes--poles {
  --pole-span-frac: 0.884;                                  /* (411.6 - 25.6) / 438, from lines-extremes.svg */
  --view-measure: 22rem;                                    /* readable measure; dial 20rem (tighter) to 26rem */
  --pole-span: calc(var(--pole-span-frac) * var(--fig-w));
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: calc(var(--pole-span) - var(--view-measure));        /* derived channel: centres each view ON its pole */
}
/* Un-box: strip every card cue back to plain text. Keeps .note's own column layout (label + body). */
.case-notes--poles .note {
  flex: 0 0 var(--view-measure);
  max-width: 100%;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  overflow: visible;
}
.case-notes--poles .note::after { content: none; }          /* remove the dog-ear */

/* ============================================================
   05 GALLERY (.case-gallery) — uniform frames so images don't compete
   ============================================================ */
.case-gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--grid-gutter); }

/* ============================================================
   NEXT / RETURN (.case-next) — keep people in the work. Light, before the dark footer.
   Becomes real prev/next when more case pages exist.
   ============================================================ */
.case-next__link { display: inline-flex; align-items: center; gap: var(--space-12); margin-top: var(--space-16); }
.case-next__label { font-size: var(--fs-h1); font-weight: var(--weight-medium); letter-spacing: var(--tracking-tight); }
.case-next__link:hover { color: var(--accent); }
.case-next__link .arrow { transition: transform .2s ease; }
.case-next__link:hover .arrow { transform: translate(3px, -3px); }

/* ============================================================
   READING SPINE (.beam) — the act rule in the left margin
   The template's progress indicator: a straight hairline that traces as you
   read, ticked once per act. It answers "how far, and through what" (Krug) —
   the one question a long case study actually provokes.

   Zero markup. main.js builds it on any page carrying a .case-flow, so every
   future case study inherits its spine by existing. No-JS gets no spine, which
   is the honest degradation: a scroll indicator cannot exist without scroll.

   It rides the margin --pad-x already reserves, so the Golden Canon grid is
   untouched and nothing on the page reflows — purely additive. It is chrome,
   not content: aria-hidden and pointer-events none, because the .eyebrow act
   labels it ticks against are the real, readable structure.

   Straight means no SVG. Aceternity's tracing beam animates gradient stops
   along an SVG path because its path curves; a straight rule is a box, so this
   is two divs and one gradient. No path, no library, no dependency.

   Knobs (component-local, per --note-skew / --ratio):
     --beam-gap     rule-to-text distance. The rule hangs off the type like a
                    book's margin rule rather than floating mid-margin, so it
                    stays glued to the column as --pad-x shrinks.
     --beam-fade    length of the soft head. Anchored in px from the head, so
                    it stays constant at any trace length.
     --beam-tick-w  width of an act mark.
   Set by main.js: --beam-t (top), --beam-h (span), --beam-p (traced), --tick-y.

   KNOWN BOUNDARY: one element cannot carry two themes. The beam resolves --fg
   and --border once, from .has-beam, so it does NOT invert across a restored
   .section--dark act (see cynoia.html's parked dark zone). If the dark
   Confrontation comes back, the beam has to become one segment per act, each
   inheriting its own act's token layer — contiguous segments still read as one
   unbroken rule. Deliberately not built for a feature that is currently parked.
   ============================================================ */
.has-beam { position: relative; }

.beam {
  --beam-gap: var(--space-48);
  --beam-fade: 64px;
  --beam-tick-w: 9px;
  --beam-t: 0px;   /* main.js */
  --beam-h: 0px;   /* main.js */
  --beam-p: 0px;   /* main.js */

  position: absolute;
  /* Mirror .container's centring so the rule holds a constant --beam-gap from
     the text at ANY width. Below --container-max the container is flush and
     --pad-x alone is the offset; above it the container centres, so its extra
     auto margin (100% - max)/2 has to be added back. max() clamps the below case. */
  left: calc(max(0px, (100% - var(--container-max)) / 2) + var(--pad-x) - var(--beam-gap));
  top: var(--beam-t);
  width: 1px;
  height: var(--beam-h);
  pointer-events: none;
}

/* The untraced rule: the whole arc, always visible, so the reader can see the
   shape of what is left before committing to it. */
.beam__track { position: absolute; inset: 0; background: var(--border); }

/* The traced rule. Height is the trace; the gradient softens the head so it
   reads as a trace rather than a progress bar (a hard terminus reads clinical).
   `to top` places the stops in px UP from the head, so the fade is a constant
   length at any trace height rather than a percentage that stretches. */
.beam__ink {
  position: absolute;
  top: 0; left: 0;
  width: 1px;
  height: var(--beam-p);
  background: linear-gradient(to top, transparent 0, var(--fg) var(--beam-fade));
}

/* Act marks. Muted, never inked: the trace already says what is passed, so
   re-colouring a passed tick would state it twice. */
.beam__tick {
  position: absolute;
  top: var(--tick-y);
  left: 50%;
  width: var(--beam-tick-w);
  height: 1px;
  background: var(--fg-muted);
  transform: translateX(-50%);
}

/* Reduced motion keeps the map and drops the movement: the rule and its act
   marks stay, the scroll-linked trace goes (main.js also skips the listener).
   Degrades to a plain ticked margin rule, which is a legitimate thing to be. */
@media (prefers-reduced-motion: reduce) {
  .beam__ink { display: none; }
}

/* ============================================================
   SERIF ACCENT — the one editorial moment, extended to the case selectors.
   Re-declared here (not added to styles.css) so the home build is untouched.
   Same recipe as the shared rule: Baskerville italic, pulled back to 0.92em.
   ============================================================ */
.case-hero__name em,
.case-hero__desc em,
.case-confront__statement em,
.case-para__title em {
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: var(--weight-regular);
  font-size: 0.92em;
  letter-spacing: 0;
}

/* ============================================================
   RESPONSIVE — 1024px, the pole-centred views release (V4.19).
   The two views centre on near-edge poles, so side by side they only fit while the field is
   wide; below ~1024 they would reach past the field toward the viewport edges. So the pair
   alone linearises here (the rest of the page holds until 810), stacking un-boxed in reading
   order. The constraint is width, not device, which is why it is above the master collapse.
   --fig-w still shrinks the figure to the field, so the drawing keeps its size.
   ============================================================ */
@media (max-width: 1024px) {
  .case-notes--poles { flex-direction: column; align-items: stretch; gap: var(--space-32); }
  .case-notes--poles .note { flex: 0 0 auto; max-width: none; }
}

/* ============================================================
   RESPONSIVE — 810px, the site's master mobile collapse.
   Every multi-column layout linearises to one column in reading order.
   ============================================================ */
@media (max-width: 810px) {
  /* V4.15: one list, because there is one row shape now. .case-meta is absent by
     design — it is a flex column at every width, so it was already the mobile
     shape. The --gap-half channel collapses to a plain vertical rhythm: a 120px
     void between stacked halves would read as a section break, not a split. */
  .case-hero__grid,
  .case-tone,
  .case-para,
  .case-notes,
  .case-gallery { grid-template-columns: minmax(0, 1fr); }

  .case-hero__grid,
  .case-tone,
  .case-para,
  .case-notes { gap: var(--space-32); }

  /* .case-diverge needs no override: its max-width is a ceiling, and on a phone the
     column is always the smaller of the two, so the stack takes the column on its own. */

  /* V4.16: .case-half releases here for the same reason the grid rows collapse —
     there is no second half to hold a centre line against, so half of a phone is
     just a narrow column. .case-confront__statement is gone from this list because
     it no longer carries a width; .case-half is what measures it now. */
  .case-body,
  .case-statement,
  .case-hero__desc,
  .case-half { max-width: none; }

  /* Poles need no override here: they already released to a stacked column at 1024 (below),
     and once stacked they are just a full-width list that the master collapse leaves alone. */

  /* ---- Mobile gallery grids (Baha's call) ------------------------------------
     Each gallery's column count is set per-instance as an inline style="--panel-cols",
     which OUTRANKS any class rule that re-sets that custom property. So on mobile we
     override grid-template-columns DIRECTLY (a real property the inline style never
     touches) to reflow each panel to the count Baha specified for phones. First number is
     columns. No crop anywhere: frames keep their --ratio, only the track count changes.
     Reels + motion are left alone (they carry their own composition). */
  #sales-enablement .case-stack-panel { grid-template-columns: repeat(2, minmax(0, 1fr)); }   /* sales   2x3 */
  #blog-covers .case-stack-panel      { grid-template-columns: repeat(2, minmax(0, 1fr)); }   /* blog    2x4 (stays --fit, no crop) */
  #gallery .case-stack-panel--inset   { grid-template-columns: repeat(2, minmax(0, var(--panel-cell, 192px))); } /* GITEX 2x3 (Baha V4.32) */
  .case-stack-panel--library          { grid-template-columns: repeat(3, minmax(0, var(--tpl-cell)));          } /* templates 3-wide (Baha V4.32) */

  /* V4.32 (Baha): the GITEX tee wall was still 5-across on a phone (tiny shirts). Drop it to
     2 so the shirts read, with a tighter gap. Scoped to the FIRST panel in the print group —
     the certificate / promo bento below keeps its own layout (it has its own overrides). */
  #gallery .case-print > .case-stack-panel { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-8); }

  /* V4.32 (Baha): on a phone the inset / library panels' desktop breathing room (96 block /
     48 inline) eats the width the images need, so "bigger" never lands. Pull the inset to a
     phone-scale room so the GITEX posts and templates fill more of the box. Still INTERNAL
     padding (breathing room), never an outer page margin. */
  .case-stack-panel--inset,
  .case-stack-panel--library { padding: var(--space-32) var(--space-24); }

  /* V4.33 (Baha): motion clips on mobile as a bento. The wide motion-01 (2:1) takes the top
     full-width, and motion-02 + motion-03 share the line beneath it at the SAME height with NO
     crop. The bottom pair reuse the desktop ratio-weighted trick: flex-wrap the row, force
     motion-01 to a full-width basis so it wraps onto its own line, and let the other two keep
     the base flex-grow = --ratio / flex-basis 0. Their widths then come out proportional to each
     clip's ratio, so both resolve to one identical height (the portrait ends a touch narrower
     than the square, but tops AND bottoms sit flush). Tight gap. */
  #motion-videos .case-stack-panel--row {
    flex-wrap: wrap;
    gap: var(--space-8);
    align-items: flex-start;
  }
  #motion-videos .case-stack-panel--row > .case-media:first-child { flex: 0 0 100%; }

  /* V4.32 (Baha): crisper corners at phone size — the 10 / 16px radii read bubbly on the small
     frames. Step each down one token (cards 10->8, panels 16->10). Dial tighter if you want. */
  .case-media               { border-radius: var(--radius-mark); }
  .case-stack-panel,
  .case-embed               { border-radius: var(--radius-card); }

  /* The print bento HOLDS on mobile (Baha's call) instead of flattening to a plain list:
     the row still linearises so the certificate gets full width on top, and the three promo
     cards — a 1x3 vertical rail on desktop — sit NEXT TO EACH OTHER as a 3-up row beneath it. */
  .case-print-row   { grid-template-columns: minmax(0, 1fr); }
  .case-print-stack { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: start; }
}
