/* ─────────────────────────────────────────────────────────────
   staff.css - page-scoped styles for /team/ + /team/{slug}/
   Loaded inline by staff.php + staff-bio.php with filemtime() cache-bust.
   Tokens: site/templates/css/design-tokens.css (Velvet Cinema).
   ───────────────────────────────────────────────────────────── */

/* ════════════════════════════════════════════════════════════
   TEAM INDEX - /team/
   ════════════════════════════════════════════════════════════ */

/* ─── Section 1: Team Hero ───────────────────────────────────
   DOMINANT MOMENT: oversized italic "<em>Behind</em>" inside the
   team-hero__title, paired with the floating stat band underneath.
*/
.team-hero {
    position: relative;
    isolation: isolate;
    /* Trimmed from 56/50 (106px). min-height already tracks --hero-h correctly,
       so the fold overrun here was never the header - it was content 582px plus
       106px of padding against a 643px fold at 1280x800. Padding is the only
       lever that does not cut copy. */
    padding-block: clamp(1.75rem, 1.25rem + 1.75vw, 3rem) clamp(1.5rem, 1.25rem + 1.5vw, 2.75rem);
    background:
        radial-gradient(ellipse at 80% 0%, color-mix(in srgb, var(--color-accent-glow) 10%, transparent) 0%, transparent 55%),
        linear-gradient(to bottom, var(--color-bg-mid) 0%, var(--color-bg) 100%);
    overflow: hidden;
}

/* Keep the entire hero - through the stat band - above the fold on desktop.
   The sticky header is ~98px; fill the viewport minus that and vertically
   centre, so short laptops (~800px tall) never clip the stats. Mobile keeps
   natural flow (it already fits) per mobile-first. */
@media (min-width: 768px) {
    .team-hero {
        display: flex;
        flex-direction: column;
        justify-content: center;
        /* --hero-h, not 7rem: 112px was derived from the old header and left
           .team-hero 45px past the fold at the 157px one (measured 1280x800). */
        min-height: var(--hero-h);
    }
}

.team-hero__grain {
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    opacity: 0.05;
    background-image: radial-gradient(circle at 1px 1px, var(--color-heading) 1px, transparent 0);
    background-size: 24px 24px;
}

/* Dark theme: a flat navy hero is the #1 depth failure. Strengthen the radial
   teal glow behind the display heading, add a soft rose counter-glow, deepen the
   grain, and give the elevated stat band a faint inset top-light so it lifts off
   the field instead of sitting flat on it. */
[data-theme="dark"] .team-hero {
    background:
        radial-gradient(ellipse at 78% -5%, color-mix(in srgb, var(--color-accent-glow) 22%, transparent) 0%, transparent 58%),
        radial-gradient(ellipse at 12% 108%, rgba(212, 160, 160, 0.10) 0%, transparent 55%),
        linear-gradient(to bottom, var(--color-bg-mid) 0%, var(--color-bg) 100%);
}
[data-theme="dark"] .team-hero__grain { opacity: 0.09; }
[data-theme="dark"] .team-hero__meta {
    box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05) inset, var(--shadow-md);
}

.team-hero__inner {
    position: relative;
    max-width: var(--container-wide);
    text-align: center;
}

.team-hero__breadcrumbs {
    margin-bottom: var(--space-xl);
    font-size: var(--text-xs);
}
.team-hero__breadcrumbs ol {
    display: inline-flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.5rem;
    padding: 0;
    margin: 0;
    list-style: none;
    color: var(--color-muted);
}
/* Separator comes from the global `.breadcrumbs li + li::before` (style.css).
   A local ::after here rendered a second slash: "Home / / Team". */
.team-hero__breadcrumbs a {
    color: var(--color-muted);
    text-decoration: none;
    transition: color var(--transition-fast);
}
.team-hero__breadcrumbs a:hover { color: var(--color-accent); }

/* Vertical rhythm is svh-aware, not a fixed token. This hero fails on viewport
   HEIGHT (100px past the fold at 1280x720) while its spacing was keyed to
   fixed rems, so a short laptop paid the same 80px of stacked margin as a
   1080p screen. The clamps give the full spacing back above ~1080 and reclaim
   ~31px at 720, which is exactly the shortfall copy alone could not close.
   Caps equal the previous fixed values, so tall screens are unchanged. */
.team-hero__eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: clamp(0.75rem, 2svh, var(--space-lg));
    color: var(--color-eyebrow);
}

.team-hero__title {
    /* Velvet Cinema editorial serif hero (matches .svcs-hero__title). */
    font-family: var(--font-serif);
    font-optical-sizing: auto;
    font-size: clamp(2.6rem, 1.6rem + 5vw, 5rem);
    font-weight: 350;
    line-height: 1.02;
    letter-spacing: -0.015em;
    margin: 0 auto clamp(0.75rem, 2svh, var(--space-lg));
    max-width: 20ch;
    color: var(--color-heading);
    text-wrap: balance;
}
.team-hero__title em {
    font-style: italic;
    font-weight: 480;
    color: var(--color-rose-deep);
}

.team-hero__lead {
    font-family: var(--font-body);
    font-size: var(--text-lg);
    line-height: 1.6;
    max-width: 56ch;
    margin: 0 auto clamp(1rem, 2.75svh, var(--space-xl));
    color: var(--color-body);
    text-wrap: pretty;
}

/* Stat band elevated into the page's one "number moment": oversized light-weight
   serif numerals (300 vs the 700-weight body voice elsewhere) with full-height
   editorial hairline rules between items and generous breathing room, so the
   band reads as a designed stat panel, not an afterthought pill. */
.team-hero__meta {
    display: inline-flex;
    align-items: stretch;
    gap: clamp(1.5rem, 3vw, 3.5rem);
    padding: clamp(1.25rem, 2vw, 1.75rem) clamp(1.75rem, 3vw, 2.75rem);
    background: var(--color-bg-elevated);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-md);
}
.team-hero__meta-item {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    align-items: center;
}
.team-hero__meta-num {
    font-family: var(--font-display);
    font-size: var(--text-3xl);
    font-weight: 300;
    letter-spacing: -0.02em;
    color: var(--color-heading);
    line-height: 1;
}
.team-hero__meta-label {
    font-family: var(--font-body);
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--color-muted);
}
.team-hero__meta-divider {
    width: 1px;
    align-self: stretch;
    background: var(--color-border-hover);
}

/* ─── Section 2: Filter chips + Card grid ────────────────────
   DOMINANT MOMENT: oversized numerical card indices (01, 02, ...)
   overlaying each portrait, plus the asymmetric staggered grid.
*/
.team-grid {
    padding-block: clamp(3rem, 2rem + 4vw, 6rem);
    background: var(--color-bg-alt);
}

.team-grid__header {
    margin-bottom: var(--space-2xl);
}

/* Sits above the filter chips and reads as their caption, so it is deliberately
   quieter than body copy - muted, centered, and capped near the chip row's width
   so it does not run the full container and compete with the cards below. */
.team-grid__intro {
    max-width: 62ch;
    margin: 0 auto var(--space-lg);
    text-align: center;
    font-size: var(--text-base);
    line-height: 1.6;
    color: var(--color-muted);
    text-wrap: pretty;
}

.team-grid__filters {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-sm);
    padding: var(--space-sm);
    background: var(--color-bg-elevated);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-full);
    max-width: max-content;
    margin: 0 auto;
}

.team-grid__chip {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    min-height: 44px;
    padding: 0.55rem 1.1rem;
    border: 0;
    border-radius: var(--radius-full);
    background: transparent;
    color: var(--color-body);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: 500;
    cursor: pointer;
    transition: background var(--transition-fast),
                color var(--transition-fast),
                transform var(--transition-fast);
}
.team-grid__chip svg { color: currentColor; }
.team-grid__chip-count {
    font-size: var(--text-2xs);
    color: var(--color-muted);
    font-variant-numeric: tabular-nums;
}
@media (hover: hover) {
    .team-grid__chip:hover {
        background: var(--color-bg);
        color: var(--color-heading);
        transform: translateY(-1px);
    }
}
.team-grid__chip.is-active {
    background: var(--color-heading);
    color: var(--color-bg);
}
/* Derive the count from the chip's OWN resolved text colour instead of a literal
   white. The active chip paints `background: var(--color-heading)` with
   `color: var(--color-bg)`, and --color-heading is #3a2828 in light but #ffffff in
   dark, so the hardcoded rgba(255,255,255,.6) that reads correctly on the light
   theme's dark pill was white-on-white in dark mode and the count vanished. Mixing
   currentColor keeps the count one step quieter than its label in whichever theme
   is active: ~5.0:1 on the dark theme's white pill, ~6.7:1 on the light theme's
   dark pill, both above the 4.5:1 floor this size needs. */
.team-grid__chip.is-active .team-grid__chip-count {
    color: color-mix(in srgb, currentColor 70%, transparent);
}
/* PRECAUTIONARY, and measured inert - do not cite it as a fix. Forced-colors modes
   replace author colours with system ones, and an alpha-reduced foreground is the
   kind of thing that can degrade there, which matters because this count is the
   smallest text in the chip and already went invisible once. But measured under
   Chromium's forced-colors emulation, the count resolves to a full-opacity system
   colour with AND without this block - identical readings in both themes. Kept as
   cheap insurance for real Windows High Contrast, which could not be tested here,
   not because a live defect was observed. */
@media (forced-colors: active) {
    .team-grid__chip-count,
    .team-grid__chip.is-active .team-grid__chip-count {
        color: currentColor;
        forced-color-adjust: auto;
    }
}
.team-grid__chip:focus-visible {
    outline: 2px solid var(--color-focus-ring);
    outline-offset: 3px;
}

/* Cards grid: 1 col mobile -> 2 -> 3 -> 4. Cards top-align so each sizes to its
   own content; excerpts are clamped to a uniform line count so a row of cards
   shares one baseline. That uniform height is what lets the editorial
   raised/lowered stagger below read as a DESIGNED offset rather than ragged gaps. */
.team-grid__cards {
    list-style: none;
    padding: 0;
    margin: 0;
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-xl) var(--space-lg);
    align-items: start;
}
@media (min-width: 600px)  { .team-grid__cards { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 900px)  { .team-grid__cards { grid-template-columns: repeat(3, 1fr); gap: var(--space-2xl) var(--space-xl); } }
@media (min-width: 1200px) { .team-grid__cards { grid-template-columns: repeat(4, 1fr); } }

/* Editorial raised/lowered stagger on the standard cards - the magazine-style
   offset rhythm. Now that excerpts are clamped to a uniform height the alternate
   translateY reads as an intentional offset, not the byproduct-of-ragged-content
   gaps it used to. Desktop 3-4 col only; the full-width featured card is excluded.
   Driven by the .team-card--stagger class (server-rendered, recomputed by the
   filter JS over VISIBLE cards) - NOT :nth-child, which would count [hidden]
   cards and break the zigzag whenever a filter is active. */
@media (prefers-reduced-motion: no-preference) {
    @media (min-width: 900px) {
        .team-grid__cards .team-card--stagger:not(.team-card--feature) {
            transform: translateY(var(--space-2xl));
        }
    }
}

/* Featured lead card: the page's dominant moment. A full-width editorial band
   (own row via grid-column 1/-1, so it never shares a row height with the
   standard tiles) that sets the portrait beside an oversized name + story - an
   asymmetric split that breaks the uniform directory grid. Static modifier
   class (never nth-child) so the filter JS can hide/show it without shifting
   which card is featured. */
@media (min-width: 900px) {
    .team-grid__cards .team-card--feature {
        grid-column: 1 / -1;
        flex-direction: row;
        align-items: center;
        gap: clamp(2rem, 4vw, 4rem);
        padding: clamp(1.5rem, 2.4vw, 2.75rem);
        background: var(--color-bg-elevated);
        border: 1px solid var(--color-border);
        border-radius: var(--radius-xl);
        box-shadow: var(--shadow-md);
    }
    .team-card--feature .team-card__media {
        flex: 0 0 clamp(220px, 26%, 360px);
        aspect-ratio: 3 / 4;
        max-height: 460px;
    }
    .team-card--feature .team-card__body {
        flex: 1;
        gap: var(--space-md);
        max-width: 62ch;
    }
    .team-card--feature .team-card__name { font-size: var(--text-4xl); line-height: 1; }
    .team-card--feature .team-card__excerpt {
        font-size: var(--text-lg);
        -webkit-line-clamp: 4;
    }
}

.team-card {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
}
.team-card[hidden] { display: none; }

.team-card__media {
    position: relative;
    display: block;
    aspect-ratio: 3 / 4;
    overflow: hidden;
    border-radius: var(--radius-card);
    background: var(--color-bg);
    box-shadow: var(--shadow-card);
    transition: box-shadow var(--transition-base),
                transform var(--transition-base);
    text-decoration: none;
}
/* <picture> from StaffPortrait::pictureTag() must fill the 3:4 frame; it is
   inline by default, so stretch it like the img (matches bio-hero portrait). */
.team-card__media picture {
    display: block;
    width: 100%;
    height: 100%;
}
.team-card__media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center 25%;
    display: block;
}
@media (prefers-reduced-motion: no-preference) {
    .team-card__media img {
        transition: transform 700ms cubic-bezier(0.2, 0.7, 0.2, 1);
    }
}
@media (hover: hover) {
    .team-card__media:hover {
        box-shadow: var(--shadow-lg);
        transform: translateY(-4px);
    }
    .team-card__media:hover img { transform: scale(1.06); }
}
.team-card__media:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 3px;
}

