/**
 * Flush Split — chrome/skeleton only (Story 1 of spec-flush-split).
 *
 * Pairs with templates/flush-split.php + flush-split.js. Enqueued by the
 * Neve child theme's functions.php only when a `project` post's Template is
 * explicitly set to Flush Split.
 *
 * `.flush-split__media` and `.flush-split__card-col` are empty placeholders
 * here — no hero carousel (spec-flush-split CAP-1), flip card (same spec's
 * CAP-2) yet. Discipline pills (its CAP-4) were built and then removed
 * on 2026-09-18 (Joe), along with the breadcrumb's `Home` segment.
 * NOTE: CAP numbers in this file belong to two different specs. Unqualified ones predate
 * spec-flush-split-alignment and mean spec-flush-split; that later spec
 * reuses the same numbers for different work (its CAP-1 is band alignment,
 * CAP-2 the pill restyle, CAP-4 the carousel-controls pass), so every
 * reference below is now qualified by spec name. Their flex sizing below IS final (per this story's Design
 * Notes) — later stories only fill content into these two boxes, not
 * restructure their outer sizing.
 *
 * 100vh mechanics — read this before touching the flex rules below:
 *
 * Neve's own `.wrapper` (real header / #content / real footer) already
 * renders as a `min-height`-driven flex column that fills the viewport —
 * confirmed live against dev.joetennis.com (Auto Loan Future, an existing
 * Editorial Split Wide post) via a temporary in-page style/DOM injection,
 * never saved to any file or the live site, before writing this file. That
 * means the outer "header/chrome-bar/footer are flex: 0 0 auto, the split
 * section is flex: 1 1 auto" requirement this story's spec describes is
 * already true at the `.wrapper` level with zero CSS from this theme.
 *
 * The one real gap: `#content` (`main#content.neve-main`, Neve's own
 * content landmark — the skip-link target, so this id is load-bearing and
 * safe to rely on) is a plain block box by default. A block box does not
 * stretch its child to its own height the way a flex/grid container does,
 * so `.flush-split` (this template's own `<main>`, `#content`'s only
 * child) needs `#content` to become a flex column too before `flex: 1 1
 * auto; min-height: 0` on `.flush-split` itself has anything to fill.
 * Scoped via `:has(.flush-split)` rather than touching `#content`
 * unconditionally, so every other template/post type's rendering
 * (`.neve-main` is a generic, shared Neve class) is provably untouched.
 *
 * Verified live (same temporary-injection method, immediately reverted —
 * no lasting change to dev.joetennis.com): with just `#content` turned into
 * a flex column and `.flush-split`'s own `flex: 1 1 auto; min-height: 0`,
 * an empty-placeholder split section fills exactly the space between the
 * chrome bar and the real footer, with no page-level scroll.
 *
 * CORRECTION 2026-09-14 (Story 2 review, live-verified against
 * dev.joetennis.com/projects/cdroms-kiosks/): that verification used an
 * empty placeholder, which never stress-tested the actual cap. Neve's own
 * `.wrapper` (header/#content/real footer) uses `min-height: 100vh` -- a
 * FLOOR, not a ceiling. `#content`'s `flex: 1 1 auto; min-height: 0` only
 * ever correctly shrinks it to "leftover space" when `.wrapper`'s own
 * height is *exactly* one viewport; nothing stopped `.wrapper` from
 * growing taller when a child demanded more room, which real hero_shots
 * images (Story 2) finally did. The chain also cascades the wrong way in
 * that state: with no real ceiling anywhere, `.flush-split__media`'s own
 * `height: 100%` had nothing definite to resolve against either, so the
 * image fell back to sizing by its own intrinsic ratio at full width --
 * for a low native-resolution/near-4:3 image this produced a rendered
 * height far taller than one viewport, which is what actually pushed
 * `.wrapper` past 100vh in the first place (a normal widescreen photo
 * happened to land close enough to the intended height that Stories 1/3/4
 * never noticed the gap). Capping `.wrapper` itself, scoped to Flush
 * Split only, gives the whole chain a real ceiling to shrink into.
 *
 * SUPERSEDED IN PART 2026-09-16 (spec-flush-split-alignment CAP-3, live-
 * verified at 1235x726 and 1235x1050 before this file was edited): the
 * paragraph above is still the correct history of *why* the cap was
 * added, but its conclusion -- that the cap is what stops the intrinsic-
 * image blowup -- no longer holds, and must not be used as a reason to
 * keep the cap unconditional. `.flush-split__media` has since gained its
 * own `min-height: 0` (see its rule below), and the image's `height:
 * 100%` resolves against its grid ROW-TRACK, not against `.wrapper`.
 * Measured with the cap released at 1235x726: the media box and the
 * `<img>` box both come out at 794.8x669.2 -- exactly the row track, with
 * the picture letterboxed inside it by `object-fit: contain`. Its
 * intrinsic size at that width would be 794.8x447. No blowup. Story 2's
 * "no 100vh / no-scroll change" Never bullet is therefore superseded for
 * this specific, measured case.
 *
 * CORRECTION 2026-09-14 (live-verified on an actual iPhone): the cap
 * above must be desktop-only. Mobile stacks image-above/card-below
 * (768px breakpoint below) and can easily need far more than one
 * viewport's height -- that layout was always meant to scroll normally,
 * same as every other template. Shipping the cap unscoped by width
 * clipped `.wrapper` at 100vh on mobile too, and combined with mobile
 * Safari's well-known unreliable handling of `vh` units, produced a
 * genuinely broken render: the real site footer appeared visually
 * sandwiched inside the card's own text content instead of at the true
 * end of the page. Scoping this to `min-width: 769px` (matching the
 * stacked breakpoint's own `max-width: 768px` exactly, no gap or overlap)
 * leaves mobile's natural, unconstrained scroll completely untouched.
 *
 * ONE SCREEN IS A REWARD FOR HAVING ROOM, NOT A CONTRACT (2026-09-16,
 * spec-flush-split-alignment CAP-3). The cap alone had no escape hatch:
 * when the card column's own min-content height exceeded what one
 * viewport could give the media/card row, the row opened past the split
 * and `overflow: hidden` silently CLIPPED the excess -- measured at
 * 1235x726, the carousel controls landed 190px below the fold with no way
 * to scroll to them, so a visitor could not change slides. (Mechanism:
 * `.flush-split__media` opts out of its automatic minimum with
 * `min-height: 0`, `.flush-split__card-col` does not. Full reproduction
 * in the spec's cap-3-sizing-input.md.)
 *
 * The fix is NOT a height-keyed media query -- a guessed `max-height`
 * value is blind to how much text any given project's card actually
 * holds, and the threshold is therefore per-project, not per-device.
 * Instead flush-split.js measures the card's real minimum (shadow-cloned,
 * cap-independent) against the height one viewport could offer the row,
 * and sets `flush-split-cap-released` on `<body>` when it doesn't fit.
 * That releases the cap and the page scrolls normally, exactly as mobile
 * already does. The threshold is live-derived per project and per
 * viewport, and on the viewing account: the budget subtracts `.wrapper`'s
 * own offset from the top of the viewport, which the WP admin bar sets to
 * 32px for logged-in users. On this page's content the threshold therefore
 * lands ~32px apart for the two cases -- see the measurements companion for
 * the measured pair. Nothing here is a fixed number in CSS; a height-keyed
 * media query was rejected precisely because it could not know either.
 *
 * THE CAP IS GATED ON ITS RELEASER BEING LIVE (`html.flush-split-js`).
 * Without that gate the cap is the one rule in this file that can hide
 * content with nothing able to reveal it: if flush-split.js is blocked,
 * 404s, is dropped by CSP, or throws, the `overflow: hidden` still applies
 * and the release flag never arrives. Gating it means the failure mode is
 * a page that simply scrolls -- the same graceful degradation
 * `.has-js-controls` already gives the carousel buttons.
 *
 * STRENGTHENED 2026-09-16 (review round 2): the class is added only after
 * a split has completed a full measure/decide pass without throwing, not
 * at the top of the script. "The file started running" was the wrong
 * claim to gate on -- a throw after it would have left the cap applied
 * with no releaser. A page with no `.flush-split__split` at all (the
 * template's "Project not found" branch) never gates the cap on either.
 * Live-checked 2026-09-16: the script is a plain synchronous
 * `<script src>` at the end of `<body>` (no `defer`/`async`), so that pass
 * completes before first paint and the gate costs no flash of uncapped
 * layout.
 *
 * Specificity: the cap is (0,3,2) and the release rule (0,4,2), so the
 * release wins on the cascade with no `!important`. Both stay inside the
 * same `min-width: 769px` block: mobile never has a cap to release.
 *
 * The release restores the VERTICAL axis only. `overflow: visible` would
 * also un-hide horizontal overflow that the capped state was hiding, which
 * can hand a visitor a page-wide horizontal scrollbar -- a regression the
 * capped state structurally could not produce. Keeping `overflow-x: hidden`
 * means the horizontal behaviour is identical in both states, and only the
 * vertical clipping (the actual bug) is lifted. Note the CSS overflow rule
 * that when one axis is not `visible` the other computes to `auto`: with
 * `max-height: none` the wrapper is auto-height, so it never has vertical
 * overflow of its own to scroll and the page scrolls normally -- verified
 * live, see the measurements companion.
 */

/*
 * WIDENED 2026-09-18 (spec-flush-split-white, CAP-2): each rule below gains a
 * second selector, keyed on the White variant's own body class. Flush Split
 * White renders through this same stylesheet via a delegating template shim,
 * but WordPress emits the body class from the ASSIGNED template slug, so the
 * single-selector versions matched only Flush Split and the cap would neither
 * apply nor release there -- a silent regression, since a page that simply
 * never caps looks entirely correct until a card overruns one screen.
 *
 * Widened here rather than re-declared in flush-split-white.css (Option A,
 * Joe 2026-09-18): one source of truth for the machinery that took four
 * post-ship rounds to get right on 2026-09-14. flush-split.js is untouched --
 * it keys off `.flush-split__split` elements, which the shim emits, and
 * toggles `flush-split-cap-released` on `document.body` unconditionally.
 */

@media ( min-width: 769px ) {
	html.flush-split-js body.project-template-flush-split .wrapper,
	html.flush-split-js body.project-template-flush-split-white .wrapper {
		max-height: 100vh;
		overflow: hidden;
	}

	html.flush-split-js
		body.project-template-flush-split.flush-split-cap-released
		.wrapper,
	html.flush-split-js
		body.project-template-flush-split-white.flush-split-cap-released
		.wrapper {
		max-height: none;
		overflow-x: hidden;
		overflow-y: visible;
	}
}

#content:has(.flush-split) {
	display: flex;
	flex-direction: column;
	min-height: 0;
}