.team-card__media-overlay {
    position: absolute;
    inset: 0;
    background: linear-gradient(to top, rgba(7, 53, 66, 0.55) 0%, transparent 45%);
    opacity: 0;
    transition: opacity var(--transition-base);
    pointer-events: none;
}
@media (hover: hover) {
    .team-card__media:hover .team-card__media-overlay { opacity: 1; }
}

.team-card__placeholder {
    display: grid;
    place-items: center;
    width: 100%;
    height: 100%;
    color: var(--color-muted);
    background: var(--color-bg-elevated);
}

.team-card__body {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.team-card__role {
    font-family: var(--font-body);
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--color-eyebrow);
    margin: 0;
}

.team-card__name {
    font-family: var(--font-display);
    font-size: var(--text-xl);
    font-weight: 700;
    line-height: 1.1;
    margin: 0;
    color: var(--color-heading);
    text-wrap: balance;
}
.team-card__name a {
    --hit-expander: 1;
    position: relative;
    color: inherit;
    text-decoration: none;
    background-image: linear-gradient(var(--color-accent), var(--color-accent));
    background-repeat: no-repeat;
    background-size: 0% 2px;
    background-position: 0 100%;
    transition: background-size var(--transition-base);
    padding-bottom: 2px;
}
/* Vertical hit-expander: name link tap height to 44, visible text unchanged. */
.team-card__name a::after {
    content: "";
    position: absolute;
    top: 50%; left: 0;
    transform: translateY(-50%);
    width: 100%; height: 100%;
    min-height: 44px;
}
@media (hover: hover) {
    .team-card__name a:hover { background-size: 100% 2px; }
}

.team-card__excerpt {
    font-size: var(--text-base);
    line-height: 1.5;
    color: var(--color-body);
    margin: 0.25rem 0 0;
    text-wrap: pretty;
    /* Clamp so cards across a row share a consistent body height and the grid
       reads even (paint-time clamp - verify with screenshots).

       FOUR lines, not three. Measured 2026-08-22 across the live roster: the
       standard card's excerpt box is 280px at 1280 and 261px at 900, which
       carries roughly 28.5 and 26.8 characters per line. Three lines therefore
       held only ~80 characters, and every one of the 24 excerpts overflowed it
       - the shortest on the roster (105 chars) already needed four lines. The
       result was a grid where every card was visibly cut mid-sentence.

       Four lines buys ~114 chars on desktop and ~107 on tablet; mobile's wider
       358px box already holds ~123 at three. So 105 characters is the single
       budget that fits every breakpoint, and the excerpts are written to it
       (migration 2026-08-22-004). The clamp stays as a safety net rather than
       the thing readers actually see. Re-run docs/handoffs/2026-08-22-measure-excerpts.js if the
       column width or type scale ever changes. */
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 4;
    overflow: hidden;
}

.team-card__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-md);
    margin-top: var(--space-sm);
}

.team-card__profile-link {
    --hit-expander: 1;
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    color: var(--color-heading);
    text-decoration: none;
    font-size: var(--text-sm);
    font-weight: 600;
    border-bottom: 1px solid var(--color-border-hover);
    padding-bottom: 2px;
    transition: color var(--transition-fast),
                border-color var(--transition-fast);
}
.team-card__profile-link::after {
    content: "";
    position: absolute;
    top: 50%; left: 0;
    transform: translateY(-50%);
    width: 100%; height: 100%;
    min-height: 44px;
}
.team-card__profile-arrow {
    display: inline-flex;
    transition: transform var(--transition-fast);
}
@media (hover: hover) {
    .team-card__profile-link:hover {
        color: var(--color-accent);
        border-color: var(--color-accent);
    }
    .team-card__profile-link:hover .team-card__profile-arrow {
        transform: translateX(3px);
    }
}
.team-card__profile-link:focus-visible {
    outline: 2px solid var(--color-focus-ring);
    outline-offset: 3px;
    border-radius: 2px;
}

.team-card__book-link {
    --hit-expander: 1;
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    /* Demoted from teal to heading color: repeated x24 across the grid the teal
       Book link read as a rash. Teal is now an interactive-only accent, applied
       on hover/focus, so the persistent accent budget stays disciplined. */
    color: var(--color-heading);
    text-decoration: none;
    font-size: var(--text-sm);
    font-weight: 600;
    transition: transform var(--transition-fast),
                color var(--transition-fast);
}
.team-card__book-link::after {
    content: "";
    position: absolute;
    top: 50%; left: 0;
    transform: translateY(-50%);
    width: 100%; height: 100%;
    min-height: 44px;
}
@media (hover: hover) {
    .team-card__book-link:hover {
        color: var(--color-accent-hover);
        transform: translateY(-1px);
    }
}
.team-card__book-link:focus-visible {
    outline: 2px solid var(--color-focus-ring);
    outline-offset: 3px;
    border-radius: 2px;
}

.team-card--reception .team-card__role { color: var(--color-rose); }

.team-grid__empty {
    text-align: center;
    margin-top: var(--space-2xl);
    color: var(--color-muted);
    font-size: var(--text-base);
}
.team-grid__reset {
    background: none;
    border: 0;
    color: var(--color-accent);
    text-decoration: underline;
    cursor: pointer;
    font: inherit;
    padding: 0;
}

/* ─── Section 3: Closing CTA ─────────────────────────────────
   DOMINANT MOMENT: italic "match" inside the heading on the
   accent-saturated band, the only such band on the page.
*/
.team-cta {
    padding-block: clamp(4rem, 3rem + 5vw, 7rem);
    text-align: center;
}
.team-cta__eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    color: var(--color-heading);
    margin-bottom: var(--space-lg);
}
.team-cta__heading {
    font-family: var(--font-display);
    font-size: var(--text-3xl);
    font-weight: 700;
    line-height: 1.05;
    letter-spacing: -0.02em;
    max-width: 22ch;
    margin: 0 auto var(--space-lg);
    color: var(--color-heading);
    text-wrap: balance;
}
.team-cta__heading em {
    font-style: italic;
    font-weight: 400;
}
.team-cta__lead {
    font-size: var(--text-lg);
    line-height: 1.55;
    max-width: 50ch;
    margin: 0 auto var(--space-xl);
    color: var(--color-body);
}
.team-cta__actions {
    display: inline-flex;
    flex-wrap: wrap;
    gap: var(--space-md);
    justify-content: center;
}

/* ════════════════════════════════════════════════════════════
   STYLIST BIO - /team/{slug}/
   ════════════════════════════════════════════════════════════ */

/* ─── Section 1: Bio Hero ───────────────────────────────────
   DOMINANT MOMENT: stacked first/last name with the last name
   set in italic display - reads as a signature line on desktop,
   stacks vertically on mobile.
*/
.bio-hero {
    position: relative;
    /* Mobile baseline: let the hero grow with the stacked content (text + CTAs
       + portrait won't fit in 812px reliably). Desktop locks to exactly one
       viewport via the @media block below - per memory feedback_hero_fullscreen. */
    min-height: 100svh;
    padding-block: clamp(1rem, 0.5rem + 1.5vw, 2.5rem);
    box-sizing: border-box;
    display: flex;
    align-items: center;
    background:
        radial-gradient(ellipse at 10% 0%, rgba(212, 160, 160, 0.10) 0%, transparent 50%),
        radial-gradient(ellipse at 95% 100%, color-mix(in srgb, var(--color-accent-glow) 12%, transparent) 0%, transparent 55%),
        linear-gradient(to bottom, var(--color-bg-mid) 0%, var(--color-bg) 100%);
    /* `overflow: hidden` here used to amputate the booking CTAs on every laptop
       under ~850px of viewport height. This is a CENTERED flex container, so
       content that outgrows the box overflows equally at both ends - and a
       centered overflow reports scrollHeight === clientHeight, which is why it
       read as "no overflow" to a naive probe while the buttons were being
       clipped away with no way to scroll to them. Only the horizontal axis
       needs containing here; `clip` (not `hidden`) is the one value that
       constrains one axis while leaving the other genuinely visible. */
    overflow-x: clip;
}
.bio-hero__bg { display: none; }

.bio-hero__inner {
    width: 100%;
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-2xl);
    align-items: center;
    max-width: var(--container-wide);
}
@media (min-width: 768px) {
    /* Lock the hero to exactly the VISIBLE area below the sticky header. The
       site-header is `position: sticky` and ~84px tall on desktop, so a plain
       100svh hero spills 84px past the fold and forces a scroll-to-see. We
       subtract the header height so the hero settles in the visible viewport.
       Both min-height and height are reset together so neither overrides. */
    .bio-hero {
        /* Was a hardcoded 84px, which was the header height when this was
           written. The header is now 98px at 900-1279 and 157px at 1280+, so
           the hardcode made the hero spill ~73px past the fold on desktop -
           exactly the scroll-to-see failure the comment above says this rule
           exists to prevent. --header-h tracks both tiers automatically. */
        --hero-header-offset: var(--header-h);
        /* min-height ONLY. The `height` that used to sit alongside it made this
           a hard ceiling rather than a floor, so a bio whose content did not
           fit - which is most of them at 1280+, where the header eats 157px -
           had its hero content overflow a box it was forbidden to grow past.
           As a floor this still fills the fold on short bios (the reason the
           lock was written) without capping the tall ones. Fitting is now the
           job of the content itself - see "short-viewport condense" at the end
           of this file, which scales the hero's type and rhythm by svh. */
        min-height: calc(100svh - var(--hero-header-offset));
    }
    /* minmax(0, …) on BOTH tracks. A bare `0.9fr` means `minmax(auto, 0.9fr)`, so the track's
       floor is its item's MIN-CONTENT width - and the portrait's min-content width is derived
       from its own max-height below (aspect-ratio 3/4 against calc(100svh - header)). On a
       tall tablet that floor is enormous, it wins the whole row, and the text track gets the
       remainder. Measured on 25 bio pages before this fix, resolved tracks text/photo:

           768x1024   16 / 665      834x1194    0 / 745      960x1024  ~29 / 779
           800x1280    0 / 712      860x1280    0 / 770     1024x1024  525 / 401
           820x1180    0 / 731      880x1024  126 / 663

       Zero. On an iPad the bio copy had no column at all - the lede wrapped to one word per
       line or vanished. It only looked sane at 1024 because the viewport finally outgrew the
       portrait's intrinsic floor.

       This is invisible to a column-count audit: the grid is 2-col at 768 AND at 1024, so the
       fingerprints match. It was found by comparing RESOLVED TRACK WIDTHS
       (docs/handoffs/band-sweep2.js, `starved-track`) after a first sweep missed it entirely.

       min-width:0 on the portrait is the other half: without it the flex item re-imposes its
       own min-content floor and undoes the minmax. */
    .bio-hero__inner {
        grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
        gap: clamp(2.5rem, 4vw, 5rem);
    }
    /* Cap portrait height so the hero never spills past the viewport.
       aspect-ratio: 3/4 stays intact - width auto-derives from height.
       flex-direction: column keeps the figcaption stacked below the photo
       rather than pushing it into the row beside the frame. */
    .bio-hero__portrait {
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        /* The other half of the minmax fix above. A grid item's automatic minimum size is its
           min-content width, so without this the portrait re-imposes the very floor the
           minmax(0, …) removed and the text track starves again. */
        min-width: 0;
    }
    .bio-hero__portrait-frame {
        /* Match the hero's height minus its padding so the portrait can't
           push the hero taller than 100svh - header. */
        max-height: calc(100svh - var(--hero-header-offset, var(--header-h)) - clamp(2rem, 1rem + 3vw, 5rem));
        width: auto;
    }
}

.bio-hero__content { min-width: 0; }

.bio-hero__breadcrumbs {
    margin-bottom: var(--space-lg);
    font-size: var(--text-xs);
}
.bio-hero__breadcrumbs ol {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    padding: 0;
    margin: 0;
    list-style: none;
    color: var(--color-muted);
}
/* Separator comes from the global `.breadcrumbs li + li::before` (style.css).
   A local ::after here would render a second slash (see .team-hero__breadcrumbs). */
.bio-hero__breadcrumbs a { color: var(--color-muted); text-decoration: none; }
.bio-hero__breadcrumbs a:hover { color: var(--color-accent); }

.bio-hero__eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: var(--space-md);
    color: var(--color-eyebrow);
}

.bio-hero__name {
    /* Velvet Cinema editorial serif hero (matches .svcs-hero__title): stacked
       first/last name, the last name set in rose-deep serif-italic as the
       signature accent. */
    font-family: var(--font-serif);
    font-optical-sizing: auto;
    font-size: clamp(2.75rem, 2rem + 5vw, 5.75rem);
    font-weight: 350;
    line-height: 0.98;
    letter-spacing: -0.015em;
    margin: 0 0 var(--space-lg);
    color: var(--color-heading);
    display: flex;
    flex-direction: column;
}
.bio-hero__name-first { display: block; }
.bio-hero__name-last {
    display: block;
    font-style: italic;
    font-weight: 480;
    color: var(--color-rose-deep);
    padding-left: clamp(0.5rem, 2vw, 2rem);
}

.bio-hero__specialties {
    list-style: none;
    padding: 0;
    margin: 0 0 var(--space-xl);
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}
.bio-hero__specialties li {
    /* Chips carry full client-state phrases, not labels: --text-sm clamps to
       ~13px at 375w, below the readable floor. Hold them at 14px minimum. */
    font-size: max(0.875rem, var(--text-sm));
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    padding: 0.4rem 0.85rem;
    border: 1px solid var(--color-border-hover);
    border-radius: var(--radius-full);
    color: var(--color-body);
    background: var(--color-bg-elevated);
}

.bio-hero__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-md);
}

.bio-hero__portrait {
    margin: 0;
    position: relative;
}
.bio-hero__portrait-frame {
    position: relative;
    aspect-ratio: 3 / 4;
    overflow: hidden;
    border-radius: var(--radius-xl);
    box-shadow: var(--shadow-xl);
    background: var(--color-bg-alt);
    isolation: isolate;
}
.bio-hero__portrait-frame::after {
    content: '';
    position: absolute;
    inset: 0;
    box-shadow: inset 0 0 0 8px rgba(245, 235, 229, 0.4);
    pointer-events: none;
    border-radius: var(--radius-xl);
}
.bio-hero__portrait-frame img,
.bio-hero__portrait-frame picture {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center 20%;
    display: block;
}
.bio-hero__portrait-frame picture img {
    width: 100%;
    height: 100%;
}

.bio-hero__portrait-caption {
    text-align: center;
    margin-top: var(--space-md);
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--color-muted);
}

/* ─── Section 2: Story + Specialties ─────────────────────────
   DOMINANT MOMENT: ornamental quote icon paired with the
   editorial body column at narrow measure for readability.
*/
.bio-story {
    /* Tighter top padding closes the hollow seam between the hero CTAs and the
       "Meet {name}" heading; the bottom keeps its full breathing room. */
    padding-block: clamp(1.5rem, 1rem + 2vw, 3rem) clamp(3rem, 2rem + 4vw, 6rem);
}
.bio-story__header { text-align: center; margin-bottom: var(--space-xl); }
.bio-story__header .eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    color: var(--color-eyebrow);
    margin-bottom: var(--space-md);
}
.bio-story__heading {
    font-family: var(--font-display);
    font-size: var(--text-3xl);
    font-weight: 700;
    line-height: 1.05;
    letter-spacing: -0.02em;
    margin: 0;
    color: var(--color-heading);
    text-wrap: balance;
}
.bio-story__heading em {
    font-style: italic;
    font-weight: 400;
    color: var(--color-accent);
}

.bio-story__body {
    max-width: 62ch;
    margin: 0 auto;
    font-size: var(--text-base);
    line-height: 1.75;
    color: var(--color-body);
}
.bio-story__body p { margin: 0 0 1.25em; }
/* Inline service links in the bio prose inherit the global teal prose-link
   treatment, which speckles a personal two-paragraph story (8+ links). Drop
   them to the prose color and bring the teal back only on hover/focus, so the
   calm prose keeps the underline as its link affordance. Scoped to the bio
   body so global CMS content is untouched.
   Specificity note: style.css paints prose links via .cms-content a:not(.btn)
   (0,2,1) - color teal, text-decoration none, underline redrawn as a 1px
   currentColor background-image gradient. A bare .bio-story__body a (0,1,1)
   mute silently loses to it (the original 2026-06-23 mute never rendered), so
   the mute doubles up the classes the story div already carries to win at
   (0,3,1). Muting color also mutes the gradient underline for free - it rides
   currentColor - and on hover/focus it re-teals whole the same way. */
.bio-story__body.cms-content a:not(.btn) {
    color: var(--color-body);
    /* Covers any plain-underline fallback path (UA underline is teal via the
       global a:not([class]) text-decoration-color). */
    text-decoration-color: currentColor;
}
.bio-story__body.cms-content a:not(.btn):focus-visible { color: var(--color-link); }
@media (hover: hover) {
    .bio-story__body.cms-content a:not(.btn):hover { color: var(--color-link); }
}
.bio-story__body p:first-of-type::first-letter {
    font-family: var(--font-display);
    font-size: 3.25em;
    font-weight: 800;
    float: left;
    line-height: 0.85;
    padding: 0.05em 0.1em 0 0;
    color: var(--color-accent);
}

/* ─── Pull-quote: break-out wide, centered editorial moment ───────────
   DOMINANT MOMENT of the Story section. Triggered by any <blockquote>
   in the CMS body. Bleeds wider than the 62ch reading column so it
   reads as a real interruption, not a tinted paragraph. Decorative
   opening curly quote anchors the top-left as a soft brand mark.
*/
.bio-story__body blockquote {
    /* Break out of the 62ch parent. Negative margin pulls the block
       wider than its container; the inner padding keeps the reading
       measure comfortable. The clamp ensures we never bleed past the
       section container at any viewport. */
    position: relative;
    margin: clamp(2.5rem, 2rem + 2vw, 4rem)
            clamp(0rem, -8vw, -2rem)
            clamp(2.5rem, 2rem + 2vw, 4rem);
    padding: clamp(2rem, 1.5rem + 2vw, 3.5rem)
             clamp(1.5rem, 1rem + 2vw, 4rem)
             clamp(1.75rem, 1.25rem + 1.5vw, 2.75rem);
    background:
        linear-gradient(
            145deg,
            rgba(212, 160, 160, 0.08) 0%,
            rgba(212, 160, 160, 0.02) 100%
        ),
        var(--color-bg-elevated);
    border-radius: var(--radius-lg);
    border-left: 3px solid var(--color-rose, var(--color-accent));
    box-shadow:
        0 1px 0 rgba(255, 255, 255, 0.4) inset,
        0 30px 60px -36px rgba(58, 40, 40, 0.22);
    quotes: '\201C' '\201D';

    /* Editorial typography - the actual pull-quote treatment. */
    font-family: var(--font-display);
    font-size: clamp(1.4rem, 1.1rem + 1vw, 2rem);
    font-style: italic;
    font-weight: 500;
    line-height: 1.35;
    letter-spacing: -0.01em;
    color: var(--color-heading);
    text-wrap: balance;
}
@media (min-width: 900px) {
    /* Wider break-out on desktop for the magazine-feature feel. */
    .bio-story__body blockquote {
        margin-inline: clamp(-6rem, -8vw, -10rem);
    }
}
[data-theme="dark"] .bio-story__body blockquote {
    background:
        linear-gradient(
            145deg,
            rgba(212, 160, 160, 0.06) 0%,
            rgba(212, 160, 160, 0.02) 100%
        ),
        var(--color-bg-elevated);
    box-shadow:
        0 1px 0 rgba(255, 255, 255, 0.04) inset,
        0 30px 60px -36px rgba(0, 0, 0, 0.55);
}

/* Decorative opening curly quote at top-left. Pure ornament - the
   block's actual text uses straight punctuation. */
.bio-story__body blockquote::before {
    content: '\201C';
    position: absolute;
    top: clamp(-0.4rem, -1rem + 1vw, -0.6rem);
    left: clamp(0.6rem, 0rem + 1.5vw, 1.5rem);
    font-family: var(--font-display);
    font-size: clamp(5rem, 4rem + 3vw, 8rem);
    line-height: 1;
    color: var(--color-rose, var(--color-accent));
    opacity: 0.18;
    pointer-events: none;
    user-select: none;
    font-style: normal;
    font-weight: 700;
}

/* Inner paragraph spacing (blockquote may contain a <p> wrapper). */
.bio-story__body blockquote p {
    margin: 0 0 0.6em;
}
.bio-story__body blockquote p:last-child { margin-bottom: 0; }
/* First paragraph of body should NOT drop-cap if it's inside the
   blockquote - the editorial quote treatment owns the visual. */
.bio-story__body blockquote p:first-of-type::first-letter {
    all: unset;
}

/* Citation tag: render as small-caps mono attribution. */
.bio-story__body blockquote cite {
    display: block;
    margin-top: clamp(0.75rem, 0.5rem + 0.5vw, 1.25rem);
    font-family: var(--font-mono, var(--font-body));
    font-size: var(--text-2xs);
    font-style: normal;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--color-muted);
}
.bio-story__body blockquote cite::before {
    /* Hyphen + nbsp prefix on the citation line. CLAUDE.md rule 11
       forbids em/en dashes in visible text - even decorative pseudo
       content. Hyphen still reads as attribution. */
    content: '\002D \00a0';
    color: var(--color-rose, var(--color-accent));
    opacity: 0.7;
}

/* Tier-2 fade-up reveal as the quote enters viewport. Pure CSS via
   the [data-pq-revealed] class added by tiny inline JS. Gated by
   reduced-motion. */
@media (prefers-reduced-motion: no-preference) {
    .bio-story__body blockquote {
        opacity: 0;
        transform: translateY(18px);
        transition: opacity 600ms ease, transform 600ms cubic-bezier(0.16, 1, 0.3, 1);
    }
    .bio-story__body blockquote.is-revealed {
        opacity: 1;
        transform: none;
    }
}

.bio-story__specialties {
    max-width: 62ch;
    margin: var(--space-xl) auto 0;
    padding-top: var(--space-lg);
    border-top: 1px solid var(--color-border);
}
.bio-story__specialties-label {
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--color-eyebrow);
    margin: 0 0 var(--space-md);
}
.bio-story__chips {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}
.bio-story__chips li {
    /* Same 14px floor as .bio-hero__specialties li: these are content phrases. */
    font-size: max(0.875rem, var(--text-sm));
    padding: 0.35rem 0.85rem;
    border: 1px solid var(--color-border-hover);
    border-radius: var(--radius-full);
    color: var(--color-body);
    background: var(--color-bg);
}