.flush-split {
	--color-bg-surface: #111827;
	--color-accent-primary: #00e5ff;
	--color-accent-secondary: #a855f7;
	--color-text-heading: #f8fafc;
	--color-text-body: #e2e8f0;
	--color-text-muted: #ffffff;

	/* THE CARD'S RING, named at v1.13.15 (SPEC-content-module-tabs story
	   3). It was a repeated literal at two sites -- the card's own border
	   and the tab bar's bottom edge -- and The Team pane adds two more,
	   the between-blocks divider and the credit table's row separator.
	   Four consumers is past the point where restating the literal is the
	   cheaper option.

	   IT STAYS AN ALPHA, for the reason the tab bar's background does: it
	   composites against whatever card surface is beneath it, #111827
	   here and #3C5158 on Flush Split White, which is why
	   flush-split-white.css retains it unchanged (:250) rather than
	   overriding it. A fixed hex would bind the ring to one ground.

	   THE NAME IS THE PROTOTYPE'S OWN (`--card-ring`), prefixed to this
	   sheet's `--color-*` convention. */
	--color-card-ring: rgba(0, 229, 255, 0.15);

	/* `--color-tab-current` IS NOT DECLARED HERE ANY MORE, and that is the
	   only reason this comment exists where a declaration used to be.

	   It lived on this root from v1.13.15, correctly: a token spent by
	   rules in this sheet belongs on this sheet's root, and its two
	   consumers -- `.flush-split__card-tab[aria-selected="true"]` (:1105)
	   and `.flush-split__panel-kicker` (:1471) -- are both inside
	   `.flush-split`. Story 3 of SPEC-about-page added a THIRD consumer,
	   the About medallion's current arc label, which is not inside
	   `.flush-split` and therefore cannot see a token hosted here.

	   SO IT MOVED TO `:root` IN site-interactive-states.css, beside the
	   Family C shape tokens and for the same stated reason: it is a
	   COMPONENT-surface value, ground-independent, and it now has more
	   than one component. Both rules below still read
	   `var(--color-tab-current)` unchanged -- `:root` is an ancestor of
	   this root, so the cascade delivers it here exactly as before and
	   NOTHING REPAINTS. The value, the 8.31:1 / 3.92:1 measurements and
	   Joe's ruling that keeps the lime are all recorded at the new
	   declaration; do not re-derive them here.

	   Restating `#8cc201` in about.css was the alternative and was
	   refused: `checks/06` counted declarations in THIS FILE ONLY, so the
	   fork would have been silent. That assertion is now scoped to every
	   `theme/*.css` and still wants exactly one -- which this file no
	   longer supplies, deliberately. */

	/* TWO ROLES NAMED RATHER THAN HARDCODED, v1.13.3
	   (SPEC-revision01-colour-adoption, decision 8 and CAP-3). Both are
	   declared HERE, on the shared root, and carry the value the DARK
	   template already rendered -- so naming them repaints nothing.
	   flush-split-white.css retargets both. The point of declaring them
	   here rather than only there is that a token used by a rule in this
	   sheet must be bound in this sheet: bound only on the white template,
	   the dark one would resolve `var()` to nothing and drop the
	   declaration entirely.

	   `--color-psr-label` is the P/S/R label's ink. Decision 8 names the
	   role `psr-label`; on Flush Split it is still the secondary accent,
	   which is why this reads the token rather than restating #a855f7 and
	   giving one cell two sources.

	   `--flush-split-breadcrumb-rest-fg` is the breadcrumb ANCESTOR link's
	   rest ink. `currentColor` on `color` is defined as equivalent to
	   `inherit`, which is exactly what this rule declared before -- a
	   breadcrumb at rest is ground ink, not a state.

	   WHY THE BREADCRUMB IS A TOKEN AND NOT A WHITE-SCOPED RULE, and it is
	   the trap worth keeping: `.flush-split__breadcrumb-item a` is (0,1,1),
	   and site-interactive-states.css's hover and focus-visible rules for it
	   are (0,2,1). A white override scoped the usual way --
	   `body.project-template-flush-split-white .flush-split__breadcrumb-item
	   a` -- is (0,2,2) and would BEAT both, silently deleting the link's
	   hover AND its focus ring. Moving the value instead of the rule keeps
	   the specificity exactly where it was. */
	--color-psr-label: var(--color-accent-secondary);
	--flush-split-breadcrumb-rest-fg: currentColor;

	/* Single source of truth for the card column's width -- referenced by
	   both `.flush-split__split`'s grid track and
	   `.flush-split__carousel-controls`'s width calc, which must always
	   size to "everything except this column" to stay lined up under the
	   media column. Previously duplicated as a literal in both places with
	   nothing enforcing they matched. */
	--flush-split-card-col-width: clamp(360px, 32vw, 560px);

	flex: 1 1 auto;
	min-height: 0;
	display: flex;
	flex-direction: column;
}

/* Screen-reader-only, but still focusable/toggleable via keyboard — the
   checkbox itself drives the flip via :checked, it just shouldn't be
   visibly rendered as a checkbox. Duplicated per this theme's per-file
   convention (see editorial-split.css, full-bleed-float.css,
   editorial-split-wide.css). */