/* ─── Section 3: Certifications ──────────────────────────────
   DOMINANT MOMENT: tactile badge cards in a generous grid, with
   duotone medal icon as the unifying mark across all credentials.
*/
.bio-certs {
    padding-block: clamp(3rem, 2rem + 4vw, 6rem);
    /* --color-bg-alt, not --color-bg: in light theme root --color-bg matches
       .section--cream, so Credentials on --color-bg continued the Story field
       as one undifferentiated slab - the adjacency the removed medallion
       divider was stamped over. The deeper field IS the seam now, and it is
       the same move dark theme already made below. Rose radial keeps the band
       off flat, matched to the dark rule's 0.07. */
    background:
        radial-gradient(ellipse 60% 44% at 50% 100%, rgba(212, 160, 160, 0.07) 0%, transparent 60%),
        var(--color-bg-alt);
}

/* Dark theme: the tall Story + Credentials run cannot be one flat navy slab.
   Anchor a soft radial teal glow behind the "Meet {name}" heading, and give the
   credentials band a rose glow on --color-bg-alt so it separates tonally from
   the story above it rather than continuing one uninterrupted field. */
[data-theme="dark"] .bio-story {
    background:
        radial-gradient(ellipse 68% 42% at 50% 2%, color-mix(in srgb, var(--color-accent-glow) 13%, transparent) 0%, transparent 62%),
        var(--color-bg);
}
[data-theme="dark"] .bio-certs {
    background:
        radial-gradient(ellipse 60% 44% at 50% 100%, rgba(212, 160, 160, 0.07) 0%, transparent 60%),
        var(--color-bg-alt);
}
.bio-certs__header { text-align: center; margin-bottom: var(--space-2xl); }
.bio-certs__header .eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    color: var(--color-eyebrow);
    margin-bottom: var(--space-md);
}
.bio-certs__heading {
    font-family: var(--font-display);
    font-size: var(--text-2xl);
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: -0.02em;
    margin: 0;
    color: var(--color-heading);
    text-wrap: balance;
}
.bio-certs__list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
    gap: var(--space-md);
    max-width: 1000px;
    margin-inline: auto;
}
/* Small credential groups (<=4 badges) center as a flex row instead of the
   auto-fit grid, so 4 badges form a balanced 2x2 and a lone last badge never
   strands beside a wide void (the 3+1 orphan the auto-fit grid produced on
   low-credential bios like Nada's). */
.bio-certs__list--few {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    max-width: 760px;
}
.bio-certs__list--few .bio-certs__badge {
    flex: 1 1 300px;
    max-width: 360px;
}
.bio-certs__badge {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-md) var(--space-lg);
    background: var(--color-bg-elevated);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    transition: transform var(--transition-base),
                box-shadow var(--transition-base),
                border-color var(--transition-base);
}
.bio-certs__badge-icon {
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    border-radius: var(--radius-md);
    background: var(--color-bg);
    color: var(--color-accent);
    flex-shrink: 0;
}
.bio-certs__badge a,
.bio-certs__badge span {
    color: var(--color-heading);
    font-weight: 500;
    font-size: var(--text-sm);
    line-height: 1.35;
    text-decoration: none;
}
.bio-certs__badge a {
    --hit-expander: 1;
    border-bottom: 1px solid var(--color-border-hover);
    padding-bottom: 1px;
}
/* Stretched-link hit area: the whole cert row is clickable (well past the 44px
   floor) while the visible underlined label is unchanged. */
.bio-certs__badge a::after {
    content: "";
    position: absolute;
    inset: 0;
}
@media (hover: hover) {
    .bio-certs__badge:hover {
        transform: translateY(-2px);
        box-shadow: var(--shadow-md);
        border-color: var(--color-accent);
    }
    .bio-certs__badge:hover a {
        color: var(--color-accent);
        border-color: var(--color-accent);
    }
}

/* ─── Section 4: Recent Work portfolio
   Handled by .bio-portfolio in photo-library.css. We add only a soft
   transition between it and the next section here. */
.bio-portfolio + .bio-specials,
.bio-portfolio + .bio-book {
    margin-top: 0;
}

/* DOMINANT MOMENT: the wall of work itself - a full-width block of the
   stylist's own 3:4 frames, read newest-first left to right.

   The bio grid shares .gallery__grid with /gallery/, and that class is CSS
   multi-column (photo-library.css: column-count 2/3/4/5 up the breakpoints).
   Multi-column is the wrong model here on two counts, both visible on
   Nikole's bio at 1920px (17 cards, measured 2026-08-08):

     1. It fills DOWN each column before moving right, so the four newest
        sessions stacked vertically in column 1. Reading order was wrong.
     2. The balancer distributes by height, so 17 cards over 5 columns packed
        4/4/4/4/1 - a full-height right-hand column holding ONE photo, with
        dead space below it (Ben: "the right column only has one photo in
        it"). Any card count that is not a clean multiple of the column count
        starves the last column the same way; 1280px packed 5/5/5/2.

   A real grid fixes both: row-major order, and a partial LAST ROW instead of
   a starved last column. /gallery/ is unaffected - it overrides this with the
   JS-packed .gallery__grid--justified layout.

   Column ladder deliberately mirrors photo-library.css's multi-column ladder
   so density is unchanged; only the flow model moves. gap replaces both the
   inherited column-gap and the card's masonry margin-bottom.

   align-items: start keeps a non-3:4 frame (none today - all 17 measure
   0.750) from stretching to its row's height. Never crop to force a row. */
.bio-portfolio .gallery__grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: clamp(0.75rem, 1.5vw, 1.5rem);
    align-items: start;
}
.bio-portfolio .gallery__grid .photo-card--gallery {
    margin-bottom: 0;
}
@media (min-width: 700px) {
    .bio-portfolio .gallery__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 1100px) {
    .bio-portfolio .gallery__grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (min-width: 1400px) {
    .bio-portfolio .gallery__grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}

/* ─── Section 6: Reviews ────────────────────────────────────
   Reviews now render through the shared _partials/_reviews-filtered.php
   component (the same one the homepage and pillar/landing pages use),
   scoped to the stylist via $reviewStaffPages and shown with the badge-less
   --solo aggregate variant. All of its styling lives in style.css under
   .landing-reviews / .reviews__*, so the old bespoke .bio-reviews /
   .bio-review CSS that lived here was removed. See style.css
   ".landing-reviews__aggregate--solo" for the bio-specific header tweak. */

/* ─── Section 7: Specials ───────────────────────────────────
   DOMINANT MOMENT: asymmetric image+body split cards with
   the accent expiry chip floating over the image edge.
*/
.bio-specials {
    padding-block: clamp(3rem, 2rem + 4vw, 6rem);
    background: var(--color-bg);
}
.bio-specials__header { text-align: center; margin-bottom: var(--space-2xl); }
.bio-specials__header .eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    color: var(--color-eyebrow);
    margin-bottom: var(--space-md);
}
.bio-specials__heading {
    font-family: var(--font-display);
    font-size: var(--text-2xl);
    font-weight: 700;
    line-height: 1.1;
    margin: 0;
    color: var(--color-heading);
    text-wrap: balance;
}
.bio-specials__list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-lg);
}
@media (min-width: 800px) { .bio-specials__list { grid-template-columns: repeat(2, 1fr); } }

.bio-special {
    display: grid;
    grid-template-columns: 1fr;
    background: var(--color-bg-elevated);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    overflow: hidden;
}
@media (min-width: 600px) { .bio-special { grid-template-columns: 0.85fr 1.15fr; } }

.bio-special__media {
    position: relative;
    aspect-ratio: 3 / 4;
    overflow: hidden;
}
.bio-special__media img { width: 100%; height: 100%; object-fit: cover; display: block; }

.bio-special__body {
    padding: var(--space-lg);
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
}
.bio-special__expiry {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    align-self: flex-start;
    padding: 0.35rem 0.75rem;
    background: var(--color-accent);
    color: var(--color-btn-primary-text);
    border-radius: var(--radius-full);
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-weight: 600;
    margin: 0;
}
.bio-special__title {
    font-family: var(--font-display);
    font-size: var(--text-lg);
    font-weight: 700;
    line-height: 1.2;
    margin: 0;
    color: var(--color-heading);
}
.bio-special__title a { color: inherit; text-decoration: none; }
.bio-special__title a:hover { color: var(--color-accent); }
.bio-special__excerpt {
    margin: 0;
    font-size: var(--text-sm);
    line-height: 1.5;
    color: var(--color-body);
}
.bio-special__link {
    margin-top: auto;
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    color: var(--color-accent);
    font-weight: 600;
    text-decoration: none;
    font-size: var(--text-sm);
}
@media (hover: hover) {
    .bio-special__link:hover { gap: 0.6rem; }
}

/* ─── Section 8: Book CTA ───────────────────────────────────
   DOMINANT MOMENT: italic personalized "<em>{firstName}</em>"
   inside the heading, paired with the credibility recap chip
   floating above the primary book button on desktop.
   Asymmetric 58/42 split on >= 900px so the section reads as a
   closing peak, not a centered template band.
*/
.bio-book {
    position: relative;
    isolation: isolate;
    padding-block: clamp(4rem, 3rem + 5vw, 7rem);
}
/* Soft rose radial atmosphere anchored top-right - matches the
   alternating-corner pattern across the other bio sections so
   the closing band reads cohesive, not pasted-on. */
.bio-book::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    opacity: 0.7;
    background: radial-gradient(
        ellipse at 90% 8%,
        rgba(212, 160, 160, 0.12) 0%,
        transparent 55%
    );
}
[data-theme="dark"] .bio-book::before { opacity: 0.5; }

.bio-book__inner {
    display: grid;
    grid-template-columns: 1fr;
    gap: clamp(2rem, 1.5rem + 2vw, 3rem);
    text-align: center;
    align-items: center;
}
/* Batch 3 of the 768-1023 sweep: gate 900 -> 768, on 25 bio pages. Below the gate the
   booking block centred as one column while .bio-book__action-stack kept
   align-items:stretch and a 26rem cap inside the 768 block, so the CTA stack sat centred
   and full-width under centred copy - the same axis mismatch the about awards grid had.
   The asymmetric 58/42 is what the section was designed as; it just never arrived until
   900. minmax(0, ...) added because the CTA stack contains nowrap buttons, which are a
   real min-content floor - a bare fr here is the .svc-form__shell trap one file over. */
@media (min-width: 768px) {
    /* Asymmetric 58/42 - content lean to left, CTA stack right. */
    .bio-book__inner {
        grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
        gap: clamp(2.5rem, 2rem + 3vw, 5rem);
        text-align: left;
    }
}

.bio-book__content {
    /* On mobile this is a centered block; on desktop the grid handles it. */
    margin: 0 auto;
}
@media (min-width: 900px) {
    .bio-book__content { margin: 0; }
}

.bio-book__eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    color: var(--color-heading);
    margin-bottom: var(--space-lg);
}
.bio-book__heading {
    font-family: var(--font-display);
    font-size: var(--text-3xl);
    font-weight: 700;
    line-height: 1.05;
    letter-spacing: -0.02em;
    max-width: 22ch;
    margin: 0 auto var(--space-lg);
    color: var(--color-heading);
    text-wrap: balance;
}
@media (min-width: 900px) {
    .bio-book__heading { margin: 0 0 var(--space-lg); }
}
.bio-book__heading em {
    font-style: italic;
    font-weight: 400;
    /* color-rose (not color-accent) so the italic firstName stays visible
       on the .section--cta teal background. In light .section--cta the
       --color-accent token is rebound to teal #4daab8, which matches the
       section bg and renders teal-on-teal (invisible). Rose has strong
       contrast in both themes' CTA color scheme. */
    color: var(--color-rose, var(--color-accent));
}
.bio-book__lead {
    font-size: var(--text-lg);
    line-height: 1.55;
    max-width: 50ch;
    margin: 0 auto;
    color: var(--color-body);
    text-wrap: pretty;
}
@media (min-width: 900px) {
    .bio-book__lead { margin: 0; max-width: 48ch; }
}

/* ─── Right column: credibility recap + primary CTA ─────────── */
.bio-book__action-stack {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-md);
}
/* The `@media (min-width: 900px) { align-items: flex-start }` that used to sit here
   was DEAD and contradicted the shipped design. A later 768 block in this same file
   sets `align-items: stretch` on the same element at the same (0,1,0) specificity,
   and because it comes further down it won every width from 768 up - so flex-start
   never applied at any viewport, and reading this rule gave the wrong idea of what
   the CTA column does. The stretch is deliberate: it ties the credibility pill and
   the booking button into one block with a shared edge, capped at 26rem. Batch 3
   moved .bio-book__inner's own gate to 768, so the grid and the stretch now switch
   on together and the axis mismatch this pair used to create is gone. Removed
   rather than reordered - there is no width that wants flex-start. */

.bio-book__credibility {
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
    margin: 0;
    padding: 0.55rem 0.95rem;
    background: var(--color-bg-elevated);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-full);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    line-height: 1.4;
    color: var(--color-body);
    box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4) inset;
    max-width: 100%;
}
[data-theme="dark"] .bio-book__credibility {
    box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset;
}
.bio-book__credibility strong {
    color: var(--color-heading);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.bio-book__cred-star {
    display: inline-flex;
    color: var(--color-star, #d4a64a);
    flex-shrink: 0;
    transform: translateY(1px);
}

.bio-book__actions {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-md);
}

/* ─── Section 9: Meet the rest of the team (cross-browse) ────
   Sits BELOW the Book CTA. Deliberately secondary visual weight
   so it never competes with the conversion peak above it. Reads
   as "if not this one, here are some others" not "primary CTA."

   Salon shopping is iterative - readers consider 3-5 stylists
   before booking. This section serves the next-stylist click as
   a positive conversion path, not abandonment.
*/
.bio-meet-team {
    position: relative;
    isolation: isolate;
    padding-block: clamp(3rem, 2rem + 3vw, 5rem);
    background: var(--color-bg-alt);
}
[data-theme="dark"] .bio-meet-team {
    background: var(--color-bg-alt);
}

.bio-meet-team__header {
    text-align: center;
    margin: 0 auto var(--space-2xl);
    max-width: 56ch;
}
.bio-meet-team__heading {
    font-family: var(--font-display);
    font-size: var(--text-2xl);
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: -0.02em;
    margin: 0 0 var(--space-md);
    color: var(--color-heading);
    text-wrap: balance;
}
.bio-meet-team__heading em {
    font-style: italic;
    font-weight: 400;
    color: var(--color-rose, var(--color-accent));
}
.bio-meet-team__lead {
    font-size: var(--text-base);
    line-height: 1.55;
    color: var(--color-body);
    margin: 0;
    text-wrap: pretty;
}

/* ─── Card grid (3 across desktop, 2 tablet, 1 mobile) ────────── */
.bio-meet-team__list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-lg);
    max-width: var(--container-wide, 1200px);
    margin-inline: auto;
}
@media (min-width: 600px) {
    .bio-meet-team__list { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 900px) {
    .bio-meet-team__list {
        grid-template-columns: repeat(3, 1fr);
        gap: var(--space-xl);
        align-items: start;
    }
}

/* Editorial raised/lowered stagger, matching the staff + about + blog card
   grids (playbook item 2 - apply the offset rhythm consistently). Cards are
   near-uniform height (fixed 4:5 media + a short role/name/link body), so the
   alternate offset reads as an intentional magazine cadence. With the section
   capped at 3 stylists this lowers the middle card for a centered offset.
   Desktop 3-col only; reduced-motion opts out. */
@media (prefers-reduced-motion: no-preference) {
    @media (min-width: 900px) {
        .bio-meet-team__list .bio-meet-team__card:nth-child(even) {
            transform: translateY(var(--space-2xl));
        }
    }
}

.bio-meet-team__card {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    /* Grid/flex children default to min-width:auto, so they refuse to shrink below
       their min-content. A long role line ("Owner | Master Stylist | Clinically
       Certified Trichologist") therefore pushed the whole page 98px wide at 320px,
       giving a real horizontal scroll. Intermittent because this strip rotates which
       stylists it shows, so it surfaced on some bios and some loads only. */
    min-width: 0;
}
.bio-meet-team__list > li {
    min-width: 0;
}
.bio-meet-team__role,
.bio-meet-team__name {
    overflow-wrap: break-word;
}

.bio-meet-team__media {
    display: block;
    position: relative;
    aspect-ratio: 4 / 5;
    overflow: hidden;
    border-radius: var(--radius-card, var(--radius-lg));
    background: var(--color-bg);
    box-shadow: var(--shadow-sm);
    text-decoration: none;
    transition: box-shadow var(--transition-base), transform var(--transition-base);
}
/* <picture> from StaffPortrait::pictureTag() must fill the 4:5 frame; it is
   inline by default, so stretch it like the img (matches bio-hero portrait). */
.bio-meet-team__media picture {
    display: block;
    width: 100%;
    height: 100%;
}
.bio-meet-team__media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center 25%;
    display: block;
}
@media (prefers-reduced-motion: no-preference) {
    .bio-meet-team__media img {
        transition: transform 600ms cubic-bezier(0.2, 0.7, 0.2, 1);
    }
}
@media (hover: hover) {
    .bio-meet-team__media:hover {
        box-shadow: var(--shadow-md);
        transform: translateY(-3px);
    }
    .bio-meet-team__media:hover img { transform: scale(1.05); }
}
.bio-meet-team__media:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 3px;
}
.bio-meet-team__placeholder {
    display: grid;
    place-items: center;
    width: 100%;
    height: 100%;
    color: var(--color-muted);
    background: var(--color-bg-elevated);
}

.bio-meet-team__body {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.bio-meet-team__role {
    font-family: var(--font-body);
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--color-eyebrow);
    margin: 0;
    /* Many role strings have pipe-separated segments; clamp to avoid
       multi-line role labels that overwhelm the card. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.bio-meet-team__name {
    font-family: var(--font-display);
    font-size: var(--text-lg);
    font-weight: 700;
    line-height: 1.15;
    margin: 0;
    color: var(--color-heading);
    text-wrap: balance;
}
.bio-meet-team__name a {
    --hit-expander: 1;
    position: relative;
    color: inherit;
    text-decoration: none;
    background-image: linear-gradient(var(--color-rose, var(--color-accent)), var(--color-rose, var(--color-accent)));
    background-repeat: no-repeat;
    background-size: 0% 2px;
    background-position: 0 100%;
    transition: background-size var(--transition-base);
    padding-bottom: 2px;
}
/* Vertical hit-expander: lifts the name link's tap height to 44 without
   changing the visible text or its underline animation. */
.bio-meet-team__name a::after {
    content: "";
    position: absolute;
    top: 50%; left: 0;
    transform: translateY(-50%);
    width: 100%; height: 100%;
    min-height: 44px;
}
@media (hover: hover) {
    .bio-meet-team__name a:hover { background-size: 100% 2px; }
}

.bio-meet-team__cta { margin: 0.25rem 0 0; }
.bio-meet-team__link {
    --hit-expander: 1;
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    color: var(--color-accent);
    text-decoration: none;
    font-weight: 600;
    font-size: var(--text-sm);
    transition: gap var(--transition-fast);
}
.bio-meet-team__link::after {
    content: "";
    position: absolute;
    top: 50%; left: 0;
    transform: translateY(-50%);
    width: 100%; height: 100%;
    min-height: 44px;
}
@media (hover: hover) {
    .bio-meet-team__link:hover { gap: 0.6rem; }
}

/* "See the full team" footer link, centered below the card grid. */
.bio-meet-team__all {
    text-align: center;
    margin: var(--space-2xl) 0 0;
    /* Override global p max-width so the link stays centered in the full
       container instead of inside a 70ch paragraph box. */
    max-width: none;
}
.bio-meet-team__all-link {
    --hit-expander: 1;
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    color: var(--color-heading);
    text-decoration: none;
    font-weight: 600;
    font-size: var(--text-sm);
    border-bottom: 1px solid var(--color-border-hover);
    padding-bottom: 3px;
    transition: gap var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
}
.bio-meet-team__all-link::after {
    content: "";
    position: absolute;
    top: 50%; left: 0;
    transform: translateY(-50%);
    width: 100%; height: 100%;
    min-height: 44px;
}
@media (hover: hover) {
    .bio-meet-team__all-link:hover {
        gap: 0.7rem;
        color: var(--color-accent);
        border-color: var(--color-accent);
    }
}

/* ─── Utility: visually hidden ──────────────────────────────── */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ─── Reduced motion: kill the stagger + scale transforms ────── */
@media (prefers-reduced-motion: reduce) {
    .team-grid__cards .team-card:nth-child(even) { transform: none; }
    .team-card__media,
    .team-card__media img,
    .team-card__media-overlay,
    .team-grid__chip,
    .team-card__profile-arrow,
    .bio-certs__badge,
    .bio-special__link,
    .bio-toc,
    .bio-toc__link { transition: none; }
}

/* ============================================================
   SESSION G - Bio polish additions (2026-05-16)
   ============================================================ */

/* ─── Hero SEO lede ──────────────────────────────────────────
   DOMINANT MOMENT: calm body-voice handoff layer between the
   oversized display name and the uppercase chip set.
*/
.bio-hero__lede {
    max-width: 45ch;
    margin: calc(var(--space-lg) * -0.5) 0 var(--space-xl);
    font-family: var(--font-body);
    font-size: clamp(1.05rem, 0.95rem + 0.4vw, 1.25rem);
    font-weight: 400;
    line-height: 1.55;
    color: var(--color-body);
    text-wrap: pretty;
}

/* ─── Sticky right-rail TOC ──────────────────────────────────
   DOMINANT MOMENT: numbered vertical rail tracks scroll
   position via IntersectionObserver. Auto-hides while the
   hero is on screen so it never competes with the portrait.
   Hidden under 1280px (anchors still work in body).
*/
.bio-toc { display: none; }

/* Show the TOC only where the gutter can actually hold it.
   The sections run in a 1280px .container, so the gutter is (100vw - 1280)/2 -
   80px at 1440, against a 200px-wide rail. It never fit. The old 1440px gate
   let it render anyway and the `max(1rem, ...)` fallback then pinned it to the
   viewport edge ON TOP of the content: measured 136px of overlap at 1440,
   88 at 1536, 56 at 1600, 16 at 1680 - it was sitting across a stylist's face
   in the cross-browse row at every common laptop width.

   The positioning formula below is correct; only this threshold was wrong. It
   stops clamping once (100vw - 1280)/2 - 232 clears 16px, i.e. at 1776px, at
   which point the rail lands in real gutter with a ~32px gap. Below that there
   is no honest place to put it, so it stays hidden and the section headings
   carry the navigation instead. */
@media (min-width: 1776px) {
    .bio-toc {
        display: block;
        position: fixed;
        top: 50%;
        /* Pin to the right gutter outside the container, with a 12px buffer.
           Falls back to a safe inset when the viewport is too narrow for gutter. */
        right: max(1rem, calc((100vw - 1280px) / 2 - 220px - 12px));
        transform: translateY(-50%);
        z-index: 20;
        width: 200px;
        opacity: 1;
        transition: opacity 260ms ease, transform 260ms ease;
    }
    .bio-toc--hidden {
        opacity: 0;
        transform: translateY(-50%) translateX(12px);
        pointer-events: none;
    }
}

/* DOMINANT MOMENT: floating TOC reads as a soft rose pill in both themes.
   Brand-consistent (no light/dark token mixing) but tuned per theme so the
   contrast against cream (light) vs deep teal (dark) stays strong. */
.bio-toc__sticky {
    /* Light mode: warm rose pill on cream. Rose is lifted slightly via gradient
       so it doesn't read as flat. */
    --bio-toc-bg: #efd0d0;
    --bio-toc-bg-2: #f5dddd;
    --bio-toc-fg: var(--color-heading);
    --bio-toc-fg-soft: var(--color-muted);
    --bio-toc-rule: rgba(58, 40, 40, 0.10);
    --bio-toc-hover-bg: rgba(255, 255, 255, 0.55);
    --bio-toc-active: var(--color-accent);
    padding: var(--space-md);
    background: linear-gradient(155deg, var(--bio-toc-bg-2) 0%, var(--bio-toc-bg) 100%);
    border: 1px solid var(--bio-toc-rule);
    border-radius: var(--radius-lg);
    box-shadow:
        0 1px 0 rgba(255, 255, 255, 0.55) inset,
        0 22px 38px -22px rgba(58, 40, 40, 0.25);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    position: relative;
    isolation: isolate;
}
/* Editorial signature: hairline accent rule at the top-left corner so the
   pill has a fixed anchor point that ties to the rest of the brand language. */
.bio-toc__sticky::before {
    content: '';
    position: absolute;
    top: 0;
    left: 1rem;
    width: 2.25rem;
    height: 2px;
    background: var(--color-accent);
    border-radius: 0 0 2px 2px;
    pointer-events: none;
}

/* Dark mode: tinted-teal pill - lifts off the deep teal section background
   while staying in-family with the rest of dark-mode chrome. Headings and
   accents stay teal/cream tokens (no rose on rose stacking issues). */
[data-theme="dark"] .bio-toc__sticky {
    --bio-toc-bg: #0a2a33;
    --bio-toc-bg-2: #0e3340;
    --bio-toc-fg: var(--color-heading);
    --bio-toc-fg-soft: rgba(241, 236, 228, 0.62);
    --bio-toc-rule: rgba(212, 160, 160, 0.20);
    --bio-toc-hover-bg: rgba(255, 255, 255, 0.05);
    --bio-toc-active: var(--color-rose);
    background: linear-gradient(155deg, var(--bio-toc-bg-2) 0%, var(--bio-toc-bg) 100%);
    box-shadow:
        0 1px 0 rgba(255, 255, 255, 0.05) inset,
        0 22px 38px -22px rgba(0, 0, 0, 0.55);
}
[data-theme="dark"] .bio-toc__sticky::before {
    background: var(--color-rose);
}

.bio-toc__label {
    margin: 0 0 var(--space-sm);
    padding-bottom: 0.5rem;
    border-bottom: 1px solid var(--bio-toc-rule);
    font-family: var(--font-mono, var(--font-body));
    font-size: var(--text-2xs);
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--bio-toc-fg-soft);
}