.visually-hidden {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

.flush-split__not-found {
	padding: 64px 24px;
	font-family: var(--font-body);
	font-size: 18px;
	opacity: 0.7;
}

/* ---------------------------------------------------------------------
   Chrome bar: the breadcrumb, alone in its row since 2026-09-18.
   No new visual standard invented for the breadcrumb itself — text
   inherits the page's own color, and the two existing real Neve custom
   properties already used elsewhere in this theme (site-footer.css,
   site-nav.css) cover the one accent touchpoint (link hover) this
   needs. flex: 0 0 auto -- sized to its own content, never grows or
   shrinks. The discipline pills that once shared this row were removed on
   2026-09-18 (Joe), so the breadcrumb is its only child and the shared
   row's `space-between` leaves it flush left.
   ------------------------------------------------------------------ */

/* Band alignment (max-width/centering/horizontal padding and the
   left/right flex anchoring) is owned by the shared
   `.joetennis-breadcrumb-utility-row` class (site-breadcrumb-utility-row.css)
   applied alongside this one in templates/flush-split.php — CAP-1,
   spec-flush-split-alignment, superseding this rule's old full-bleed
   `padding: 16px 24px`, whose 24px was what put the breadcrumb 71.5px left
   of the header logo (and, before their removal, the pills 71.5px right of
   the CTA button). Only
   this row's own vertical padding and wrap/gap behavior live here now. */
.flush-split__chrome-bar {
	flex: 0 0 auto;
	/* display:flex and justify-content duplicated here defensively — the
	   canonical declarations live on the shared
	   `.joetennis-breadcrumb-utility-row` class, but this rule keeps its own
	   copies so the row still lays out as a flex row with its two ends
	   anchored, rather than stacking to block, if that shared stylesheet
	   ever fails to load. Harmless duplication when both are present — same
	   values, no conflict. `.portfolio-archive__toolbar` in
	   archive-project.css does the same for `display` (it has no reason to
	   duplicate `justify-content`, since its own rule sets no conflicting
	   value); this rule duplicates both.

	   Scope of that fallback, stated honestly: it covers layout, NOT the
	   band. The horizontal inset lives only on the shared class and is
	   deliberately not duplicated here — a second copy of the gutter would
	   silently win over the shared value (this file loads later) and drift
	   the moment that value is ever tuned, which is a worse and more likely
	   failure than the one it guards. So if the shared sheet 404s, this row
	   reverts to full-bleed while `.flush-split__split` below stays banded
	   (its band is local) and the two rows visibly disagree. That state is
	   near-unreachable in practice: the shared sheet is a declared `$deps`
	   dependency of this one, so an UNREGISTERED handle takes this
	   stylesheet down with it rather than leaving a half-styled page. */
	display: flex;
	align-items: center;
	justify-content: space-between;
	flex-wrap: wrap;
	gap: 8px 16px;
	/* Stated, not assumed: `width: 100%` + horizontal padding below only
	   lands on the band under border-box. Neve sets that globally, but this
	   rule's correctness shouldn't depend on an inherited global — same
	   reason `.flush-split__card-face-inner` declares it explicitly. */
	box-sizing: border-box;
	/* Cross-axis `margin: 0 auto` from the shared class would otherwise
	   suppress this flex item's `align-self: stretch` (auto cross-axis
	   margins beat stretch), leaving the row shrink-wrapped to its own
	   content instead of spanning the band — so `space-between` would have
	   no free space to distribute and both anchors would drift inward.
	   `width: 100%` gives the auto margins a definite cross size to centre.
	   Same gotcha `.flush-split__split` documents below; it does not arise
	   on `.portfolio-archive__toolbar`, whose parent is not a flex
	   container. Live-verified 2026-09-16. */
	width: 100%;
	/* Longhands, NOT the `padding` shorthand: flush-split.css loads after
	   site-breadcrumb-utility-row.css (declared dependency) and both
	   selectors are single-class, so a shorthand here would reset the shared
	   class's `padding: 0 var(--band-gutter)` horizontal inset to zero and
	   undo the band.
	   Same reason archive-project.css uses `padding-top` alone. */
	padding-top: 16px;
	padding-bottom: 16px;
}

.flush-split__breadcrumb {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 8px;
	list-style: none;
	margin: 0;
	padding: 0;
	font-family: var(--font-body);
	font-size: 14px;
}

.flush-split__breadcrumb-item {
	display: flex;
	align-items: center;
	gap: 8px;
}

/* Separator between items -- not after the last one. */
.flush-split__breadcrumb-item:not(:last-child)::after {
	content: '/';
	opacity: 0.5;
}

.flush-split__breadcrumb-item a {
	color: var(--flush-split-breadcrumb-rest-fg);
	text-decoration: none;
}

/* Hover/focus retired to site-interactive-states.css (story 2). This rule
   read a BARE `var(--nv-primary-accent)` with no local fallback -- the one
   pattern violation in this sheet: an unresolved Neve variable drops the
   whole declaration and the link silently loses its only affordance. The
   shared tokens carry the fallback.

   REST INK IS GROUND INK AGAIN ON BOTH TEMPLATES, v1.13.4, and the round
   trip is the point of this note. v1.13.3 retargeted it to #8100fc on Flush
   Split White, straight from the revision01 design. That turned out to
   collide: hover reads `--state-breadcrumb-hover-fg`, which follows
   `--current-marker`, and decision 3 had moved that marker to #8100fc as
   well -- so rest and hover were one hex and the link had no hover
   affordance at all. Two ruled values from two different decision rows, and
   neither row saw the other.

   JOE RESOLVED IT AT THE REST END, 2026-09-22: black at rest, 16.52:1 on
   the #e4e4e4 band, and #8100fc keeps its 4.95:1 on hover where it now
   means something. Worth keeping because the fix is not obvious from either
   row on its own -- the collision only exists when you read them together,
   and the resolution was to move the value that had the MORE surface to
   spare, not the one that was ruled later. Focus is untouched throughout:
   #0b6b78 at 6.20:1, plus its ring.

   The token still defaults to `currentColor` for the dark template, so this
   rule's specificity trap below is unchanged. */

/* Current title: plain text, not a link -- slightly muted, same
   opacity-based "quieter than the interactive items" treatment
   site-footer.css already uses for its own copyright text. */
.flush-split__breadcrumb-item--current {
	opacity: 0.85;
}

/* A LONE CRUMB TAKES THE CURRENT TREATMENT'S WEIGHT, v1.13.4 (Joe).

   This <ol> has exactly two shapes and the second one is easy to forget.
   With a post, it is "Portfolio / <title>" and the title carries
   `--current` and `aria-current="page"`. WITHOUT one -- the template is
   registered for `page` as well as `project`, and `$has_post` gates the
   whole second <li> -- the list is just "Portfolio", with no `--current`
   class and no `aria-current` anywhere in it.

   In that shape "Portfolio" IS where you are, so it should read like it.
   `:only-child` is the whole test: it matches exactly when the second <li>
   was never emitted, and cannot match the two-item case. Whitespace text
   nodes do not count against it.

   WEIGHT ONLY, and not because colour was forgotten. The ancestor link's
   rest ink and the current item's ink are both #000000 on Flush Split
   White as of v1.13.4, so the colour half of the "current treatment" is
   already what this element paints; declaring it again here would give one
   cell two sources. On the dark template both are inherited ground ink,
   which is the same relationship in the other palette. If those two inks
   are ever pulled apart, this rule needs the colour added -- it is the one
   thing keeping it to a single declaration.

   NOT `--current`-scoped and NOT white-scoped: it is a structural fact
   about the list, true on both templates, and weight is not a colour. */
.flush-split__breadcrumb-item:only-child {
	font-weight: 600;
}

/* ---------------------------------------------------------------------
   Split section -- a CSS Grid, not a flex row. TWO areas, ONE row:
   "media" and "card", side by side on desktop, stacked on mobile.

   REWRITTEN 2026-09-21 (CAP-4, v1.11.19). This comment used to describe a
   THIRD area, "controls", occupying a separate full-width row below the
   pair, and argued at length for why the carousel controls had to live
   there rather than inside `.flush-split__media`. That third row is gone
   and the controls are back inside the media column. The old argument is
   preserved here because it was right about its mechanism, and the next
   person to consider moving them will rediscover it otherwise:

     Story 2 originally nested the controls inside `.flush-split__media`
     as a second flex-column child. That shrank the IMAGE's own visible
     height to make room for them without touching `.flush-split__card-col`
     at all -- so the two outer boxes still matched, but the image's real
     bottom, now short of its box by the controls' height, no longer lined
     up with the card's real bottom. A "stepped" mismatch, invisible to a
     height-only check and obvious on screen. The 2026-09-14 revert gave
     "controls" its own row so that neither column individually lost any
     height to it.

   WHY THAT NO LONGER APPLIES. The old fix bought flush image/card bottoms
   at the price of the controls' position, and the price was only visible
   in `syncSplit()`'s BAIL case (`cardMin > image`). There, the row stays
   stretched to the card, the image letterboxes with all its slack at the
   bottom (`object-position: right top`), and the separate controls row
   sits below ALL of that -- so the gap between the picture and the buttons
   grew as the viewport narrowed. That was the reported bug. The two
   guarantees -- (A) image bottom == card bottom, (B) controls follow the
   image -- cannot both hold once sync bails; they coincide only while it
   succeeds. Joe's call, 2026-09-21: B wins, the card's bottom floats free.

   AND THE OLD OBJECTION IS ANSWERED, not merely overruled. Nesting no
   longer eats the image's height, because the image is no longer stretched
   to the row: it is sized by its own aspect ratio and capped by
   `max-height: calc(100% - var(--flush-split-controls-h))`, so the
   controls' space is RESERVED rather than stolen, and the image's box and
   its painted pixels coincide. That box/paint difference was the whole
   bug; moving the controls without closing it would only have relocated
   the slack. See `.flush-split__media` and `.flush-split__hero-image`.

   The per-breakpoint `grid-template-areas` trick this comment used to
   credit -- desktop wanting controls below both columns, mobile wanting
   them BETWEEN media and card -- is no longer needed for the controls at
   all. Nested in the media column, they land directly after the image in
   both layouts by DOM order alone. The areas still do the media/card
   stacking. */

.flush-split__split {
	flex: 1 1 auto;
	min-height: 0;
	display: grid;
	grid-template-columns: 1fr var(--flush-split-card-col-width);
	/* ONE row as of CAP-4 (v1.11.19); the "controls" row is gone. */
	grid-template-rows: 1fr;
	grid-template-areas: "media card";
	/* Banded to the same shared `var(--container)` edges as the header, the
	   Portfolio archive, the footer and the chrome bar above, so the
	   hero/card unit reads as one centered unit rather than a full-bleed
	   strip (CAP-1, spec-flush-split-alignment). Set here directly rather
	   than by applying `.joetennis-breadcrumb-utility-row` — that class sets
	   `display: flex`, which would destroy this grid.

	   `width: 100%` is load-bearing, not redundant: this is a flex item of
	   `.flush-split` (a column flex container), so its cross axis is
	   horizontal, and an auto cross-axis margin suppresses
	   `align-self: stretch` — `max-width` + `margin: 0 auto` alone would
	   leave the grid sized to its own content instead of the band, and the
	   `1fr` media track would collapse with it. `width: 100%` gives the auto
	   margins a definite cross size to centre.

	   `var(--container)`, never a literal px value: it reads off Neve's own
	   global Customizer container-width dial, the same single source the
	   shared row consumes (see site-breadcrumb-utility-row.css's header
	   comment). `--band-gutter` is the sitewide inset and Neve's own
	   `.container` is overridden to take it too (style.css), so the two
	   still agree and no small-breakpoint override is needed. It was a flat
	   15px hand-copied from Neve until v1.14.0.

	   Does not disturb the carousel controls' width either, and as of
	   CAP-4 (v1.11.19) cannot: they are no longer a grid item of this
	   element with a `calc(100% - var(--flush-split-card-col-width))`
	   width that had to be reasoned about against this padding. They are
	   `width: 100%` inside `.flush-split__media`, so they measure the
	   media column by construction rather than by arithmetic that has to
	   be kept in step with both tracks.

	   CONSEQUENCE FOR spec-flush-split-alignment CAP-3, flagged here because
	   that story has to size
	   this unit: banding narrows the media column. At a 1776px viewport the
	   column goes 1201px -> 1010px (the row capped at 1600 then, 1400 now,
	   while
	   `--flush-split-card-col-width` stays at its 560px clamp ceiling).
	   But the column is not the picture: measured post-deploy, roughly half
	   that reduction came out of empty letterbox space, not image. The
	   painted image went 1106.5x622.4 -> 1010x568.1 — about 9% smaller per
	   side, ~17% less area — and it now fills its column exactly (the 94.5px
	   letterbox gap that used to sit left of it is gone). Joe's reported
	   symptom feeding that story is "the carousel gets unusually small";
	   this change does move that in the wrong direction, just less than the
	   column numbers alone suggest. Re-baseline against the post-fix
	   figures in flush-split-measurements.md, not the pre-fix ones. */
	box-sizing: border-box;
	width: 100%;
	max-width: var(--container);
	margin: 0 auto;
	/* The 15px is hand-duplicated from site-breadcrumb-utility-row.css's
	   own inset, not inherited from it: this element deliberately does NOT
	   carry `.joetennis-breadcrumb-utility-row` (that class sets
	   `display: flex`, which would destroy this grid), so nothing enforces
	   that the two stay equal. If the shared row's inset ever changes,
	   this value has to be changed with it by hand.

	   A shorthand is safe HERE, unlike on `.flush-split__chrome-bar`.
	   CAP-1's gotcha was specific to that element: it carries the shared
	   class, so a `padding` shorthand in this later-loading file would
	   reset the shared class's own `0 var(--band-gutter)` inset to zero at equal
	   specificity. This element owns its inset outright, so there is no
	   earlier declaration for a shorthand to silently half-undo.

	   The vertical half of the padding -- the 20px carousel-controls
	   clearance above the footer -- is NOT set here. It is desktop-only
	   and lives in the `min-width: 769px` block immediately below; a
	   base-level rule would leak it into the stacked mobile layout, which
	   is live-verified on a real iPhone and must stay untouched. */
	padding: 0 var(--band-gutter, 15px);
}

/* The 20px clearance between the carousel controls' bottom and the site
   footer's top edge (spec-flush-split-alignment CAP-3's success criterion;
   20px is this site's established rhythm unit, already used by the archive
   toolbar and the footer's own vertical padding).

   Desktop-only, and placed AFTER the base rule rather than in the
   `min-width: 769px` block at the top of this file -- a media query does
   not raise specificity, so from up there an equal-specificity
   `padding: 0 var(--band-gutter)` later in the file would simply win and
   silently zero this out again.

   It works because this element is a flex item that FILLS its parent: flex
   sizes its border box, so padding comes straight out of the grid
   row-tracks' content box and lifts the controls row by exactly 20px
   rather than growing the element. Measured live at 1235x1050: controls
   bottom 977.2, footer top 997.2.

   flush-split.js reads this padding back (`getSplitVerticalPadding`) for
   both its available-row-height and its one-screen-budget maths -- both of
   which measure this element's border box and would otherwise overcount by
   exactly this much. Change the value here and the JS follows
   automatically; do not hardcode 20 there. */
@media ( min-width: 769px ) {
	.flush-split__split {
		padding-bottom: 20px;
	}
}

/* --synced (spec-flush-split Story 2, CAP-1): applied by flush-split.js only when the
   active hero image's natural height, at the row's current width, leaves
   the media/card row shorter than its full available height (the split's
   own height minus whatever the controls row needs) *and* the card's
   real content minimum still fits at that shorter height (shadow-
   measured -- never estimated from font-size/line-count). Since media
   and card-col share one grid row-track, sizing that ONE track to
   `--flush-split-sync-height` sizes both of them identically and keeps
   them flush -- no per-element height rule needed the way the old flex
   version required. `align-content: center` centers that row within the
   split's own unchanged total height, leaving equal empty space above and
   below rather than pinning it to the top.

   CAP-4 (v1.11.19) changed TWO things here. The track list is one row, not
   two -- there is no controls row left to size `auto`. And what the JS
   syncs TO now includes the controls: `--flush-split-sync-height` is
   `renderedImageHeight + controlsHeight`, because the controls live inside
   the media column and the row has to hold both. The visible consequence,
   and it is the guarantee CAP-4 deliberately traded away: in the synced
   state the card's bottom is now level with the CONTROLS' bottom, not with
   the image's. */
.flush-split__split--synced {
	grid-template-rows: var(--flush-split-sync-height);
	align-content: center;
}

/* `min-height: 0` is load-bearing, not decorative -- live-verified
   2026-09-14 that without it, the "media" row-track ignores its `1fr`
   share entirely and grows to the hero image's full intrinsic size
   instead (879px measured, vs. ~519px actually available), overflowing
   `.flush-split__split`'s own height and pushing the controls past the
   real footer (they had their own grid row below the pair at the time;
   since CAP-4 they sit inside this element, but the overflow this guards
   against is unchanged). This is the standard grid/flex "automatic minimum
   size" rule for items containing a replaced element (the `<img>`) --
   the browser gives such an item a content-based minimum equal to the
   image's own intrinsic size unless `min-height: 0` (alongside
   `min-width: 0`, needed for the same reason on the cross axis)
   explicitly opts out of it. */
/* The GROUND the hero letterboxes against, added v1.11.17.
   `--color-bg-surface` is the card panel's own fill (:723, its only other
   consumer), so the media column and the card read as one continuous
   surface instead of the image leaving a hole beside the card.

   WHY THIS ELEMENT, and why a fill rather than a geometry fix. The image is
   `object-fit: contain` inside a box stretched to the shared grid row, so
   whenever the card is taller than the image's aspect ratio needs, the
   surplus shows as unpainted page ground below the image -- measured 258.8px
   at a 900px viewport on Flush Split and 287.5px on Flush Split White,
   against a 0px gap at 1604px. That surplus is what reads as a notch. The
   geometry half of the problem is CAP-4 of spec-flush-split-alignment
   (the controls follow the image's real bottom edge); this rule is the
   colour half and is independent of it -- it neither fixes nor depends on
   the controls' position.

   ONE DECLARATION, BOTH TEMPLATES, deliberately. Flush Split White already
   retargets this token to #3C5158 on the `.flush-split` root
   (flush-split-white.css:191), so the white template picks up its own value
   with no branch here. Verified live on both at 900px before shipping.
   The card keeps its identity where the two surfaces meet: its
   `1px solid var(--color-card-ring)` ring still draws the seam.

   NOT gated to desktop: at <=768px `.flush-split__media` is `height: auto`
   so it hugs the image and there is no slack for this to paint.

   STILL PAINTS AFTER CAP-4, and that is why this element deliberately
   keeps stretching to its grid row rather than gaining `align-self: start`
   to hug the image. Letting it hug would look like the tidier expression
   of "the controls follow the image", and it would silently unpaint the
   region beside the card -- undoing v1.11.17. So: the BOX stretches and
   keeps painting, the IMAGE hugs inside it, the controls follow the image,
   and the slack ends up below the controls instead of above them. Still
   painted, just no longer between the picture and the buttons. */

/* FLEX COLUMN as of CAP-4 (v1.11.19): hero image, then the carousel
   controls, which templates/flush-split.php now nests here. Direction
   `column` with the default `align-items: stretch` is all the stacking
   needs; the image's own `max-height` does the space reservation. */
.flush-split__media {
	grid-area: media;
	display: flex;
	flex-direction: column;
	min-width: 0;
	min-height: 0;
	background-color: var(--color-bg-surface);
}

/* Never cropped/stretched -- exactly one of {height maxes to the box} or
   {width maxes to the box} is the limiting edge, per implementation-
   notes.md's Layout mechanics; the other axis letterboxes. Right-anchored
   via `object-position` so the image's right edge always touches the card
   column, matching the comp's confirmed-correct proportions.

   TOP-anchored on the cross axis, not centred. When the card column pushes
   the shared grid row taller than this image's aspect ratio needs, the slack
   appears as vertical letterbox. Centred, that slack splits evenly and the
   picture's top edge floats BELOW the card's top edge by half of it -- which
   is what reads as the hero sitting low, and it grows as the row grows.
   Measured live on Auto Loan Future 2026-09-19 with the row forced to the
   640px height the handoff recorded: 71.88px of slack, picture top 35.94px
   below the card top at `center`, exactly 0.00px at `top`. Where there is no
   slack -- the common case at wide viewports, where the media box already
   matches 16:9 -- this changes nothing, so it carries no regression risk.
   The `right` half is unchanged and still load-bearing. Width/height
   attrs (ACF `width`/`height`, output by flush-split.php) give the
   intrinsic aspect ratio flush-split.js reads at parse time -- this rule
   only controls the rendered box, not the ratio used for sizing math.
   `height: auto` REPLACED `height: 100%` in CAP-4 (v1.11.19), and this is
   the load-bearing half of that story -- not the DOM move. Stretched to
   the row at `height: 100%`, this element's BOX was taller than its
   PAINTED pixels whenever the card drove the row past the image's aspect
   ratio, and `object-position: ... top` put the whole difference at the
   bottom. Anything placed after the image inherited that slack, so simply
   nesting the controls here would have relocated the gap rather than
   closed it. At `height: auto` the box IS the picture: `width: 100%` plus
   the intrinsic ratio (from the `width`/`height` attributes
   flush-split.php emits) makes the two coincide, and the gap below the
   image becomes exactly the controls' own `padding-top`, at every
   viewport, in both the synced and the bailed state.

   `max-height` is what keeps the pair inside the row. `--flush-split-controls-h`
   is published by flush-split.js from the controls' measured height; the
   `0px` fallback is correct, not defensive, for both cases that can hit it
   -- a 0/1-image project, where PHP emits no controls at all, and a
   JS-less render, where `.has-js-controls` is never added and the controls
   stay `display: none`. In both, the image legitimately gets the full row.
   The percentage resolves against this element's flex container
   (`.flush-split__media`), whose height is definite from its grid
   row-track. At <=768px that height is `auto`, so the percentage resolves
   to `none` and the image runs at its natural ratio -- which is exactly
   what the stacked layout wants, so no override is needed there.

   `min-height: 0` and `flex-shrink: 0` both defend the `max-height`.
   A flex item defaults to `min-height: auto`, which for a replaced element
   is its INTRINSIC height -- and a minimum beats a maximum, so without the
   explicit `0` a tall source image could push straight past the cap. And a
   shrunk box would no longer match the picture's ratio, at which point
   `object-fit: contain` re-letterboxes inside it and the gap comes back.
   Same family of reasoning as the `min-height: 0` on `.flush-split__media`
   above, one level down.

   `object-fit: contain` is now a no-op in the common case -- box and ratio
   agree -- and stays declared for the one case where they do not: when
   `max-height` binds, the box is wider than the ratio needs and the image
   letterboxes HORIZONTALLY, anchored right by `object-position`, with
   flush-split.js's `left` nudge re-centering the controls on the picture's
   real width. */
.flush-split__hero-image {
	display: block;
	width: 100%;
	height: auto;
	max-height: calc(100% - var(--flush-split-controls-h, 0px));
	min-height: 0;
	flex-shrink: 0;
	object-fit: contain;
	object-position: right top;
}

.flush-split__hero-image[hidden] {
	display: none;
}

/* ---------------------------------------------------------------------
   Carousel chrome -- ported from single-project.css:246-304 (the shared
   P/S/R + Details carousel button/counter/dot family), renamed to this
   template's own prefix. Only the button + counter pieces are used here
   -- this carousel is prev/next + "N of N" only, no dot pagination, so
   the source family's `.carousel-dot` rules were left unported rather
   than carried over unused; a future story can port them if dots are
   ever added. `.has-js-controls` is added by flush-split.js only after
   it successfully initializes the carousel, so if JS fails to load, the
   (otherwise inert) prev/next buttons never appear -- only the first
   hero image renders, same graceful-degradation shape as the ported
   pattern's own no-JS fallback. PHP only emits this markup at all when
   `hero_shots` has 2+ images (flush-split.php), so 0/1-image posts never
   have controls to gate in the first place.
   ------------------------------------------------------------------ */

/* CAP-4, v1.11.19: back inside `.flush-split__media` as its second
   flex-column child, reversing the 2026-09-14 correction that had made
   this its own grid area. The full reasoning -- which guarantee was traded
   for which, and why the old objection to nesting no longer applies -- is
   in the `.flush-split__split` header comment above; do not re-derive it
   from this rule alone.

   `grid-area: controls` is GONE, not merely unused: this element is not a
   grid item any more, and no `controls` area exists for it to name.

   `width: 100%` replaced `calc(100% - var(--flush-split-card-col-width))`.
   That calc existed to make an element spanning both grid columns measure
   only the media one -- arithmetic that had to be kept in step with the
   card column's clamp. Inside the media column, 100% IS the media column,
   so the coupling is gone. Note that this is the one place CAP-4 touches
   the card column's width token, and it touches it by deleting a
   reference, not by changing a value.

   `padding: 16px 0` is now load-bearing in a way it was not before: its
   TOP half is the image-to-controls gap that CAP-4's acceptance criterion
   measures as constant. Change it and that gap changes.

   `position: relative` is kept ONLY so flush-split.js can nudge this
   horizontally (`left`) to center it on the active image's own real width,
   which can be narrower than this box when `max-height` binds and the
   image letterboxes sideways -- that offset never removes this from the
   document flow. Gated by `.has-js-controls` on THIS element. */
.flush-split__carousel-controls {
	display: none;
	position: relative;
	width: 100%;
	align-items: center;
	justify-content: center;
	gap: 24px;
	padding: 16px 0;
}

.flush-split__carousel-controls.has-js-controls {
	display: flex;
}

.flush-split__carousel-btn {
	/* The FILL is retired to site-interactive-states.css too (story 5),
	   as `--state-carousel-rest-bg`. The `background: transparent`
	   shorthand that used to sit here is gone deliberately: it was a
	   literal doing a token's job, and at (0,1,0) it would have lost to
	   the shared rest rule anyway the moment a ground bound a real
	   fill. Declaring nothing here is what lets the ground decide. */
	/* Geometry only. Edge COLOUR, ink, radius and weight are retired to
	   site-interactive-states.css (SPEC-interactive-color-states story 2);
	   the rest edge still renders `rgba(0, 229, 255, 0.3)`, now as
	   `--state-carousel-rest-bd`. Longhands, not the `border` shorthand:
	   the shorthand would set border-color to currentColor and beat the
	   shared rule from later in the cascade. */
	border-width: 1px;
	border-style: solid;
	/* `padding: 0` IS THE FIX; `box-sizing` IS BELT AND BRACES (story 5).
	   Neve already sets `box-sizing: border-box` on every `button`, so
	   only the padding line changes the used width -- it is declared here
	   for the same reason simple-stack.css:425-431 declares it, explicit
	   rather than inherited, because the 40x40 metric is alignment-load-
	   bearing. Neve sets `padding: 12px 32px` on every `button`. Under border-box
	   that padding plus the 1px edges is 66px wide, which is MORE than the
	   40px declared below -- and a used width can never be less than its
	   own padding box, so the declaration was silently floored and this
	   button rendered 66x40. `--shape-carousel-radius: 999px` then clamped
	   to half the shorter side and produced a stadium instead of the
	   circle story 4 intended. simple-stack.css had `padding: 0` already,
	   which is the only reason one of the three was ever round. Measured
	   live 2026-09-19 at 66x40 before, 40x40 after. */
	box-sizing: border-box;
	padding: 0;
	width: 40px;
	height: 40px;
	font-size: 20px;
	line-height: 1;
	cursor: pointer;
}

.flush-split__carousel-counter {
	font-family: var(--font-body);
	font-size: 14px;
	color: var(--color-text-body);
}

.flush-split__card-col {
	grid-area: card;
}

/* ---------------------------------------------------------------------
   TABBED CONTENT CARD -- About / Case Study, and The Team from story 3
   (SPEC-content-module-tabs, v1.13.10).

   IT WAS A FLIP CARD UNTIL NOW, and the mechanism it replaced is worth
   naming because three other templates still use it: a hidden
   `<input type="checkbox">` plus a `<label class="flush-split__card-scene">`
   wrapping the whole scene, driven by `:checked ~`. Pure CSS, no JS, two
   faces. Gone entirely here -- three discrete tab targets and a
   click-anywhere surface cannot coexist, so the scene element went with
   the mechanism rather than being kept as an empty wrapper.

   `.flush-split__card` absorbed the scene's sizing job (100% of
   `.flush-split__card-col`, which stretches to its grid row-track) and is
   now a FLEX COLUMN: a fixed 44px tab bar, then the panel wrapper taking
   the rest. Unlike Editorial Split's centered `max-width: 480px` card-wrap
   there is no wrap div here; the card column is the sizing context
   directly, which is what gives every `height: 100%` below a definite
   height to resolve against.

   THE PANEL PAINT MOVED UP ONE BOX, and this is the one structural change
   the tabs forced. `background-color` and `border` were on
   `-card-face-inner` for the card's whole life. With a bar above the panes
   that leaves the bar floating on the column ground rather than sitting in
   the card, and puts each pane's own top border directly under the bar's
   bottom border -- a doubled hairline. They are declared on
   `.flush-split__card` instead, which is also what lets the bar's
   `rgba(0, 0, 0, 0.14)` composite against the card surface the way it is
   specified to. `-card-face-inner` keeps everything else it had: the
   padding, the height, the internal scroll.
   ------------------------------------------------------------------ */

/* No `perspective`, and none since 2026-09-21: nothing here rotates in 3D.
   Leaving it would establish a containing block for descendants' fixed
   positioning and cost a compositing layer for no visual return.

   `overflow: hidden` is what makes the slide readable: the exiting pane
   travels a full card-width and would otherwise paint over the media
   column beside it. */
.flush-split__card {
	position: relative;
	display: flex;
	flex-direction: column;
	width: 100%;
	min-height: 100%;
	background-color: var(--color-bg-surface);
	border: 1px solid var(--color-card-ring);
	box-sizing: border-box;
}

/* `min-height` IN THE BASE, `height` ONLY WHEN TABBED, and the difference
   is the whole no-JS story at DESKTOP rather than on a phone.

   Unenhanced, every pane is in flow and stacked, so the card needs more
   height than its column gives it. A `height: 100%` card with
   `overflow: hidden` -- which is what this rule said while the card was a
   flip, because two faces overlapped and one of them was absolute -- would
   clip the second pane against a fixed box with nothing able to scroll it.
   Strictly worse than the flip it replaced, and invisible in testing
   because it only shows up with scripting off. `min-height` fills the
   column when there is room and grows past it when there is not; the 100vh
   cap's own release path then lets the page scroll, which is the behaviour
   it was built for.

   Once tabbed, exactly one pane is in flow-equivalent space and the card
   is bounded again -- which is what the two-column lockup needs. */
.flush-split__card--tabbed {
	height: 100%;
	overflow: hidden;
}

/* THE TAB BAR. 44px FIXED, border-box, so the 1px bottom border is inside
   the 44 rather than added on top -- flex centring does the rest and there
   is no padding arithmetic to hit the number.

   IT SHIPS CARRYING `hidden` AND flush-split.js REMOVES IT. There is no
   CSS gate here on purpose: `hidden` takes the bar out of the
   accessibility tree as well as off the screen, which a `display: none`
   rule keyed off a JS class would also do -- but the attribute says it in
   the markup, where the no-JS reader of this template will look.

   THE BACKGROUND MUST STAY AN ALPHA. `rgba(0, 0, 0, 0.14)` composites
   against whatever card surface is beneath it: #111827 on Flush Split,
   #3C5158 on Flush Split White. A fixed hex would bind the bar to one of
   them and camouflage on the other -- the same failure `--ground-edge` was
   introduced to fix when the header's #e9ecef hairline went from 1.19:1 on
   white to 1.07:1 once the #e4e4e4 band appeared beneath it. One alpha
   serves every ground; one hex serves one. */
.flush-split__card-tabs {
	display: flex;
	flex: none;
	height: 44px;
	box-sizing: border-box;
	border-bottom: 1px solid var(--color-card-ring);
	background: rgba(0, 0, 0, 0.14);
}

/* COLOUR ONLY -- no pill, no underline. The text alone carries state:
   white at rest, the primary accent on hover, and the current tab in
   #8cc201.

   #8cc201 WAS THE FRAME'S TRIAL VALUE AND WAS RULED IN AT v1.13.15, story
   3, against a rendered comparison rather than a description. It lives in
   `--color-tab-current`, which v1.13.15 put on this sheet's root because
   the same story coupled The Team's kickers to it and coupled means one
   source -- and which story 3 of SPEC-about-page moved to `:root` in
   site-interactive-states.css when the About medallion became a third
   consumer that cannot see this root. See the note where the declaration
   used to sit, at the top of this file. The
   warning that used to stand here still does in its narrow form: it is a
   COMPONENT value on a component surface, so it did not become a
   `--state-*` or `--ground-*` role and should not.

   `flex: 1 1 0` rather than intrinsic width: the tabs share the bar
   equally, so two tabs and three tabs both fill it without the template
   doing arithmetic, and a narrow viewport shrinks them evenly instead of
   wrapping. The narrow-width type step below is the only thing that needs
   a breakpoint.

   THE FOCUS OUTLINE POINTS INWARD, `outline-offset: -2px`. The bar is only
   44px tall and sits flush against the card's top edge, so an outward
   outline would collide with that edge and with the panel below it. The
   3px radius is the ring's own, not an affordance claim: this is not one
   of the four control categories Family C covers in
   site-interactive-states.css, and it does not paint a box at rest. */
.flush-split__card-tab {
	flex: 1 1 0;
	appearance: none;
	background: none;
	border: none;
	display: flex;
	align-items: center;
	justify-content: center;
	height: 100%;
	font-family: var(--font-heading);
	font-size: 13px;
	font-weight: 600;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: #ffffff;
	padding-inline: 8px;
	cursor: pointer;
	text-align: center;
	transition: color 0.2s ease;
}

/* SPECIFICITY (0,4,0) FOR A ONE-PROPERTY STATE, and Neve is the reason.
   Reported by Joe on the installed v1.13.11: hovering a tab washed the
   WHOLE tab blue, where the design calls for a cyan text treatment and
   nothing else.

   Neve paints `button:hover { background: var(--primarybtnhoverbg);
   color: var(--primarybtnhovercolor); border-color: ... }` at (0,1,1).
   This rule shipped as `.flush-split__card-tab:hover`, (0,2,0), so the
   COLOUR half was already being won -- the text did turn cyan, which is
   why the bug read as "an extra background" rather than "my rule lost".
   The background half was never contested at all: it was being held by
   the base rule's `background: none` at (0,1,0), which Neve's (0,1,1)
   hover outranks. One state, two properties, two different fights.

   `--primarybtnhoverbg` resolves to #2f5aae -- the palette's
   `nv-secondary-accent`. CLAUDE.md recorded that slot as inert, on the
   grounds that its only consumer is `neve_button_appearance.backgroundHover`
   and the child theme overrides it. That was true until this card put a
   bare `<button>` on the page for the first time. Corrected there.

   So: state every property Neve's hover states, at a specificity that
   clears it, rather than relying on the base rule to hold a property a
   state rule is contesting. */
.flush-split__card-tabs .flush-split__card-tab[role="tab"]:hover {
	background: none;
	border-color: transparent;
	color: var(--color-accent-primary);
}

/* SPECIFICITY (0,4,0), AND IT HAS TO BE. Measured live 2026-09-22 on the
   installed v1.13.10: this rule shipped as `.flush-split__card-tab:focus-
   visible`, (0,2,0), and LOST -- the tab took a black ring pushed 2px
   OUTWARD, colliding with the card's top edge, which is the exact thing
   the negative offset exists to prevent. Only `border-radius` survived,
   because nothing else declares it, which is what made the failure look
   like a half-applied rule rather than a lost one.

   WHAT BEAT IT, and it is worth knowing because the next enumerated ring
   on any focusable element with a `tabindex` will lose the same way:
   site-interactive-states.css's baseline ends with
   `[tabindex]:not([tabindex="-1"]):focus-visible`. `:not()` contributes
   the specificity of its ARGUMENT, so that selector is (0,3,0) -- not the
   (0,1,1) its three siblings (`a`, `button`, `summary`) carry. These tabs
   are the first elements in the theme to hold a roving `tabindex` and so
   the first to meet it.

   That baseline's own docblock says "a baseline that outranks an
   enumerated rule stops being a baseline" and forbids raising it. It is
   already outranking one, accidentally, via the `:not()`. Fixing that is a
   sitewide focus change on every element that carries a tabindex, so it is
   LOGGED in deferred-work.md rather than taken under a tabs story -- and
   this rule is raised to clear (0,3,0) outright rather than tying it and
   depending on the `$deps` load order to break the tie. */
.flush-split__card-tabs .flush-split__card-tab[role="tab"]:focus-visible {
	outline: 2px solid var(--color-accent-primary);
	outline-offset: -2px;
	border-radius: 3px;
}

.flush-split__card-tab[aria-selected="true"] {
	color: var(--color-tab-current);
}

/* THE NARROW-WIDTH TYPE STEP, v1.13.15. A third tab is what makes this
   necessary, so it is SCOPED TO THREE-TAB BARS -- a card showing two tabs
   keeps 13px at every width, which today is every project on these two
   templates, since nothing is authored yet. `:has()` is already used in
   this file (`#content:has(.flush-split)`), so this is not a new
   dependency.

   NOT THE PROTOTYPE'S BREAKPOINTS. It steps at 860px and again at 420px,
   which are tuned to its own layout, not this one. These numbers were
   MEASURED on the installed v1.13.12 by rendering the real bar in a
   true-width iframe and comparing the label against the space a third of
   the bar leaves after `padding-inline`:

     viewport   bar     per tab   available   `CASE STUDY`
     390px      339px   113.0px   97.0px      81.5px   fits
     375px      324px   108.0px   92.0px      81.5px   fits
     360px      309px   103.0px   87.0px      81.5px   fits
     320px      269px    89.7px   73.7px      81.5px   does not fit

   "Does not fit" rather than "overflows": the tab is a flex item with the
   default `min-width: auto` and its text may wrap, so it does not spill
   sideways -- it WRAPS to a second line inside a bar that is 44px tall and
   does not grow. That is the failure being avoided.

   Nothing is needed at desktop: the card column bottoms out at its
   `clamp()` floor of 360px, which gives the same 358px bar as a large
   phone and 103.3px per tab. The narrow end of the PHONE is the only
   place this breaks.

   THE BREAKPOINT IS 374px, NOT 344px, AND THE FONT IS THE REASON. Every
   number above is Noto Sans. The fallback stack measures `CASE STUDY` at
   89px -- 7.5px WIDER -- and the font is requested with `display=swap`,
   so the fallback is what actually paints for the first few hundred
   milliseconds of every cold load. Judged against 89px the bar fails at
   360px rather than 320px, so a breakpoint set from the webfont's own
   metrics would have shipped a real flash of broken layout on the single
   most common Android width. The step below clears the FALLBACK at every
   width down to 320px, with 4.4px to spare at the worst of them.

   It lives here rather than in the mobile section at the foot of this
   file because it is the tab bar's own breakpoint and shares nothing with
   the 768px column stack. */
@media (max-width: 374px) {
	.flush-split__card-tabs:has(.flush-split__card-tab:nth-child(3)) .flush-split__card-tab {
		font-size: 11.5px;
		letter-spacing: 0.01em;
		padding-inline: 5px;
	}
}

/* THE PANEL WRAPPER takes whatever the bar leaves.

   `overflow: hidden`, the height transition and `min-height: 0` are all
   scoped to `--tabbed` below rather than declared here, because without JS
   this wrapper holds every pane stacked in flow, must clip none of them,
   and must be allowed to make the card taller than its column. */
.flush-split__card-panels {
	position: relative;
	flex: 1 1 auto;
}

/* ===================================================================
   EVERYTHING BELOW IS GATED ON `.flush-split__card--tabbed`, WHICH ONLY
   flush-split.js ADDS, AND ONLY AFTER THE TABS ARE WIRED.

   This is the whole no-JS contract in one selector. Unenhanced, the panes
   are static boxes stacked in document order, every one of them visible
   and readable, and the card simply grows -- which is a complete page, not
   a degraded one. The overlap, the transforms, the clipping and the
   JS-driven height all switch ON from here.

   Same fail-open shape as `.has-js-controls` (the carousel buttons) and
   `html.flush-split-js` (the 100vh cap). Per-CARD rather than page-global,
   unlike the cap's flag: the tab state belongs to one split, and this
   file has never assumed there is only one per page.
   =================================================================== */

/* THE PANES OVERLAP so the slide has something to slide over, and so the
   card is the height of the wrapper rather than the sum of three panes.
   All of them are absolute -- there is no in-flow pane, which is the one
   place this departs from the flip card's front-in-flow/back-absolute
   asymmetry. That asymmetry existed to give the card a height; the
   wrapper's `flex: 1 1 auto` does that at desktop and JS does it at
   mobile, so nothing needs to stay in flow to hold the box open. */
.flush-split__card--tabbed .flush-split__card-panels {
	overflow: hidden;
	/* `min-height: 0` ONLY HERE. A flex item's default `min-height: auto`
	   floors it at its own min-content height, which for a stack of panes
	   is the TALLEST of them -- so in the base state it is exactly what
	   stops the unenhanced card clipping, and in the tabbed state it is
	   exactly what would stop the card being bounded at desktop. Same
	   declaration, opposite roles; it belongs on one side only. */
	min-height: 0;
}

.flush-split__card--tabbed .flush-split__card-face {
	position: absolute;
	inset: 0;
	width: 100%;
	transition: transform 0.55s cubic-bezier(0.65, 0, 0.35, 1);
}

/* `[hidden]` beats the absolute positioning above only because it is more
   specific AND later; the UA sheet's own `[hidden] { display: none }` is
   (0,1,0) and would lose to the (0,2,0) rule above it. State it here. */
.flush-split__card--tabbed .flush-split__card-face[hidden] {
	display: none;
}

/* The actual styled box, one level in from `.card-face`.

   WHY IT IS STILL A SEPARATE BOX. It was introduced so the fold-corner and
   dot -- siblings of this, inside `.card-face` -- could sit UNCLIPPED on top
   of the notch a `clip-path` cut away. Both the notch and the dog-ear were
   removed on 2026-09-21 (spec-flush-split-card-affordance), so that original
   reason is gone and this box now has no sibling to protect. It is kept
   anyway, deliberately and not by oversight: `flush-split.js`'s
   `measureCardMinHeight()` shadow-measures the card's real minimum height
   by cloning every one of these, and the mobile padding block targets this
   element's padding. (The desktop seam rule below used to target its border
   too; the border moved up to `.flush-split__card` at v1.13.10 and the seam
   rule moved with it.) Collapsing it into `.card-face` is a real refactor with its
   own blast radius across CSS and JS, explicitly out of scope there, and
   recorded as a finding rather than taken in passing. */
/* `overflow-y: auto` added 2026-09-14 (Joe's feedback): with no overflow
   handling, content taller than this box's 100% height was silently
   clipped -- at the time by the `clip-path` fold-corner shapes, which
   followed this box's own height; since 2026-09-21 by
   `.flush-split__card`'s `overflow: hidden` -- with no way to reach it, a
   real gap whenever content is long, the window is short, or the user's
   font size is large. The reason is different, the rule still earns its
   place, and it is now the ONLY thing that lets a long back face be read.
   (The sentence that used to follow, about not interfering with the
   fold-corner and dot, described siblings that no longer exist.) */
.flush-split__card-face-inner {
	box-sizing: border-box;
	height: 100%;
	padding: 40px;
	overflow-y: auto;
	display: flex;
	flex-direction: column;
}

/* THE LEFT EDGE OF THAT RING IS DROPPED ON DESKTOP (v1.11.19), because
   v1.11.17 took away the thing it was drawing against.

   Reported by Joe as "a little flickering 1px vertical line in the edge
   between the two columns", and diagnosed live on the installed v1.11.18
   rather than reasoned about: painting this border red made it appear at
   exactly the column seam, and painting `.flush-split__split`'s own ground
   magenta showed NO magenta there -- so it is this ring, not a sub-pixel
   layout gap. Measured `.flush-split__card-col`.left - `.flush-split__media`.right
   == 0.0000; the tracks abut exactly.

   WHY IT ONLY APPEARED IN v1.11.17. Before the fill, this ring separated
   the card's `#111827` panel from the page ground `#0a0e1a` -- a real
   tonal step, and the ring read as the card's own edge. The fill made
   `.flush-split__media` the SAME `var(--color-bg-surface)` as this panel,
   which is the entire point of it ("so the two read as one continuous
   surface"). That erased the step and left this hairline stranded in the
   middle of a continuous field. v1.11.17's comment on `.flush-split__media`
   anticipated it and called it deliberate -- "the card keeps its identity
   where the two surfaces meet: its ring still draws the seam". Joe ruled
   against that on 2026-09-21: if the two are one surface, there is no seam
   to draw.

   WHY IT FLICKERED, which is the half that made it a defect rather than a
   preference. `--flush-split-card-col-width` resolves to `32vw` through
   most of this range, and `32vw` is `8w/25` -- an integer only when the
   viewport width is a multiple of 25. So at 24 widths out of 25 the seam
   sits at a fractional x (913.171875 measured at a 1387px viewport), and a
   1px border at a fractional device-pixel offset is antialiased across two
   physical pixels. Every relayout shifts the fraction and the line
   shimmers. (Until 2026-09-21 `.flush-split__card` was ALSO a `preserve-3d`
   rendering context under the scene's `perspective`, rasterizing on its own
   layer and resampling independently, which compounded this. The slide
   removed both, so that contribution is gone -- but the fractional-x
   antialiasing above stands on its own and is why this rule is still
   needed.) Note that the only clean fix for a FLICKERING
   hairline would be snapping the seam to a whole pixel -- i.e. changing
   the card column's width -- which CAP-4 is explicitly fenced out of.
   Removing the edge sidesteps that entirely: there is nothing left at that
   x to antialias, so the flicker is gone by construction, not damped.

   RETARGETED v1.13.10 from `-card-face-inner` to `.flush-split__card`,
   because that is where the ring now lives -- see the tabbed-card docblock
   above for why the paint moved up a box. Same edge, same pixel, same
   reason; only the selector changed, and deleting this rule instead would
   reopen the shipped v1.11.19 defect described here.

   `transparent`, NOT `border-left-width: 0`. The border box has to keep
   its 1px, or the card's 40px padding box shifts left by a pixel and every
   line of card text reflows against a different measure. A transparent
   border shows this element's own `background-color` through it
   (`background-clip` is `border-box` by default), which is the same
   `var(--color-bg-surface)` the media column is painting -- so the strip
   renders as more of the continuous surface, which is exactly the intent.

   DESKTOP ONLY, and the breakpoint is load-bearing rather than habitual.
   Below 769px the layout stacks, `.flush-split__media` is `height: auto`
   so it hugs the image and paints no field beside anything, and all four
   of this card's edges meet page ground again -- where the full ring is
   still doing its job. Scoping this to `min-width: 769px` is what keeps
   the mobile card's left edge visible.

   REACHES FLUSH SPLIT WHITE TOO, deliberately. That template retargets
   `--color-bg-surface` to #3C5158 for BOTH consumers, so it strands this
   same hairline on its own ground and needs the same answer. Its own
   comment about the prototype sampling a 1px edge "on all four sides" is
   annotated with this exception rather than left to contradict the code. */
@media ( min-width: 769px ) {
	.flush-split__card {
		border-left-color: transparent;
	}
}

/* The flex column that used to be declared here, on `--front` and `--back`
   by name, is folded into the base `-card-face-inner` rule above as of
   v1.13.10 -- one declaration for every pane, so story 3's third pane needs
   no new selector and cannot be forgotten. It was a pair because the flip
   cue was pushed to the bottom with `margin-top: auto`; the cue is gone and
   the column is kept, because the panes' own content still stacks.

   The `clip-path` polygons that used to head those rules, and the
   `padding-right: 56px` / `padding-left: 56px` beside them, went on
   2026-09-21. The polygons notched a corner out of each face for the
   dog-ear to sit in; the asymmetric padding existed only to keep text from
   running into that 44px notch. With no notch there is nothing to clear, so
   `padding: 40px` applies evenly on all four sides. Do not reintroduce
   either as a pair without the dog-ear: 56px of padding on one side of one
   pane with nothing in it reads as a typo. */

/* THE CLICK CUES ARE GONE, v1.13.10 (SPEC-content-module-tabs story 1).
   `.flush-split__flip-cue` painted "More >" and "< Back" at the bottom-left
   of each face and was this card's only visible signal that it turned. The
   card does not turn any more; the tab bar names all three destinations at
   once, which is the whole point of the change, so a cue would be a second
   and weaker way of saying what the bar already says.

   TWO OVERRIDES DIED WITH IT and were removed in the same change, rather
   than left to accumulate: flush-split-white.css's rule 5d (the white
   template's own rest/hover pair, #FFFFFF and #00e5ff on the slate), and
   `.flush-split__card-scene { --state-nav-hover-fg: #a855f7 }` in
   site-interactive-states.css, which existed solely to hold this cue's
   hover above the marker purple's 1.33:1 on the slate.

   THE 2.11:1 CELL THAT REBINDING RECORDED IS NOW MOOT, not fixed. Nothing
   on this card reads a nav role any more, so the slate has no purple cell
   left to close. The underlying gap -- that #3C5158 has no purple clearing
   4.5:1 -- still stands for anything else that lands there, and is recorded
   in site-interactive-states.css where the binding used to be. */

.flush-split__headline {
	font-family: var(--font-heading);
	font-weight: 700;
	font-size: 48px;
	line-height: 1;
	letter-spacing: -0.03em;
	color: var(--color-text-heading);
	margin: 0 0 20px;
}

.flush-split__subhead {
	font-family: var(--font-body);
	font-weight: 400;
	font-size: 18px;
	line-height: 1.6;
	color: var(--color-text-muted);
	margin: 0;
}

.flush-split__psr {
	list-style: none;
	margin: 0 0 24px;
	padding: 0;
	flex: 1 0 auto;
}

.flush-split__psr-row {
	margin: 0 0 16px;
}
.flush-split__psr-row:last-child {
	margin-bottom: 0;
}

.flush-split__psr-label {
	display: block;
	font-family: var(--font-heading);
	font-weight: 600;
	font-size: 13px;
	letter-spacing: 0.02em;
	text-transform: uppercase;
	color: var(--color-psr-label);
	margin: 0 0 4px;
}

.flush-split__psr-value {
	display: block;
	font-family: var(--font-body);
	font-size: 16px;
	line-height: 1.5;
	color: var(--color-text-body);
}

.flush-split__clip {
	font-family: var(--font-body);
	font-style: italic;
	font-size: 18px;
	line-height: 1.5;
	color: var(--color-text-muted);
	margin: 0 0 24px;
	padding: 0;
	border: none;
}

/* ---------------------------------------------------------------------
   THE TEAM pane (SPEC-content-module-tabs story 3, v1.13.15).

   THE PROTOTYPE IS AUTHORITATIVE FOR THIS PANE AND ONLY THIS PANE. The
   Figma frame's own rule is that type and spacing come from production
   rather than from the artifact -- but The Team has no production
   precedent to defer to, so its block and divider spacing are the
   prototype's, read from artifact db1d1e4a. None of it is applied to
   About or Case Study, which keep what they already have.

   TWO VALUES HERE ARE NOT IN tab-measurements.md, and one of them
   CORRECTS it:

   - The between-blocks divider (26/24/1px) is absent from the companion
     entirely. It is `.panel-block + .panel-block` in the prototype.
   - The companion puts the table's row separator and its `padding-top`
     on `tr`. PADDING DOES NOT APPLY TO A `tr`, so transcribed literally
     the 10px would have silently done nothing and the rows would have sat
     tighter than drawn. The prototype puts both on the CELLS; that is
     what is built here.
   ------------------------------------------------------------------ */

/* THE DIVIDER IS A SIBLING RULE, AND THAT IS LOAD-BEARING. `+` means the
   role-only and team-only cases lose the divider for free -- there is no
   second block for it to match -- so neither partial state needs a guard
   of its own and flush-split.php renders no divider element at all. */
.flush-split__panel-block + .flush-split__panel-block {
	margin-top: 26px;
	padding-top: 24px;
	border-top: 1px solid var(--color-card-ring);
}

/* COUPLED TO THE CURRENT TAB BY RULING, not by coincidence -- see
   `--color-tab-current`, declared on `:root` in site-interactive-states.css
   since story 3 of SPEC-about-page and noted at the top of this file where
   it used to be declared. The kicker names the pane
   you are standing in, so it echoes the tab you pressed to get here.

   `letter-spacing` is STATED because Neve paints tracking onto `p` from a
   low-specificity element selector that nothing in this repo declares.
   This rule is (0,1,0) and wins, but the value has to exist to win with.
   The role copy and the table cells below deliberately do NOT state it:
   they are body copy and take the same tracking `.flush-split__subhead`
   already takes, which is the card's own existing treatment. */
.flush-split__panel-kicker {
	font-family: var(--font-heading);
	font-weight: 600;
	font-size: 12px;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--color-tab-current);
	margin: 0 0 10px;
}