.bio-toc__list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.bio-toc__item { margin: 0; }

.bio-toc__link {
    position: relative;
    display: flex;
    align-items: baseline;
    gap: 0.7rem;
    padding: 0.45rem 0.6rem 0.45rem 0.8rem;
    border-radius: var(--radius-sm);
    color: var(--bio-toc-fg-soft);
    text-decoration: none;
    transition: color 200ms ease, background-color 200ms ease, transform 200ms ease;
}

.bio-toc__link::before {
    content: '';
    position: absolute;
    left: 0;
    top: 30%;
    bottom: 30%;
    width: 2px;
    background: var(--bio-toc-active);
    opacity: 0;
    transition: opacity 200ms ease, top 200ms ease, bottom 200ms ease;
    border-radius: var(--radius-full);
}

.bio-toc__num {
    font-family: var(--font-mono, var(--font-body));
    font-size: var(--text-2xs);
    font-weight: 500;
    color: var(--bio-toc-fg-soft);
    opacity: 0.6;
    letter-spacing: 0.05em;
    flex-shrink: 0;
    transition: color 200ms ease, opacity 200ms ease;
}

.bio-toc__text {
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: 500;
    line-height: 1.3;
}

@media (hover: hover) {
    .bio-toc__link:hover {
        color: var(--bio-toc-fg);
        background: var(--bio-toc-hover-bg);
        transform: translateX(2px);
    }
    .bio-toc__link:hover .bio-toc__num {
        color: var(--bio-toc-active);
        opacity: 1;
    }
}

.bio-toc__link.is-active {
    color: var(--bio-toc-fg);
    background: var(--bio-toc-hover-bg);
}
.bio-toc__link.is-active::before {
    opacity: 1;
    top: 15%;
    bottom: 15%;
}
.bio-toc__link.is-active .bio-toc__num {
    color: var(--bio-toc-active);
    opacity: 1;
}

.bio-toc__link:focus-visible {
    outline: 2px solid var(--bio-toc-active);
    outline-offset: 2px;
}

/* ─── Cert badge: uniform chip + labeled subsection groups ────
   The hierarchy is now LITERAL. PHP sorts certs into three
   labeled subsections (Awards & Recognition / Specialty Training /
   Professional Certifications) and skips any empty group. Every
   chip looks identical; the subsection headers carry the meaning.

   Why this works where the styled-chip-tiers didn't: "Awards &
   Recognition" is a label you READ. No squinting at a chip color
   trying to infer importance.

   Tokens used here all have dark-mode counterparts.
*/

.bio-certs__groups {
    max-width: 1000px;
    margin-inline: auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-xl);
}

.bio-certs__group { margin: 0; }

.bio-certs__group-label {
    font-family: var(--font-mono, var(--font-body));
    font-size: var(--text-2xs);
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--color-eyebrow);
    margin: 0 0 var(--space-md);
    padding-bottom: 0.65rem;
    text-align: center;
    /* Hairline accent rule centered under the label - editorial signature
       that beats a full-width border for centered text. */
    border-bottom: 0;
    position: relative;
}
.bio-certs__group-label::after {
    content: '';
    display: block;
    width: 2.25rem;
    height: 1px;
    background: var(--color-rose, var(--color-accent));
    margin: 0.65rem auto 0;
    opacity: 0.6;
}

.bio-certs__badge-icon {
    background: var(--color-bg);
    color: var(--color-accent);
    border: 1px solid var(--color-border);
}
.bio-certs__badge-icon svg {
    width: 22px;
    height: 22px;
}

/* ============================================================
   SESSION J - Bio elevation pass (2026-05-18)
   - Hero credibility strip + name size bump
   - (retired 2026-08-07: featured before/after moment)
   - Editorial section dividers (ornamental icon + rules)
   - Mobile sticky book bar
   - Section atmosphere (subtle radial accents)
   - Ticket-stub specials redesign
   ============================================================ */

/* ─── Hero name size bump ────────────────────────────────────
   Push the last-name italic up ~12% on desktop for stronger
   editorial signature without breaking the hero viewport lock.
*/
.bio-hero__name {
    font-size: clamp(2.75rem, 2rem + 5.5vw, 6.75rem);
    letter-spacing: -0.04em;
}
.bio-hero__name-last {
    /* Slightly tighter italic kerning so the larger size doesn't sprawl. */
    letter-spacing: -0.045em;
}

/* ─── Hero credibility strip ─────────────────────────────────
   DOMINANT MOMENT: condensed trust-signal row between the
   editorial lede and the chip set. Three signals max - rating,
   review count, tenure - each rendered as a tight badge with
   a numeric foreground and a quiet uppercase label.
*/
.bio-hero__credibility {
    list-style: none;
    padding: 0;
    /* Tucks under the lede; sits above the chip row. */
    margin: calc(var(--space-md) * -0.25) 0 var(--space-lg);
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-md) var(--space-lg);
    align-items: center;
}

.bio-hero__cred {
    display: inline-flex;
    align-items: baseline;
    gap: 0.4rem;
    position: relative;
    padding-right: var(--space-lg);
    color: var(--color-body);
}
.bio-hero__cred:not(:last-child)::after {
    content: '';
    position: absolute;
    right: 0;
    top: 20%;
    bottom: 20%;
    width: 1px;
    background: var(--color-border-hover);
    opacity: 0.7;
}
.bio-hero__cred:last-child { padding-right: 0; }

.bio-hero__cred-icon {
    display: inline-flex;
    align-items: center;
    color: var(--color-star, #d4a64a);
    transform: translateY(2px);
}

.bio-hero__cred-value {
    font-family: var(--font-display);
    font-size: clamp(1.35rem, 1.1rem + 0.6vw, 1.65rem);
    font-weight: 700;
    line-height: 1;
    color: var(--color-heading);
    letter-spacing: -0.01em;
    font-variant-numeric: tabular-nums;
}

.bio-hero__cred-label {
    font-family: var(--font-body);
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--color-muted);
    line-height: 1;
}

/* Tenure pill gets a quiet rose underline so it reads as a
   distinct signal class from the review numerics. */
.bio-hero__cred--tenure .bio-hero__cred-value {
    color: var(--color-accent);
}

/* ─── Mobile bio hero: photo-forward, name over the portrait (V1) ──────────
   Phones get a full-bleed portrait with the stylist's name set INLINE over a
   bottom scrim; the job title + lede + credibility + chips + CTAs sit on the
   cream below - so a long title (e.g. Kristan's "Owner | Master Stylist |
   Clinically Certified Trichologist") wraps cleanly there instead of over the
   photo. The h1 stays inside .bio-hero__content for semantics and is
   absolutely pinned to the portrait's bottom edge via --bio-portrait-h; the
   :has() guard keeps the fallback (normal stacked name) for the rare stylist
   with no portrait. Desktop (>=900px) two-column split is untouched.
   DOMINANT MOMENT: full-bleed portrait + inline name over scrim - .bio-hero__name */
/* PORTRAIT ORIENTATION ONLY, and that is load-bearing rather than tidiness.
   This block trades the portrait frame's aspect-ratio: 3/4 for height: 62svh +
   width: 100% + max-height: none. On a portrait phone that is inert - at 390x844 the
   frame is 390x523 (ratio 0.745), narrower than the 3:4 source, so object-fit: cover
   scales by HEIGHT and object-position's 14% never bites. Rotate the phone and the same
   declaration inverts: at 740x360 the frame is 740x223, a 3.32:1 letterbox, and cover
   crops vertically to source rows 10.8%-33.4% - hair, forehead and the top of the eyes,
   mouth and chin cropped away, with the name printed in white across the strip.
   Reproduced identically on four different bios, so it is the CSS, not one photo.
   Landscape phones fall back to the base single-column hero, which keeps the 3:4 frame.
   Do NOT widen this back to a bare max-width without re-checking 667x330 and 740x360. */