.flush-split__role-copy {
	font-family: var(--font-body);
	font-size: 15px;
	line-height: 1.65;
	color: var(--color-text-body);
	margin: 0;
}

/* THE CREDITS TABLE. `table-layout: fixed` plus the 38/62 split is what
   keeps the two columns from re-flowing per project as names change
   length; `overflow-wrap: break-word` on both cells is what stops a long
   single-word name blowing the fixed track out.

   THE `th, td` RESET IS NOT DEFENSIVE BOILERPLATE. This is the first
   `<table>` any project template in this theme renders, so it is the
   first thing to meet what the parent theme paints on bare cells.
   MEASURED on the installed v1.13.12, by injecting a bare table into this
   card and reading the matched rules -- all of these are LIVE, all at
   (0,0,1), and Neve's sheet loads AFTER every child sheet, so only
   specificity clears them:

     td, th        padding 20px, text-align left    style-main-new.min.css
     th            padding-bottom 10px              style-main-new.min.css
     td            font-size 0.9em, and a 2px
                   border-bottom in the palette's
                   own nv-text-color               style-main-new.min.css
     table, th, td color and border-color bound
                   to the ground-ink/ground-edge
                   roles                     site-interactive-states.css

   (Written as prose rather than as CSS on purpose: `checks/05` greps this
   directory for anything shaped like a declaration reading the Neve
   palette, and cannot tell a comment from a rule. A faithful transcript
   of Neve's `td` rule fails that check. The seam it guards is real, so
   the quote bends rather than the check.)

   THREE OF THOSE WOULD HAVE SHIPPED AS BUGS:

   - `border-bottom: 2px` plus `border-collapse: collapse` draws a 2px
     rule under every row, in `--nv-text-color` (#16191d) -- near-black on
     a dark card, so it reads as a smudge rather than as an obvious fault.
     `border: 0` kills it and the `tr + tr` rule below puts back the one
     hairline this design actually has.
   - `font-size: 0.9em` applies to the CELL, and a direct declaration
     beats inheritance -- so the 14px on the table above would have
     reached the `<th>` intact and reached the `<td>` as 12.6px. The two
     columns of one row at two sizes. `font-size: inherit` is what makes
     the table's own size mean what it says.
   - `padding: 20px` against the prototype's 7px.

   AND ONE IS THIS THEME'S OWN: `site-interactive-states.css` already
   binds table ink and border-colour to the GROUND roles, which are the
   page's, not this card's. The card is a dark panel on a white page, so
   inheriting the page's ink here would be wrong on both templates. The
   explicit colours below are what stop it. */