@media (max-width: 767.98px) and (orientation: portrait) {
    .bio-hero {
        min-height: auto;
        display: block;
        padding-block: 0;
        background: var(--color-bg);
        overflow: visible;
    }
    .bio-hero__inner {
        --bio-portrait-h: 62svh;
        position: relative;
        display: flex;
        flex-direction: column;
        /* The desktop grid centres its two columns, and this block was
           overriding display/direction/gap/padding/max-width but NOT that
           align-items. Inherited into a flex COLUMN it stops the children
           stretching, so the portrait shrank to its own content width -
           height 62svh x 3:4 = 418.5px - and centred, while the name overlay
           still spanned the full container. From 430px up the name hung off
           the photo by 6px rising to 174px at 767px: white display type on
           cream, effectively unreadable.

           It looked correct at 390 only by accident - there 418.5px is WIDER
           than the viewport, so max-width:100% clamped it back to full bleed
           and hid the bug on the one width anyone was checking. */
        align-items: stretch;
        gap: 0;
        padding: 0;
        max-width: none;
    }
    .bio-hero__breadcrumbs,
    .bio-hero__portrait-caption { display: none; }

    /* Full-bleed portrait block at the top */
    .bio-hero__portrait {
        order: -1;
        margin: 0 0 clamp(1.4rem, 3.5svh, 1.9rem);
        position: relative;
    }
    .bio-hero__portrait-frame {
        aspect-ratio: auto;
        height: var(--bio-portrait-h);
        width: 100%;
        max-height: none;
        border-radius: 0;
        box-shadow: none;
    }
    .bio-hero__portrait-frame img,
    .bio-hero__portrait-frame picture { object-position: center 14%; }
    /* Bottom scrim so the white name stays legible over the photo */
    .bio-hero__portrait-frame::after {
        box-shadow: none;
        border-radius: 0;
        background: linear-gradient(
            to bottom,
            rgba(14, 9, 9, 0) 42%,
            rgba(14, 9, 9, 0.22) 62%,
            rgba(14, 9, 9, 0.68) 90%,
            rgba(14, 9, 9, 0.8) 100%
        );
    }
    [data-theme="dark"] .bio-hero__portrait-frame::after {
        background: linear-gradient(
            to bottom,
            rgba(2, 18, 24, 0) 42%,
            rgba(2, 18, 24, 0.28) 62%,
            rgba(2, 18, 24, 0.74) 90%,
            rgba(2, 18, 24, 0.86) 100%
        );
    }

    /* Name overlaid INLINE on the portrait's bottom edge. :has() gates this to
       bios that actually render a portrait; without one the name falls back to
       normal in-flow (on cream). */
    .bio-hero__inner:has(.bio-hero__portrait) .bio-hero__name {
        position: absolute;
        top: var(--bio-portrait-h);
        transform: translateY(-100%);
        left: 0;
        right: 0;
        z-index: 3;
        margin: 0;
        padding: 0 clamp(1.25rem, 5vw, 2rem) clamp(1.1rem, 3svh, 1.5rem);
        flex-direction: row;
        flex-wrap: wrap;
        align-items: baseline;
        gap: 0.3em;
        font-size: clamp(2.1rem, 1.3rem + 4vw, 2.9rem);
        letter-spacing: -0.02em;
        color: #fff;
        text-shadow: 0 2px 16px rgba(0, 0, 0, 0.5);
    }
    .bio-hero__inner:has(.bio-hero__portrait) .bio-hero__name-last {
        padding-left: 0;
        color: var(--color-rose);
        letter-spacing: -0.02em;
    }

    /* Title (eyebrow) now leads the cream block below the photo */
    .bio-hero__content { padding: 0 clamp(1.25rem, 5vw, 2rem) clamp(1.5rem, 4svh, 2.5rem); }
    .bio-hero__content > .bio-hero__eyebrow { margin-bottom: var(--space-md); }
    .bio-hero__lede { margin-top: 0; }
}

/* ─── Section seams (2026-08-08) ─────────────────────────────
   REMOVED: .bio-divider - an ornamental icon medallion between
   two hairline rules, stamped at three section seams.

   It failed for a reason worth keeping written down. The divider
   sat BETWEEN two sections, so it inherited the page field
   rather than either neighbour's. Wherever the sections it
   separated carried their own backgrounds (every seam except
   the one it was built for), it rendered as a third horizontal
   stripe in a third tone with an icon floating in the middle of
   it - it read as a rendering artefact, not as punctuation
   (Ben, 2026-08-08: "these weird dividers ... look really
   stupid").

   The seams are now carried by the sections themselves:
   background field changes and the section padding rhythm. That
   is one less element, no inherited-tone trap, and it holds in
   both themes. Do not reintroduce a between-sections ornament
   here - if a seam needs marking, change the field.
*/

/* ─── Mobile sticky book bar ─────────────────────────────────
   DOMINANT MOMENT (mobile): persistent conversion path on a
   3000px-tall bio page. Hidden on tablet+ where the hero CTA
   stays close to the fold and the sticky header carries the
   global Book link.

   Mobile-first (inverted 2026-08-08). This used to be a `display: none` base
   plus a `max-width: 767px` block carrying every real declaration - the same
   shape as the bio hero, and that shape is what hid last session's worst bug: a
   mobile block overriding a desktop base and missing one inherited property.
   Nothing can be under-overridden here now, because the desktop state is a
   single declaration and the mobile state is the base.
*/
.bio-mobile-book {
    display: flex;
    position: fixed;
    left: var(--space-sm);
    right: var(--space-sm);
    bottom: max(env(safe-area-inset-bottom), var(--space-sm));
    z-index: 30;
    padding: 0.4rem 0.4rem 0.4rem 0.85rem;
    gap: 0.5rem;
    align-items: center;
    background: var(--color-bg-elevated);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-full);
    box-shadow:
        0 1px 0 rgba(255, 255, 255, 0.5) inset,
        0 18px 38px -18px rgba(58, 40, 40, 0.4);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1), opacity 280ms ease;
}

[data-theme="dark"] .bio-mobile-book {
    box-shadow:
        0 1px 0 rgba(255, 255, 255, 0.04) inset,
        0 18px 38px -18px rgba(0, 0, 0, 0.6);
}

.bio-mobile-book--dismissed {
    opacity: 0;
    transform: translateY(120%);
    pointer-events: none;
}

.bio-mobile-book__cta {
    flex: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    min-height: 44px;
    padding: 0.7rem 1rem;
    background: var(--color-accent);
    color: var(--color-btn-primary-text);
    border-radius: var(--radius-full);
    text-decoration: none;
    font-family: var(--font-body);
    font-weight: 600;
    font-size: var(--text-sm);
    letter-spacing: 0.02em;
    white-space: nowrap;
}

.bio-mobile-book__icon {
    display: inline-flex;
    align-items: center;
}

.bio-mobile-book__close {
    flex-shrink: 0;
    width: 44px;
    height: 44px;
    display: inline-grid;
    place-items: center;
    background: transparent;
    border: 0;
    border-radius: 50%;
    color: var(--color-muted);
    cursor: pointer;
    transition: background 200ms ease, color 200ms ease;
}

.bio-mobile-book__close:hover,
.bio-mobile-book__close:focus-visible {
    background: var(--color-bg);
    color: var(--color-heading);
    outline: 0;
}

.bio-mobile-book__close:focus-visible {
    outline: 2px solid var(--color-focus-ring);
    outline-offset: 2px;
}

/* Tablet and up: the hero CTA sits near the fold and the sticky header carries
   the global Book link, so the bar is switched off entirely. Complementary to
   the old `max-width: 767px` - identical boundary at every integer width. */
@media (min-width: 768px) {
    .bio-mobile-book {
        display: none;
    }
}

/* Landscape phones too, for the same reason. Once the hero is gated to portrait
   (see the bio-hero block above), a rotated phone gets the base single-column
   layout where the copy stack LEADS - so the hero's own "Book with {name}" CTA
   is already on the first screen and the sticky bar is redundant. It is also
   actively harmful there: at ~360px of viewport height the bar covers the h1.
   Hiding it is what makes the portrait gate free of side effects. */
@media (max-width: 767.98px) and (orientation: landscape) {
    .bio-mobile-book {
        display: none;
    }
}

/* ─── Section atmosphere ─────────────────────────────────────
   Subtle radial accents (3-6% opacity) on bio mid-sections so
   the page doesn't read as a stack of flat panels. Each section
   anchors its glow in a different corner so the eye finds a
   different rhythm walking down the page.
   Reduced motion: gradients stay (they're static, no motion).
*/
.bio-story,
.bio-certs,
.bio-specials {
    position: relative;
    isolation: isolate;
}
.bio-story::before,
.bio-certs::before,
.bio-specials::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    opacity: 0.7;
}
.bio-story::before {
    background: radial-gradient(
        ellipse at 8% 15%,
        rgba(212, 160, 160, 0.10) 0%,
        transparent 55%
    );
}
.bio-certs::before {
    background: radial-gradient(
        ellipse at 92% 85%,
        color-mix(in srgb, var(--color-accent-glow) 10%, transparent) 0%,
        transparent 55%
    );
}
.bio-specials::before {
    background: radial-gradient(
        ellipse at 88% 12%,
        color-mix(in srgb, var(--color-accent-glow) 10%, transparent) 0%,
        transparent 55%
    );
}
[data-theme="dark"] .bio-story::before,
[data-theme="dark"] .bio-certs::before,
[data-theme="dark"] .bio-specials::before {
    opacity: 0.5;
}

/* ─── Ticket-stub specials ───────────────────────────────────
   DOMINANT MOMENT: the expiry chip becomes a vertical ticket
   stub running along the image-body seam on desktop. Image
   bleeds the left edge; rose-tinted body sits to the right.
   On mobile the chip rotates back inline and the card stacks.
*/
.bio-special {
    position: relative;
    isolation: isolate;
    overflow: visible; /* the ticket stub overhangs the image edge */
}
@media (min-width: 600px) {
    .bio-special {
        /* Slightly wider image column for editorial weight. */
        grid-template-columns: 0.75fr 1.25fr;
    }
    .bio-special__media {
        /* Punch the image edge inward by 1px so the stub sits
           ON the seam, not floating beside it. */
        margin-right: -1px;
    }
}

/* Tinted-rose body backdrop with paper-grain noise so the
   editorial right column has its own texture, not just bg-elevated. */
.bio-special__body {
    position: relative;
    isolation: isolate;
    background:
        linear-gradient(
            145deg,
            rgba(212, 160, 160, 0.06) 0%,
            transparent 70%
        ),
        var(--color-bg-elevated);
}
[data-theme="dark"] .bio-special__body {
    background:
        linear-gradient(
            145deg,
            rgba(212, 160, 160, 0.04) 0%,
            transparent 70%
        ),
        var(--color-bg-elevated);
}
.bio-special__body::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    opacity: 0.04;
    background-image: radial-gradient(
        circle at 1px 1px,
        var(--color-heading) 1px,
        transparent 0
    );
    background-size: 22px 22px;
}

/* The expiry chip: stays inline at the top of the body on mobile
   (current behavior). On desktop, the chip rotates 90deg and
   pins to the image-body seam as a "ticket stub". */
@media (min-width: 600px) {
    .bio-special__expiry {
        position: absolute;
        top: 50%;
        left: 0;
        transform: translate(-50%, -50%) rotate(-90deg);
        transform-origin: center;
        margin: 0;
        padding: 0.4rem 0.85rem;
        z-index: 3;
        box-shadow: 0 4px 12px -4px rgba(58, 40, 40, 0.35);
        white-space: nowrap;
    }
    [data-theme="dark"] .bio-special__expiry {
        box-shadow: 0 4px 12px -4px rgba(0, 0, 0, 0.55);
    }
    /* Notch the stub on both ends so it reads as a ticket. */
    .bio-special__expiry::before,
    .bio-special__expiry::after {
        content: '';
        position: absolute;
        top: 50%;
        width: 8px;
        height: 8px;
        background: var(--color-bg-elevated);
        border-radius: 50%;
        transform: translateY(-50%);
    }
    .bio-special__expiry::before { left: -4px; }
    .bio-special__expiry::after  { right: -4px; }
    [data-theme="dark"] .bio-special__expiry::before,
    [data-theme="dark"] .bio-special__expiry::after {
        background: var(--color-bg-elevated);
    }
}

/* Hover lift on the whole card so the stub feels tactile. */
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
    .bio-special {
        transition: transform 320ms cubic-bezier(0.16, 1, 0.3, 1),
                    box-shadow 320ms ease;
    }
    .bio-special:hover {
        transform: translateY(-3px);
        box-shadow: 0 24px 48px -24px rgba(58, 40, 40, 0.32);
    }
    [data-theme="dark"] .bio-special:hover {
        box-shadow: 0 24px 48px -24px rgba(0, 0, 0, 0.55);
    }
}

/* ─── Reduced motion guard for this session's additions ──── */
@media (prefers-reduced-motion: reduce) {
    .bio-special,
    .bio-mobile-book { transition: none; }
}