.flush-split__credits {
	table-layout: fixed;
	width: 100%;
	border-collapse: collapse;
	font-size: 14px;
	margin: 0;
}

.flush-split__credits th,
.flush-split__credits td {
	border: 0;
	font-size: inherit;
	vertical-align: top;
	overflow-wrap: break-word;
	font-family: var(--font-body);
}

/* THE TRACKS ARE ON THE `<colgroup>`, NOT ON THE CELLS. `table-layout:
   fixed` reads its column widths from the FIRST ROW -- and the first row
   can be a roleless credit, which renders as a single `colspan="2"` cell
   and establishes no per-column width at all. Widths declared on `th`/`td`
   would then never be seen, and every row below the first would lay out at
   the browser's default split. A `<colgroup>` is read before any row, so
   the 38/62 holds whatever order the credits arrive in.

   This shipped on the cells first and the review caught it. The live check
   that "the roleless row spans the full 263px" did not: it was measured
   with a complete row ABOVE it, which had already set the tracks. */
.flush-split__credits-col-role { width: 38%; }
.flush-split__credits-col-name { width: 62%; }

/* The role. `<th scope="row">` is the honest semantic for a labelled
   pair, so it is a row header that happens to be styled quietly -- the
   heading ink rather than a second accent, because the kicker above is
   already carrying the pane's one accent. */
.flush-split__credits th {
	text-align: left;
	font-weight: 600;
	color: var(--color-text-heading);
	padding: 7px 12px 7px 0;
}

.flush-split__credits td {
	padding: 7px 0;
	color: var(--color-text-body);
}

/* A row whose name was authored with no role spans both columns rather
   than emitting an empty `<th>` -- flush-split.php decides that; this is
   the padding half of it. The width comes from the colgroup spanning both
   tracks, so nothing needs restating here. */
.flush-split__credits td[colspan="2"] {
	padding-right: 0;
}

/* THE ROW SEPARATOR SITS ON THE CELLS, NOT THE `tr`. See the header
   comment: `padding-top` on a `tr` does nothing, which is what made the
   companion's version fail silently rather than loudly. */
.flush-split__credits tr + tr th,
.flush-split__credits tr + tr td {
	border-top: 1px solid var(--color-card-ring);
	padding-top: 10px;
}

/* ---------------------------------------------------------------------
   Mobile -- stacks to a single full-width column, image above/card
   below. Matches this theme's one existing breakpoint convention
   (single-project.css, site-footer.css: max-width: 768px).
   ------------------------------------------------------------------ */

@media (max-width: 768px) {
	/* The band declarations added to `.flush-split__split`'s base rule
	   (`width`/`max-width`/`margin`/`padding`, spec-flush-split-alignment
	   CAP-1) deliberately still
	   apply at this breakpoint and are NOT reset here: `--container`'s
	   mobile tier (748px) never caps below the viewport, so the live effect
	   is the shared `--band-gutter` inset, which is exactly what the header's
	   own logo and hamburger sit on. That does mean the hero image is no longer
	   edge-to-edge on mobile as it was before that change — intended, per the
	   spec's small-viewport acceptance criterion, not an oversight.
	   Flagged here because this repo has been bitten before by a base-rule
	   restructure that missed the media block referencing it: if the base
	   band rule above is ever changed, re-read this block too.

	   RE-READ DONE 2026-09-28 (v1.14.0, the sitewide band gutter), WHICH IS
	   THIS BLOCK'S TRIPWIRE FIRING AS DESIGNED. The base rule's shorthand
	   changed -- `0 15px` became `0 var(--band-gutter, 15px)` -- so this
	   block was re-read, and nothing here needs a change: it still sets no
	   padding on this element, so there is still no shorthand to collide
	   with, and the inset it describes simply became a ramping value. At the
	   768px top of this breakpoint the gutter is `4vw` = 30.7px, up from
	   15px, so the stacked hero image insets about 16px further each side
	   than it did. Intended, and the same value the header above it takes.

	   RE-READ DONE 2026-09-16 (spec-flush-split-alignment CAP-3, corrected
	   in review round 2): the base shorthand was unchanged at `0 15px` then,
	   and the horizontal 15px inset applied here exactly as described
	   above. CAP-3's 20px carousel-controls clearance is NOT part of it and
	   does NOT reach this breakpoint — it is a `padding-bottom` longhand in
	   a `min-width: 769px` block placed after the base rule, so the stacked
	   layout's rendered height is byte-identical to v1.8.5 (measured: doc
	   height 1093 and split height 746.3 in both). Nothing in this block
	   sets padding on this element, so there is no shorthand to collide
	   with.

	   An earlier draft of this comment claimed the shorthand had become
	   `0 15px 20px` and that the 20px was deliberately kept on mobile.
	   Both were false, and they contradicted the sibling comment on the
	   base rule. Recorded rather than silently deleted because this block
	   exists specifically to catch that class of drift and had just been
	   the thing that drifted. */

	/* Stacks the two remaining areas into a single column. Joe's order --
	   image, then controls (which interact with the image immediately
	   above them), then the card text -- is unchanged, but as of CAP-4
	   (v1.11.19) only the first and last of those three are grid areas.
	   The controls sit between them for free, nested inside
	   `.flush-split__media` in source order. This block used to reassign
	   three areas to get that order; two is now enough, and the ordering
	   can no longer drift between breakpoints because it is no longer
	   restated per breakpoint. */
	.flush-split__split {
		grid-template-columns: 1fr;
		grid-template-rows: auto auto;
		grid-template-areas:
			"media"
			"card";
		/* 20px between the bottom of the carousel controls and the top of
		   the text card (Joe, 2026-09-22). Expressed as the gap between the
		   two stacked rows rather than as padding on either, so the number
		   is the distance it names.

		   READ IT WITH THE `padding-bottom: 0` ON THE CONTROLS BELOW. Those
		   carry `padding: 16px 0` at every width; left alone, the visible
		   distance here would be 16 + 20 = 36px and this declaration would
		   be a lie. The controls keep their TOP padding, which still
		   separates them from the image. */
		row-gap: 20px;
	}

	/* Sync logic is desktop-only (Story 2 Always) -- flush-split.js
	   actively clears `--synced` and the custom property once the window
	   crosses this breakpoint, but this stacked-layout override is kept
	   as a CSS-only backstop too: even if a stale `--synced` class/custom
	   property ever survived into this breakpoint, the row-track sizing
	   is forced back to this layout's own stacked defaults here, so
	   leftover synced state can never visibly affect the mobile column. */
	.flush-split__split--synced {
		grid-template-rows: auto auto;
		align-content: normal;
	}

	/* CORRECTED 2026-09-16 (review round 1): an earlier version of this
	   block claimed to be a mobile backstop against a stale cap. It was
	   not one, and could not be -- a rule that only fires when the
	   `flush-split-cap-released` flag IS present protects nothing against
	   the case that would actually hurt, which is the flag being ABSENT
	   while a cap applies. Read honestly, it restated the mobile default
	   under a condition that never needs it.

	   What actually protects mobile is structural and lives elsewhere:
	   the cap is scoped to `min-width: 769px`, so it never applies at this
	   breakpoint whatever classes are on `<body>`; flush-split.js removes
	   the flag on its own ≤768px early return; and the `--synced` backstop
	   directly above is a real backstop, because the sizing it neutralises
	   is set by an inline custom property with no width scoping of its own.

	   So the rule is gone rather than reworded. Kept as a note because
	   this file's header records that an unscoped cap once shipped and
	   genuinely broke mobile Safari, and the next person to widen the
	   cap's scope needs to find this reasoning here rather than a
	   comment that implies mobile is already defended. */

	/* THE MEDIA ROW DROPS ITS FILL AT THIS BREAKPOINT, v1.13.6 (Joe).

	   At desktop this column paints `--color-bg-surface` (the slate on
	   Flush Split White, #111827 on Flush Split) and that fill is
	   load-bearing: v1.11.17 added it so the slack beside a
	   `object-fit: contain` image reads as panel rather than as a notch of
	   unpainted page, and the carousel controls sit on it.

	   Stacked, neither reason survives. The controls come to rest ABOVE the
	   text card rather than beside it, so they no longer need the panel to
	   sit on; and the image hugs its own aspect ratio here (`height: auto`
	   directly below), so there is no slack for a fill to paint. What is
	   left is a slate band behind a letterboxed image and a row of buttons,
	   for no reason. `transparent` hands the row back to the page floor --
	   white on Flush Split White, #0a0e1a on Flush Split.

	   THE CONTROLS' COLOURS MOVE WITH IT, and not from here:
	   site-interactive-states.css releases `.flush-split__media`'s carousel
	   token re-binding at this same breakpoint, so they take the ground's
	   own values. The two halves are deliberately in different files --
	   values there, geometry here -- and they must move together. Change
	   one, check the other.

	   NOT A PROBLEM FOR THE HERO IMAGE. Joe, 2026-09-22: the ground behind
	   the image "doesn't really matter since it's hidden" -- the image
	   covers its own box at this width and any fill behind it is only ever
	   visible as letterboxing, which the `height: auto` hug removes. */
	.flush-split__media {
		width: 100%;
		height: auto;
		background-color: transparent;
	}

	.flush-split__card-col {
		width: 100%;
	}

	/* `.flush-split__carousel-controls { width: 100% }` used to sit here,
	   overriding the desktop `calc(100% - var(--flush-split-card-col-width))`
	   for the stacked column. CAP-4 (v1.11.19) made `width: 100%` the base
	   value, so this override became a restatement and is removed rather
	   than left to look like it is doing something. */

	/* The other half of the 20px above: zero the bottom padding so the
	   row-gap IS the gap. Top padding is untouched -- it separates the
	   controls from the image, which is a different job and still needed. */
	.flush-split__carousel-controls {
		padding-bottom: 0;
	}

	/* AND THE RESERVATION GOES WITH THEM, which is the part that is NOT
	   obvious and cost a measurement to find.

	   The base rule caps this image at `calc(100% - 72px)` -- the height of
	   the controls (40px button + 16px padding top and bottom) held back so
	   a stretched media column cannot let the image overlap them. At
	   desktop that is load-bearing. Here it is not, and it is actively
	   wrong in two ways.

	   FIRST, IT IS A FIXED 72 AGAINST A NOW-56px CONTROL. Zeroing the
	   bottom padding above does not remove 16px, it MOVES it: the
	   reservation still holds back 72, the controls now occupy 56, and the
	   leftover 16 sits as dead space at the foot of the column. Measured
	   live before this line existed: 36px from the button's bottom edge to
	   the card, not the 20 the row-gap claims.

	   SECOND, AT THIS BREAKPOINT THE PERCENTAGE IS CIRCULAR. `.flush-split__media`
	   is `height: auto` here, so it hugs its content -- and its content is
	   sized from a percentage of its own height. Browsers resolve that, but
	   it is not stable: correcting the constant to `calc(100% - 56px)` and
	   re-measuring FROZE the renderer outright (CDP timeout, 2026-09-22).
	   `none` removes the circularity instead of perturbing it, and does not.

	   So the image simply takes its natural size, the controls follow it,
	   and the column's bottom edge IS the button's bottom edge -- which is
	   what makes the 20px above the number it says it is. Verified live at
	   390px on both templates: gap 20.0px, zero leftover.

	   DO NOT PORT THIS ABOVE THE BREAKPOINT. The reservation is correct at
	   desktop and removing it there reopens the overlap it exists to
	   prevent. */
	.flush-split__hero-image {
		max-height: none;
	}

	.flush-split__headline {
		font-size: 32px;
	}

	/* Targets `-inner`, NOT the outer `.card-face` -- which is where this
	   rule lived once and must not go back to. `.card-face` carries no
	   padding of its own by design; padding it inflated the outer box 56px
	   (28+28) beyond `-inner`'s real content height, and at this breakpoint
	   only that detached the old fold-corner from the card's visible bottom
	   edge. The dog-ear is gone as of 2026-09-21, so that particular symptom
	   cannot recur -- but the structural reason stands: `-inner` is the box
	   with the padding and the scroll, so it is the box that takes the
	   padding. (It carried the background and the border too until
	   v1.13.10, when both moved up to `.flush-split__card` so the tab bar
	   could sit inside the card rather than above it.)

	   EVEN ON ALL FOUR SIDES as of 2026-09-21, mirroring the desktop rule.
	   The `padding-right: 44px` / `padding-left: 44px` that used to follow
	   were 28+16, clearance for the 44px notch on the corner side of each
	   face. No notch, no clearance needed. */
	.flush-split__card-face-inner {
		padding: 28px;
	}

	/* `height: auto` IS WHAT MAKES THE HEIGHT TWEEN MEASURABLE, and it is
	   the trap in this whole feature. At desktop this box is `height: 100%`
	   of a pane the column has already sized, and it scrolls internally.
	   Leave that in place here and an incoming pane reports the height it
	   is ALREADY constrained to rather than the height of its content --
	   so the card could grow but never shrink, and a short pane would keep
	   a long pane's height. Freeing it is also what stops a pane scrolling
	   inside itself on a phone, which was the original complaint behind
	   ROLLBACK-A-STACKED-CARD.

	   Applies to the base AND the marked block below, deliberately: the
	   fallback needs it as much as the enhancement does. */
	.flush-split__card--tabbed .flush-split__card-face-inner {
		height: auto;
		overflow-y: visible;
	}

	/* THE MOBILE BASE, and the thing the marked block below falls back TO.

	   At this breakpoint a tabbed card does NOT overlap its panes. The
	   active pane is in normal flow, the inactive ones carry `hidden`, and
	   the card is therefore the height of the pane being read -- no JS
	   height, no measurement, nothing that can be off by a frame. The page
	   scrolls, which is what a phone expects.

	   This exists so that deleting the marked block below leaves a WORKING
	   card rather than a collapsed one. Both halves were verified at 390px
	   before shipping; an escape hatch nobody tested is not one. */
	.flush-split__card--tabbed .flush-split__card-panels {
		overflow: visible;
	}

	.flush-split__card--tabbed .flush-split__card-face {
		position: relative;
		inset: auto;
		transition: none;
	}

	/* =================================================================
	   ROLLBACK-A-STACKED-CARD -- v1.13.7, RE-POINTED v1.13.10.

	   TO REVERT: delete this whole marked block, down to the matching
	   "END ROLLBACK-A-STACKED-CARD". The base rules immediately above are
	   written to be complete without it. `git log -S
	   ROLLBACK-A-STACKED-CARD` finds the whole lineage.

	   WHAT THIS NAME USED TO MEAN. Until v1.13.10 this card was a two-face
	   flip, and at this breakpoint its two faces were stacked in one grid
	   cell so the card grew to the TALLER face instead of scrolling inside
	   itself -- Joe, 2026-09-22: the internal scroll "fights the phone's
	   natural scroll-ability". It cost the front face 95-223px of slack
	   (measured across six projects at 390px, median ~140), because the
	   card was the height of the back face on every project.

	   WHY IT IS RE-POINTED AND NOT RETIRED. The tabs remove the condition
	   it was written for: there is no taller-of-two to stack to, because a
	   pane wrapper that animates to the ACTIVE pane makes the slack zero on
	   every pane. The planning draft therefore proposed retiring it. Joe
	   overruled that on 2026-09-22, and the reason is the better one: how a
	   card feels on a phone is not knowable before the thing is running, so
	   there has to be something to fall back to. The NAME, the marker and
	   the delete-this-block contract are what he is keeping; the rules
	   inside could not be, because `position: relative; inset: auto` on the
	   faces is exactly what the slide needs undone.

	   So the polarity is inverted. The base above is the plain, safe
	   behaviour; THIS block is the enhancement, and it is what ships.

	   WHAT IT DOES. Restores the overlap at this breakpoint so the slide
	   runs on a phone as it does at desktop, and hands the wrapper's height
	   to flush-split.js, which tweens it to the incoming pane's
	   `scrollHeight` over the same 0.55s. The card is still the height of
	   the pane being read -- the tween is how it gets there, not whether.

	   WHAT DELETING IT COSTS: the slide and the height animation on
	   phones. Tabs still switch, the card still sizes to the active pane,
	   nothing overflows or collapses. Desktop is untouched either way --
	   this block is inside `max-width: 768px`, and above it the wrapper
	   takes its height from `flex: 1 1 auto` inside a card the column
	   already sizes.

	   WHAT IT DOES NOT TOUCH: the 100vh cap (gated `min-width: 769px`,
	   never sees this breakpoint) and `measureCardMinHeight()`, which feeds
	   only that cap and the column-sync decisions, both desktop-gated.
	   ================================================================= */
	.flush-split__card--tabbed .flush-split__card-panels {
		overflow: hidden;
		transition: height 0.55s cubic-bezier(0.65, 0, 0.35, 1);
	}

	.flush-split__card--tabbed .flush-split__card-face {
		position: absolute;
		inset: 0;
		transition: transform 0.55s cubic-bezier(0.65, 0, 0.35, 1);
	}

	/* The card stops being `height: 100%` of a row it no longer has, and
	   lets the tab bar plus the wrapper's own height size it. */
	.flush-split__card--tabbed {
		height: auto;
	}
	/* ============ END ROLLBACK-A-STACKED-CARD ======================= */

}