/* ============================================================
   Bio hero: short-viewport condense (2026-08-07)
   ============================================================
   Removing the hero's fixed `height` stopped the booking CTAs being clipped,
   but on a short laptop it left them below the fold - reachable, yet no longer
   the thing you see. This section buys that space back.

   Every term is keyed to svh rather than a breakpoint, so the hero condenses
   CONTINUOUSLY as vertical room shrinks instead of snapping at some arbitrary
   height. `min(<width term>, <height term>)` inside each clamp means whichever
   axis is scarcer wins, and the clamp's own ceiling keeps tall desktops exactly
   where they were - measured identical at 1080px tall.

   Measured on /team/nikole-matovski/ against live staging, 4 widths x 4
   heights, two samples per cell. Before: CTAs clipped at 12 of 24 viewports.
   After: clipped at none, and on-screen without scrolling at every width from
   768px of viewport height up - which covers 1366x768 and 1440x900 laptops,
   where this page was worst.

   This lives at the end of the file on purpose: the declarations it overrides
   (the Session J name size, the base name/lede/chip margins) are all declared
   further up, and media queries carry no extra specificity, so an earlier
   block would silently lose on source order.

   DOMINANT MOMENT is unchanged - the stacked serif name is still the hero's
   anchor, it just yields size to the booking control when the screen is short.
*/
@media (min-width: 768px) {
    .bio-hero {
        padding-block: clamp(1rem, min(0.5rem + 1.5vw, 2.6svh), 2.5rem);
    }
    .bio-hero__name {
        font-size: clamp(2.75rem, min(2rem + 5.5vw, 0.5rem + 9.6svh), 6.75rem);
        margin-block-end: clamp(0.75rem, 2.2svh, 1.75rem);
    }
    .bio-hero__lede         { margin-block-end: clamp(0.85rem, 2.3svh, 2rem); }
    .bio-hero__credibility  { margin-block-end: clamp(0.75rem, 1.9svh, 1.5rem); }
    .bio-hero__specialties  { margin-block-end: clamp(0.85rem, 2.4svh, 2rem); }

    /* The void down the middle of a wide hero was the text column not being
       used: measured at 1440-2560 the column is ~715-725px while the lede was
       capped at 45ch = 546px, leaving ~170px of empty column before the
       portrait even starts. 56ch fills the column and still sits inside the
       comfortable measure, and it costs the lede a line, which is the other
       half of "the content is too tall". */
    .bio-hero__lede { max-width: 56ch; }

    /* Name set as a wrapping row rather than a hard stack. "Nikole Matovski"
       measures 660px against a 725px column, so short names now land on ONE
       line - reclaiming the horizontal space that was empty and taking ~100px
       of height out of the hero. Longer names (Michelle Evangelista) simply
       wrap to two lines as before. The rose italic surname is untouched; only
       the forced line break goes.

       The clamp ceiling drops 6.75rem -> 6.25rem because the old max let the
       type keep growing past 1600px while the container stopped at 1440, which
       is what pushed the one-line width (734px) back past the column (713px)
       on the widest screens. */
    .bio-hero__name {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: baseline;
        column-gap: 0.28em;
        row-gap: 0;
        font-size: clamp(2.75rem, min(2rem + 5.5vw, 0.5rem + 9.6svh), 6.25rem);
    }
    .bio-hero__name-last { padding-left: 0; }

    /* The rest of the void was the portrait floating in the middle of its own
       column: the frame is height-capped so it renders ~470px wide, but its
       grid column is ~725px, leaving ~125px of slack on each side that read as
       a gap between the two halves of the hero.

       Sizing that column to the portrait itself (auto) removes the slack
       entirely and hands the width back to the text column, which is where the
       reading happens. minmax(0, ...) on the text column keeps long unbroken
       words from forcing an overflow now that it is the flexible one. */
    /* `auto` was right about the slack and wrong about the ceiling - and THIS is the rule that
       governs the band. It is the last .bio-hero__inner declaration in the file, so it beats
       the 1.1fr/0.9fr one ~2000 lines above no matter what that one says.

       An `auto` track takes its item's max-content width, and this portrait's width derives
       from its own max-height (aspect-ratio 3/4 against calc(100svh - header)). So the TALLER
       the viewport, the WIDER the photo track demands to be, and the text track - being the
       flexible one - absorbs the loss. Measured across 25 bio pages, resolved text/photo:

           768x1024   16 / 665     834x1194    0 / 745     960x1024  205 / 661
           800x1280    0 / 712     860x1280    0 / 770    1024x1024  525 / 401
           820x1180    0 / 731     880x1024  126 / 663

       Zero. On an iPad the bio copy had no column at all. It only came right at 1024 because
       the viewport finally outgrew the portrait's intrinsic demand.

       fit-content(42%) keeps the intent - the track still shrinks to the portrait and hands
       the slack back to the text - but caps what it may claim, so a tall viewport can no
       longer starve the reading column. 42%, not 38%: at 1024 the portrait wants 401px of a
       966px row (41.5%), so 42% leaves DESKTOP BYTE-IDENTICAL (525/401 before and after) and
       binds only inside the band. 38% clipped the photo to 367px at 1024 - a change to an
       approved layout as a side effect of a band fix, which is exactly what the parity run
       exists to catch.

       Do not simplify this back to `auto`. Verify any change with
       docs/handoffs/band-sweep2.js (`starved-track`) or _qa/verify-biohero.js at a REAL
       tablet height - an 800px-tall harness measures the conservative case and hides it. */
    .bio-hero__inner {
        grid-template-columns: minmax(0, 1fr) fit-content(42%);
    }
    .bio-hero__portrait { align-items: flex-end; }
}

/* ============================================================
   Bio page: unified vertical rhythm (2026-08-07)
   ============================================================
   Measured down a single bio, the sections were running six different vertical
   rhythms - 44.8/89.6, 89.6, 64, 120, 112 and 75.2 at 1440px. Only the shared
   reviews component used the site's own --space-section token. Six unrelated
   spacings stacked down one page is what reads as unfinished, more than any
   single section does on its own.

   Two deliberate tiers, both derived from that one token, so the contrast
   between them is intentional rather than accidental:
     major      - the beats a client came for (story, work, booking)
     supporting - the context around them (credentials, offers, other stylists)
*/
.bio-story,
.bio-portfolio,
.bio-book {
    padding-block: var(--space-section);
}
.bio-certs,
.bio-specials,
.bio-meet-team {
    padding-block: calc(var(--space-section) * 0.7);
}

/* The story runs straight on from the hero with no divider between them, so a
   full beat on top reads as a dropout rather than a transition. */
.bio-story {
    padding-block-start: calc(var(--space-section) * 0.55);
}

/* These seams used to hold a divider whose margin stacked on top of both
   neighbours' padding: 48 + 32 + 36 + 32 + 64 = 211px of dead space between the
   last credential and "Recent Work" on a 390x844 phone, a quarter of the screen
   left blank. The fix then was to let the divider carry the gap and have the
   sections give back half a beat each.

   The divider is gone (see the Section seams note above), and with it the extra
   36px mark and its two margins. The neighbours now meet on plain section
   padding - the same rhythm the rest of the page uses - so the give-back is no
   longer needed and would pull these seams TIGHTER than every other one. */

/* ============================================================
   Closing Book CTA: make the action column read as one block
   ============================================================
   The credibility pill and the button were sized independently and both
   left-aligned, so the page's single conversion moment came out as two
   unrelated objects of different widths floating in the right-hand column -
   a ~375px pill above a ~240px button, with a ragged right edge between them.

   Stretching the stack ties them into one deliberate booking block with a
   shared edge. The cap stops it becoming a banner on wide screens; the button
   keeps its own centred label. */
@media (min-width: 768px) {
    .bio-book__action-stack {
        align-items: stretch;
        max-width: 26rem;
    }
    .bio-book__actions {
        display: flex;
        width: 100%;
    }
    .bio-book__actions .btn {
        flex: 1;
        justify-content: center;
    }
}

/* ── Cross-browse cards: 2-up on phones ──────────────────────
   These stacked one per row on mobile, so a SECONDARY section ran 2012px and
   showed a single stylist at a time. The point of the section is comparing
   faces to pick a chair, which needs more than one of them on screen. */
.bio-meet-team__list {
    grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 600px) {
    .bio-meet-team__list { grid-template-columns: repeat(3, 1fr); }
}

/* ============================================================
   Story: subordinate the CMS body headings to the section title
   ============================================================
   Bios open with the serif section title ("Meet Nikole") and then the body
   field's OWN heading immediately underneath. Measured at 390px those were
   Fraunces 33.75px/350 followed by Schibsted Grotesk 24px/700 - the subhead
   carrying more visual weight than the title above it, because bold sans at
   700 out-shouts a 350-weight serif even when the serif is 10px larger. Two
   titles competing, and no clear owner of the section.

   The body heading also shipped `margin-bottom: 0`, so the drop-capped opening
   paragraph collided with its underside.

   This keeps both - the section title stays the display moment, the body
   heading becomes a genuine subhead - by pulling weight and size down and
   giving the prose room to start. Specificity is doubled up on purpose
   (.bio-story__body.cms-content) so the global cms-content rules do not win on
   source order. */
.bio-story__body.cms-content :is(h2, h3, h4) {
    font-size: clamp(1.2rem, 1.05rem + 0.55vw, 1.5rem);
    font-weight: 600;
    line-height: 1.3;
    letter-spacing: -0.01em;
    color: var(--color-heading);
    text-wrap: balance;
    margin-block: 2em 0.7em;
}
/* The very first body heading sits directly under the section header, which
   already supplied the space - a second full margin there reads as a dropout. */
.bio-story__body.cms-content > :is(h2, h3, h4):first-child {
    margin-block-start: 0;
}

/* ============================================================
   Credentials: an even grid instead of a centred orphan row
   ============================================================
   The --few modifier switched small credential sets from the auto-fit grid to
   a centred flex row, to stop a lone badge stranding beside a void on bios
   with 5 credentials (the 3+1 orphan). For exactly THREE badges it produced
   the same fault it was written to prevent: `flex: 1 1 300px` fits two per
   760px row, so Nikole's page rendered 2 across with the third centred
   underneath, every box a different width because each sized to its own text.

   A grid keyed to the actual number of badges gives a balanced row at every
   count, equal widths, and equal heights - so a two-line credential no longer
   breaks the row. Quantity queries via :has(), already used elsewhere in this
   file for the mobile hero portrait guard. */
.bio-certs__list--few {
    display: grid;
    grid-template-columns: 1fr;
    max-width: 760px;
}
/* A lone credential would otherwise stretch the full 760px into a banner. The
   old flex rule capped this with max-width: 360px on the badge itself. */
.bio-certs__list--few:has(li:only-child) {
    max-width: 420px;
}
/* Grid owns the sizing now; the flex basis would fight it. */
.bio-certs__list--few .bio-certs__badge {
    flex: initial;
    max-width: none;
}
@media (min-width: 700px) {
    .bio-certs__list--few:has(li:nth-child(2):last-child) {
        grid-template-columns: repeat(2, 1fr);
    }
    .bio-certs__list--few:has(li:nth-child(3):last-child) {
        grid-template-columns: repeat(3, 1fr);
        max-width: 1000px;
    }
    /* Four reads best as a 2x2 block, not a wide single row. */
    .bio-certs__list--few:has(li:nth-child(4):last-child) {
        grid-template-columns: repeat(2, 1fr);
    }
}

/* The role was pinned to ONE line with nowrap + ellipsis to stop long
   pipe-separated titles overwhelming the card. It over-corrected: measured
   across 10 widths in both themes it truncated at every one of them, including
   1920px where the card is ~380px wide and the text had room to spare, so
   "Stylist | Salon Coordinator | Spray Tans" reached the client as "Stylist |
   Salon Coord...". The role is one of the few things someone skims when
   picking a stylist, so it should be readable.

   The first fix took this from one line to two, and explicitly accepted that
   "longer titles still ellipsize, just with twice the room". They did: measured
   on staging, "Master Stylist | Client Relations | Apprentice Mentor" needs 3
   lines in a 167px box at 390, "Stylist | Salon Coordinator | Spray Tans" needs
   3 in 132px at 320, and "Scalp Spa & Wig Specialist | Apprentice" the same.

   Raising the clamp to 3 would only move the cliff to the next long role, so
   the ceiling is gone and only the floor remains. The min-height below is what
   actually delivered the "bounded, predictable card height" the clamp was
   credited with; a role needing a third line now takes it instead of being cut.
   Role strings are short and author-controlled - there is no runaway length
   here that the clamp was protecting against. */
.bio-meet-team__role {
    white-space: normal;
    display: block;
    overflow: visible;
    text-overflow: clip;
    /* Reserve both lines even when the role only needs one. Without this the
       cards in a row end at different heights - a two-line role pushes that
       card's name and "See X's bio" link down out of step with its neighbour,
       which is obvious now that phones show two cards side by side. */
    line-height: 1.3;
    min-height: 2.6em;
}