/* ---------------------------------------------------------------------
   TABLET TIER -- the first width breakpoint in this codebase between
   `max-width: 768px` and `min-width: 769px`. Everything else in theme/
   is one of those two or `prefers-reduced-motion`; this is a new
   convention, not an existing hook, so it is documented rather than
   dropped in.

   WHY 1125px AND NOT NEVE'S 992px. `--flush-split-card-col-width` is
   `clamp(360px, 32vw, 560px)`, so from 769px up to ~1125px the card
   column is pinned to its 360px FLOOR and takes a fixed bite out of the
   band. That is exactly the range where `cardMin` is at its highest
   (697.98 at the floor vs 669.19 at 1235px, measured on Public Site
   Strategy during CAP-3 round 2). The typography step and the geometry
   problem therefore share a boundary -- which is the whole argument for
   this number. Snapping to 992px instead would return the headline to
   48px while the card is still pinned, leaving the worst band carrying
   the biggest type.

   WHY 36px AND NOT 24px. Joe's original ask was "about half", but half
   of 48 is 24 and `max-width: 768px` already ships 32px -- 48 -> 24 -> 32
   would make tablet the SMALLEST tier. 36px is also the largest value
   that still captures the entire measured fit benefit: cardMin is
   identical at 36, 32, 28 and 24 (654.61, a 235.37px improvement on
   48px's 889.98, measured at the 360px clamp floor), because below 36px
   the card's BACK face -- 707 characters with no large heading -- becomes
   the tallest element and the headline stops deciding anything. Anything
   under 36px costs readability and buys no fit.

   Reaches Flush Split White too: that template shares this stylesheet
   and overrides no type rule. --------------------------------------- */
@media ( min-width: 769px ) and ( max-width: 1125px ) {
	.flush-split__headline {
		font-size: 36px;
	}
}

/* REDUCED MOTION -- BOTH MOVING THINGS, v1.13.10.

   Two properties animate on this card and a guard naming only one of them
   is worse than none, because it reads as handled. The panes translate,
   and at `max-width: 768px` the panel wrapper's HEIGHT is tweened as well
   -- that second one is new, it is the larger movement on a phone, and it
   is the one a guard written before the tabs would have missed.

   (This rule was retargeted once already, on 2026-09-21, from
   `.flush-split__card` to `.flush-split__card-face`, for the same class of
   reason: the card itself had stopped transitioning anything.)

   flush-split.js carries the same guard independently and swaps panes
   instantly rather than driving a transition it knows is off -- both are
   needed. CSS alone cannot stop the script writing intermediate heights;
   the script alone cannot stop the declarations here from animating. */
@media (prefers-reduced-motion: reduce) {
	.flush-split__card-face,
	.flush-split__card--tabbed .flush-split__card-face,
	.flush-split__card--tabbed .flush-split__card-panels {
		transition: none;
	}
}
