  :root { --ease-premium: cubic-bezier(0.16, 1, 0.3, 1); }

  html { scroll-behavior: smooth; }
  body { background-color: #F5F3EF; color: #1E2128; }

  /* Dial Fraunces toward Mackinac's bold, warm, slightly wonky slab character.
     Has no effect now that the real Adobe Fonts embed for p22-mackinac-pro is loaded
     in index.html — these variable-font axes are simply ignored on a static face. */
  .font-display {
    font-variation-settings: 'SOFT' 40, 'WONK' 1, 'opsz' 72;
    font-weight: 600;
    letter-spacing: -0.01em;
  }

  /* Statement tier — the fluid, oversized display size shared by the hero, the
     About headline, and the numbered section-divider h2s. It sets a drastic scale
     contrast against the 12px small-caps annotation labels (left untouched), which
     is the whole point of the jump. Fluid via clamp() so it scales smoothly from
     52px on mobile up to 96px on wide screens rather than stepping at breakpoints.
     Tracking is pulled tighter than the base -0.01em: Mackinac's default spacing
     reads loose past ~70px, so -0.025em keeps the words feeling set as a block.
     Placed after .font-display so its letter-spacing wins (equal specificity). */
  .type-statement {
    font-size: clamp(3.25rem, 8.2vw, 6rem);
    line-height: 0.96;
    letter-spacing: -0.025em;
  }

  /* Hero — a two-tier headline. The upright lead line ("Insight and craft,") is the
     loudest type on the site; the italic line ("shaped with the same hands.") sits a
     step smaller so it reads as a subordinate phrase and, crucially, stays on ONE line
     across the full-width (col-span-12) hero from the md breakpoint up. The two clamps
     are tuned against the real rendered widths so the longer italic line clears its
     column at every desktop/tablet width; below md both lines wrap, which is fine.
     Shared tracking/leading with the rest of the display type. */
  .type-hero-lead {
    font-size: clamp(3.5rem, 9.7vw, 7.75rem);
    line-height: 0.96;
    letter-spacing: -0.025em;
  }
  .type-hero-sub {
    font-size: clamp(2.25rem, 6.1vw, 4.875rem);
    line-height: 0.96;
    letter-spacing: -0.025em;
  }

  /* Quiet "brewing" placeholder — the third Work slot. Warm and informal, deliberately
     off the charcoal documentary system used by the two real case studies: a soft
     verdant fill and a centred "coming soon" note in the site's personal-voice italic,
     at reduced opacity so it eases into the page rather than sitting with the full
     presence of finished work. No image, rule, or Fig tag; the 0.78 opacity lives on the
     panel (a static child) so the .reveal entrance fade on the wrapper doesn't overwrite
     it. The panel matches the real tiles' 4/3 height (and an invisible spacer holds the
     Fig-tag slot in the markup) so the heading below lands on the same baseline as the
     real case-study titles. */
  .work-soon-panel {
    aspect-ratio: 4 / 3;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.75rem;
    border-radius: 0.375rem;
    background-color: rgba(28, 139, 87, 0.10);
    opacity: 0.78;
  }
  .work-soon-note {
    font-family: "p22-mackinac-pro", "Fraunces", Georgia, serif;
    font-style: italic;
    font-size: 1.0625rem;
    line-height: 1.5;
    text-align: center;
    text-wrap: balance;
    color: #1E2128;
    max-width: 22ch;
  }

  .small-caps { font-variant: small-caps; letter-spacing: 0.14em; font-feature-settings: "smcp"; }
  .tag-num { font-variant-numeric: tabular-nums; }

  /* ---- Skip link ---- */
  .skip-link {
    position: absolute; left: -999px; top: auto;
    background: #1E2128; color: #F5F3EF;
    padding: 12px 18px; z-index: 100; border-radius: 4px;
  }
  .skip-link:focus { left: 24px; top: 24px; }

  a:focus-visible, button:focus-visible {
    outline: 2px solid #1C8B57; outline-offset: 3px;
  }

  /* ---- Page router ---- */
  .page[hidden] { display: none; }

  /* ---- Reveal, scroll-linked ----
     Opacity/translateY are set directly from scroll position every frame (see script),
     so elements glide in sync with the scroll instead of firing a fixed-length transition
     once a threshold is crossed. The short transition below only smooths between frames. */
  .reveal {
    opacity: 0;
    transform: translateY(24px);
    transition: opacity 0.05s linear, transform 0.05s linear;
    will-change: opacity, transform;
  }

  /* Mask-wipe variant: text glides up from behind an overflow-hidden clip, no fade —
     used sparingly for the boldest headline moments.

     ROOT CAUSE of the clipping bug this replaces: this span's em-based padding was
     being computed against an INHERITED font-size (Tailwind's preflight resets h1 to
     font-size: inherit, so with nothing else set it fell back to the body's 16px),
     not the ~96px the text actually renders at via a class on the *child* span. That
     made every padding value here roughly 6x too small no matter how generous it
     looked in em units. Fixed by also putting the responsive text-size classes on
     this wrapper (see markup) so its em basis matches the real rendered size.

     Once that was fixed, pixel-level measurement (rendering the real Fraunces font
     and diffing a clipped render against an unclipped ground truth) showed the
     italic line's ascenders — particularly "h" and "t" in "that", which lean up and
     right under the italic slant — overshoot their own line-box top by ~0.85em.
     That's asymmetric to the upright line, which needs much less headroom, hence
     two variants below rather than one shared value. */
  .reveal-clip {
    /* We want to clip ONLY the vertical axis (for the wipe-up reveal) and leave the
       horizontal axis unclipped, since italic glyphs bleed left of their box. But CSS
       forbids that combination: when overflow-y is hidden and overflow-x is visible,
       the spec forces overflow-x to compute to `auto` — which clips horizontally after
       all. So overflow alone can't protect the italic bleed; the left padding on
       .reveal-clip-italic below pulls the bleeding ink inside the (clipped) padding
       box, and an equal negative margin keeps the text visually on the same edge. */
    overflow-x: visible; /* NB: computes to `auto` here (see above) — left padding compensates */
    overflow-y: hidden;  /* only the vertical axis needs clipping, for the wipe-up effect */
    display: block;
  }
  .reveal-clip-upright {
    padding: 0.15em 0 0.35em;
    margin: -0.05em 0 -0.2em;
  }
  .reveal-clip-italic {
    /* left 0.2em: the italic "f" in "follows" bleeds ~0.13em left of the box; padding
       gives it room inside the clip, negative left margin (below) keeps alignment. */
    padding: 1.3em 0 0.5em 0.2em; /* generous top: covers the measured ~0.85em ascender overshoot plus safety margin */
    margin: -1.05em 0 -0.32em -0.2em; /* top pulls the smaller italic sub-line up snug under the lead line; margin only repositions the box, it can't clip (the padding does). Safe at every width including wrapped mobile, since the sub line is a step smaller than the lead so it never rides up over it. */
  }

  /* ---- Header rule entrance: fades in once, on the very first page load ----
     Gated by the .hero-loaded class main.js adds to <html> on DOMContentLoaded.
     Deliberately NOT part of the page-entered system below: the header rule is
     shared chrome that's always present once the site has loaded, so it settles
     once and stays — retriggering it on every in-app navigation would make the
     persistent nav divider flicker on every click, which reads as chrome
     glitching rather than content arriving. */
  .hero-rule {
    opacity: 0;
    transition: opacity 0.2s var(--ease-premium);
  }
  html.hero-loaded .hero-rule { opacity: 1; }

  /* ---- Page load entrance: staggered fade-in every time a page becomes active ----
     Headings/subtext/CTAs rise 20px while fading in over 1s with an exponential
     ease-out (--ease-premium approximates easeOutExpo), staggered on their own
     delays; media fades in last. Gated by the .page-entered class the router
     (main.js) adds to whichever <section class="page"> is now showing — removed
     from every section on each navigation first, so the transition genuinely
     retriggers rather than silently no-opping because the class never changed.
     Decoupled from the scroll-linked reveal engine above: this fires once per
     page visit, from a fixed delay, not from scroll position. */
  .page-load-heading, .page-load-subtext, .page-load-cta, .page-load-media {
    opacity: 0;
    transition: opacity 0.2s var(--ease-premium), transform 0.2s var(--ease-premium);
  }
  .page-load-heading, .page-load-subtext, .page-load-cta { transform: translateY(20px); }

  .page-entered .page-load-heading {
    opacity: 1;
    transform: translateY(0);
    transition-duration: 1s;
    transition-delay: 0.2s;
  }
  .page-entered .page-load-subtext {
    opacity: 1;
    transform: translateY(0);
    transition-duration: 1s;
    transition-delay: 0.35s;
  }
  .page-entered .page-load-cta {
    opacity: 1;
    transform: translateY(0);
    transition-duration: 1s;
    transition-delay: 0.5s;
  }
  .page-entered .page-load-media {
    opacity: 1;
    transition-duration: 0.3s;
    transition-delay: 0.4s;
  }

  /* Media variant: imagery/video settles from a slight zoom while it glides in. */
  .reveal[data-reveal-style="media"] { transform: translateY(24px) scale(0.94); }

  @media (prefers-reduced-motion: reduce) {
    .reveal { opacity: 1 !important; transform: none !important; transition: none !important; }
    .hero-rule, .page-load-heading, .page-load-subtext, .page-load-cta, .page-load-media { opacity: 1 !important; transform: none !important; transition: none !important; }
    html { scroll-behavior: auto; }
  }

  /* ---- Custom cursor ----
     A single cream-colored dot with mix-blend-mode: difference. This blend mode
     auto-inverts against whatever's underneath — cream backgrounds read the dot as
     dark, charcoal backgrounds read it as light — without needing to detect what's
     under the cursor in JS. Fine-pointer (mouse) devices only; touch devices and
     prefers-reduced-motion get the native cursor untouched (see script). */
  html.custom-cursor-active,
  html.custom-cursor-active body {
    cursor: none;
  }
  #cursor-dot {
    position: fixed;
    top: 0;
    left: 0;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background-color: #F5F3EF;
    mix-blend-mode: difference;
    pointer-events: none;
    z-index: 9999;
    opacity: 0;
    transition: opacity 0.3s ease, width 0.25s var(--ease-premium), height 0.25s var(--ease-premium);
    will-change: transform;
  }

  /* ---- Marquee ticker ---- */
  .marquee-strip { overflow: hidden; white-space: nowrap; }
  .marquee-track {
    display: inline-flex;
    width: max-content;
    animation: marquee-scroll 26s linear infinite;
  }
  .marquee-track span.marquee-item {
    display: inline-flex;
    align-items: center;
    padding: 0 1.25rem;
  }
  @keyframes marquee-scroll {
    from { transform: translateX(0); }
    to { transform: translateX(-50%); }
  }
  @media (prefers-reduced-motion: reduce) {
    .marquee-track { animation: none; }
  }

  /* ---- Magnifying glass, peeking from behind a thumbnail corner ----
     Rests fully tucked behind the (opaque, higher z-index) thumbnail — completely
     hidden. Every 5s it slides fully clear of the thumbnail's edge, rocks side to
     side in full view, then retreats back to being completely hidden again.
     The reveal/hide is purely positional (moving in and out of the overlap with
     the opaque thumbnail above it), not opacity — no clip-path needed. */
  .magnifier-peek {
    position: absolute;
    z-index: 0;
    animation: magnifier-peek-cycle 5s ease-in-out infinite;
    transform-origin: 65% 65%;
  }
  @keyframes magnifier-peek-cycle {
    0%, 45% { transform: translate(0, 0) rotate(0deg); }
    58% { transform: translate(74px, 74px) rotate(-12deg); }
    68% { transform: translate(74px, 74px) rotate(12deg); }
    78% { transform: translate(74px, 74px) rotate(-10deg); }
    88% { transform: translate(74px, 74px) rotate(0deg); }
    100% { transform: translate(0, 0) rotate(0deg); }
  }
  @media (prefers-reduced-motion: reduce) {
    .magnifier-peek { animation: none; }
  }

  /* ---- Status dots ----
     Pulsing dots signal a live/current state (verdant = actively available or an
     in-progress project; amber = currently booked). A solid, non-pulsing dot marks
     a settled, finished state — pulsing a completed project would misleadingly
     suggest it's still ongoing. */
  .status-dot,
  .status-dot-amber {
    position: relative;
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background-color: #1C8B57;
  }
  .status-dot-amber { background-color: #B8863B; }
  .status-dot::after,
  .status-dot-amber::after {
    content: '';
    position: absolute;
    inset: -5px;
    border-radius: 50%;
    background-color: #1C8B57;
    opacity: 0.55;
    animation: status-pulse 2s ease-out infinite;
  }
  .status-dot-amber::after { background-color: #B8863B; }
  .status-dot-complete {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background-color: #1E2128;
  }
  @keyframes status-pulse {
    0% { transform: scale(0.6); opacity: 0.55; }
    100% { transform: scale(2); opacity: 0; }
  }
  @media (prefers-reduced-motion: reduce) {
    .status-dot::after,
    .status-dot-amber::after { animation: none; opacity: 0; }
  }

  /* ---- Nav underline reveal, center-out ---- */
  .nav-link { position: relative; }
  .nav-link::after {
    content: ''; position: absolute; left: 50%; bottom: -4px; width: 100%; height: 1px;
    background-color: #1C8B57; transform: translateX(-50%) scaleX(0); transform-origin: center;
    transition: transform 0.45s var(--ease-premium);
  }
  .nav-link:hover::after,
  .nav-link[aria-current="page"]::after { transform: translateX(-50%) scaleX(1); }
  .nav-link[aria-current="page"] { color: #1E2128; }

  /* ---- Invert-fill buttons ----
     Besides the ink-fill sweep, every button gently enlarges on hover: a small,
     slow scale so it reads as a soft cue rather than a pop. inline-block so the
     scale transform applies even to buttons that aren't flex items. */
  .btn-invert {
    display: inline-block;
    position: relative; overflow: hidden; z-index: 0;
    transition: color 0.4s var(--ease-premium), transform 0.5s var(--ease-premium);
  }
  .btn-invert::before {
    content: ''; position: absolute; inset: 0; background-color: #1E2128;
    transform: scaleX(0); transform-origin: left; transition: transform 0.4s var(--ease-premium); z-index: -1;
  }
  .btn-invert:hover::before { transform: scaleX(1); }
  .btn-invert:hover { color: #F5F3EF; transform: scale(1.04); }
  .btn-invert-verdant::before { background-color: #1C8B57; }
  @media (prefers-reduced-motion: reduce) {
    .btn-invert { transition: color 0.4s var(--ease-premium); }
    .btn-invert:hover { transform: none; }
  }

  /* ---- Row / card hover shift ---- */
  .service-row { transition: padding-left 0.5s var(--ease-premium), background-color 0.5s var(--ease-premium); }
  .service-row:hover { padding-left: 1.25rem; background-color: rgba(30,33,40,0.02); }

  /* ---- Practice modules: the abstract visual comes to focus on hover (desktop) or
     tap (mobile, via .is-active toggled in initPracticeModules). At rest it sits quiet
     so bringing it forward rewards engaging with the module. The scroll-reveal lives on
     a wrapper around the svg, so the reveal engine's inline opacity/transform never
     fights this hover state (different elements). ---- */
  .practice-visual {
    opacity: 0.72;
    transform-origin: center;
    transition: opacity 0.45s var(--ease-premium), transform 0.45s var(--ease-premium);
  }
  .practice-module:hover .practice-visual,
  .practice-module.is-active .practice-visual {
    opacity: 1;
    transform: scale(1.03);
  }
  @media (hover: none) { .practice-module { cursor: pointer; } }
  @media (prefers-reduced-motion: reduce) {
    .practice-visual { transition: opacity 0.45s var(--ease-premium); }
    .practice-module:hover .practice-visual,
    .practice-module.is-active .practice-visual { transform: none; }
  }

  .work-card { transition: transform 0.4s var(--ease-premium); }
  .work-card:hover { transform: translateY(-4px); }
  .work-thumb { transition: transform 0.5s var(--ease-premium); }
  .video-preview { transition: opacity 0.4s var(--ease-premium); }
  .work-card:hover .video-preview.has-source { opacity: 1; }

  /* ---- Thumbnail: forward tilt on hover ----
     On hover the inner .thumb-stage tips its top edge toward the viewer — a fixed
     lean, not cursor-tracked — with a slight scale. The negative rotateX is what
     brings the top forward (and lets the bottom recede) about the centre; a positive
     value would lean it the other way, out from the base. Perspective lives on the
     thumb; the tilt is applied to .thumb-stage and never to .work-thumb, because on
     the Work page that element is also a .reveal whose transform the scroll engine
     owns. Pure CSS — no JS. */
  .work-thumb[data-tilt] { perspective: 900px; }
  .thumb-stage {
    position: absolute;
    inset: 0;
    transform-origin: center;
    transition: transform 0.5s var(--ease-premium);
    will-change: transform;
  }
  .thumb-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
  }
  /* .gallery-item is a <button>, so it takes the same tilt on keyboard focus as it
     does on hover — otherwise the affordance only exists for pointer users. */
  .work-thumb-standalone:hover .thumb-stage,
  .work-card:hover .thumb-stage,
  .gallery-item:hover .thumb-stage,
  .gallery-item:focus-visible .thumb-stage {
    transform: rotateX(-6deg) scale(1.02);
  }
  .gallery-item { cursor: pointer; }
  .gallery-item:focus-visible {
    outline: 2px solid #1C8B57;
    outline-offset: 3px;
  }
  @media (prefers-reduced-motion: reduce) {
    .thumb-stage { transition: none; }
    .work-thumb-standalone:hover .thumb-stage,
    .work-card:hover .thumb-stage,
    .gallery-item:hover .thumb-stage,
    .gallery-item:focus-visible .thumb-stage { transform: none; }
  }

  /* ---- Featured-work scroll-scrubbed marquee ----
     A stacked, full-width feature card. With JS off or reduced motion the static rounded
     .feature-hero shows and nothing moves. Otherwise setupFeatureMarquee() adds .is-scroll
     (revealing the strip, hiding the hero) and paintFeatureMarquee() rides the same scroll/rAF
     loop as the reveal engine: from the card's progress through the viewport it translates
     .feature-track horizontally (right to left, scrubbed by scroll, not a timer) and sets each
     tile's own opacity from its distance to the viewport centre (full through a plateau, easing
     to 0.15 at the sides). .feature-stage does not clip, so tiles travel full-bleed across the
     page as discrete 4:3 photo tiles with gutters and hard edges; the Fig line follows whichever
     tile is nearest centre. Movement lives on non-.reveal nodes, clear of the reveal engine's
     inline opacity on the wrapper. */
  /* The feature tiles bleed to the viewport edges; clip #home horizontally so the wide
     off-screen strip never adds a horizontal scrollbar. overflow: clip (not hidden) avoids
     making #home a scroll container, so it can't affect the sticky nav or vertical scrolling. */
  #home { overflow-x: hidden; overflow-x: clip; }
  .feature-stage { overflow: visible; } /* unclip so tiles cross the page, not the card box */
  .feature-hero {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    border-radius: 0.375rem; /* keep the rest-state plate rounded even though the stage no longer clips */
    transition: opacity 400ms var(--ease-premium);
  }
  .feature-card.is-scroll .feature-hero { opacity: 0; }
  .feature-strip {
    position: absolute;
    inset: 0;
    opacity: 0;
    pointer-events: none; /* full-bleed tiles must never intercept clicks on the card link */
    transition: opacity 400ms var(--ease-premium);
  }
  .feature-card.is-scroll .feature-strip { opacity: 1; }
  .feature-track {
    display: flex;
    height: 100%;
    width: max-content;
    will-change: transform;
  }
  .feature-shot {
    flex: 0 0 auto;
    height: 100%;
    object-fit: cover;
    display: block;
    margin-right: 1.25rem;
    border-radius: 1rem; /* each image sits in its own rounded tile */
    aspect-ratio: 4 / 3; /* landscape default; portrait tiles override below for variation */
    opacity: 0.15; /* JS raises the centred tile; a low default avoids a flash before rAF runs */
  }
  .feature-shot[data-orient="portrait"] { aspect-ratio: 3 / 4; }
  @media (prefers-reduced-motion: reduce) {
    .feature-strip { display: none; }
  }

  /* ---- Lightbox ----
     One dialog reused by every gallery tile. It sits outside the .page sections so the
     router never hides it mid-open. The backdrop is the close target; the figure stops
     propagation so clicking the image itself doesn't dismiss it. */
  .lightbox {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.5rem;
    background-color: rgba(30, 33, 40, 0.94);
    opacity: 0;
    transition: opacity 0.35s var(--ease-premium);
  }
  .lightbox.is-open { opacity: 1; }
  .lightbox-figure {
    margin: 0;
    max-width: min(1200px, 100%);
    max-height: 100%;
    display: flex;
    flex-direction: column;
    gap: 0.9rem;
    align-items: center;
  }
  .lightbox-img,
  .lightbox-video {
    display: block;
    max-width: 100%;
    /* leave room for the caption so tall posters never push it off screen */
    max-height: calc(100vh - 8rem);
    width: auto;
    height: auto;
    object-fit: contain;
    border-radius: 0.375rem;
  }
  .lightbox-video { background-color: #000; }
  .lightbox-img[hidden],
  .lightbox-video[hidden] { display: none; }

  /* Play badge over a video gallery tile. It lives inside .thumb-stage so it
     leans with the same tilt as the poster, and is pointer-events:none so the
     click always lands on the button underneath. */
  .thumb-play {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
  }
  .thumb-play-badge {
    width: 4rem;
    height: 4rem;
    border-radius: 999px;
    background-color: rgba(30, 33, 40, 0.5);
    border: 1px solid rgba(245, 243, 239, 0.75);
    color: #F5F3EF;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background-color 0.3s var(--ease-premium);
  }
  .gallery-item:hover .thumb-play-badge,
  .gallery-item:focus-visible .thumb-play-badge {
    background-color: rgba(28, 139, 87, 0.85);
  }
  .lightbox-caption {
    color: rgba(245, 243, 239, 0.7);
    font-size: 11px;
    letter-spacing: 0.1em;
    text-align: center;
  }
  .lightbox-close {
    position: absolute;
    top: 1.25rem;
    right: 1.25rem;
    width: 2.75rem;
    height: 2.75rem;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid rgba(245, 243, 239, 0.3);
    border-radius: 999px;
    color: rgba(245, 243, 239, 0.75);
    background-color: transparent;
    cursor: pointer;
    transition: color 0.3s var(--ease-premium), border-color 0.3s var(--ease-premium);
  }
  .lightbox-close:hover,
  .lightbox-close:focus-visible { color: #F5F3EF; border-color: rgba(245, 243, 239, 0.8); }
  @media (min-width: 768px) {
    .lightbox-close { top: 2rem; right: 2rem; }
  }
  @media (prefers-reduced-motion: reduce) {
    .lightbox { transition: none; }
  }

  /* ---- Case study hero ----
     A full-bleed band sitting behind the case study title: a muted video on SOAT
     social, a photo on SOAT branding. Three layers: the media at z-0, a dark scrim
     at z-1, and the copy at z-2. The charcoal base shows through while the media is
     still loading, so the cream copy never lands on bare cream.

     The scrim carries the whole readability burden, because the media underneath is
     arbitrary: the branding photo is a bright golden-hour frame and the video runs
     60s through changing light. It is therefore weighted for the worst case rather
     than the frame that happens to be showing, and deepens toward the foot where the
     smaller subtitle sits. */
  .case-hero {
    position: relative;
    overflow: hidden;
    background-color: #1E2128;
    isolation: isolate;
  }
  .case-hero-media {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: 0;
    pointer-events: none;
  }
  .case-hero-scrim {
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background: linear-gradient(to bottom, rgba(30, 33, 40, 0.60), rgba(30, 33, 40, 0.80));
  }
  .case-hero-inner { position: relative; z-index: 2; }

  /* ---- Case study hero sound toggle ----
     Deliberately quiet: the video is silent by default and this is the only way to
     opt in, so it reads as an offer rather than a control demanding attention. The
     dot borrows the status-dot language used elsewhere: verdant once sound is live. */
  .case-hero-sound {
    position: absolute;
    z-index: 3;
    right: 1.5rem;
    bottom: 1.5rem;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.9rem;
    border: 1px solid rgba(245, 243, 239, 0.3);
    border-radius: 999px;
    background-color: rgba(30, 33, 40, 0.35);
    color: rgba(245, 243, 239, 0.7);
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 0.1em;
    transition: color 0.4s var(--ease-premium),
                border-color 0.4s var(--ease-premium),
                background-color 0.4s var(--ease-premium);
  }
  @media (min-width: 768px) {
    .case-hero-sound { right: 2.5rem; bottom: 2.5rem; }
  }
  .case-hero-sound:hover {
    color: #F5F3EF;
    border-color: rgba(245, 243, 239, 0.65);
    background-color: rgba(30, 33, 40, 0.55);
  }
  .case-hero-sound-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background-color: rgba(245, 243, 239, 0.45);
    transition: background-color 0.4s var(--ease-premium);
  }
  .case-hero-sound.is-on { color: #F5F3EF; border-color: rgba(28, 139, 87, 0.9); }
  .case-hero-sound.is-on .case-hero-sound-dot { background-color: #1C8B57; }

  /* ---- Hover tag reveal ----
     A cluster of "areas of expertise" pills sits just above the thumbnail's own
     clipped (overflow-hidden) box, so at rest it's fully invisible — not faded,
     genuinely outside the visible area. On hover it slides down into that clipped
     box, reading as pills sliding in and settling at the top-right corner. */
  .tag-peek {
    position: absolute;
    top: 0.75rem;
    right: 0.75rem;
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 0.4rem;
    max-width: calc(100% - 1.5rem);
    transform: translateY(-160%);
    transition: transform 0.5s var(--ease-premium);
    pointer-events: none;
    z-index: 20;
  }
  .work-card:hover .tag-peek,
  .work-thumb-standalone:hover .tag-peek {
    transform: translateY(0);
  }
  .tag-peek span {
    background-color: #F5F3EF;
    color: #1E2128;
    font-variant: small-caps;
    letter-spacing: 0.1em;
    font-size: 11px;
    font-weight: 500;
    padding: 0.4rem 0.8rem;
    border-radius: 999px;
    white-space: nowrap;
    border: 1px solid rgba(30, 33, 40, 0.85); /* matches the Practice page's own capability-tag treatment — outlined, not filled+shadow */
  }
  .tag-peek span:first-child {
    border-color: #1C8B57; /* the first tag carries the accent, echoing how a verdant mark leads every eyebrow label on the site */
    color: #1C8B57;
  }

  /* ---- About page closing reveal ----
     The hometown photo is visible from the instant the About page becomes active,
     sitting behind the type under the same scrim strength the case study heroes use,
     so the cream copy reads over it without the photo being hidden first. Bio and
     Approach/Scope/Based are likewise visible on arrival, stacked in normal flow one
     above the other.

     A tall spacer (.about-reveal-stage) then gives room to scroll while the inner
     panel stays pinned (position: sticky), driving one continuous exit: bio and
     Approach travel up and fade out together while the scrim lifts and the photo
     settles, ending with the photo alone, undimmed, filling the screen.

     The gradient below is the scrim's resting strength (matching .case-hero-scrim);
     the element's opacity is what paintAboutReveal animates, 1 -> 0. */
  .about-reveal-stage {
    position: relative;
    height: 260vh;
  }
  .about-reveal-sticky {
    position: sticky;
    top: 0;
    height: 100svh;
    overflow: hidden;
  }
  .about-reveal-photo {
    position: absolute;
    /* Bleeds past the panel vertically. At rest the photo sits 60px down (it settles
       upward as you scroll) and scale(1.05) only claws back a little of that, so at
       inset:0 a band of the charcoal stage was left showing along the top. The bleed
       is >= the 60px travel, so the photo covers the panel at every point on the
       curve regardless of panel height; .about-reveal-sticky's overflow:hidden clips
       the excess. */
    inset: -72px 0;
    background-size: cover;
    background-position: center;
    transform: translateY(60px) scale(1.05);
    will-change: transform;
  }
  .about-reveal-scrim {
    position: absolute;
    inset: 0;
    background: linear-gradient(to bottom, rgba(30, 33, 40, 0.60), rgba(30, 33, 40, 0.80));
    will-change: opacity;
  }
  .about-reveal-bio {
    will-change: opacity, transform;
  }
  .about-reveal-content {
    will-change: opacity, transform;
  }
  /* Below md, the Approach/Scope/Based grid collapses to a single stacked column,
     tall enough that bio + Approach together can exceed a short mobile viewport —
     with .about-reveal-sticky's overflow:hidden that means real clipping, not just
     a layout squeeze. Small, unpredictable mobile viewport heights make the pinned
     scroll-jack effect fragile generally, so it's skipped below md the same way it's
     skipped under reduced motion: everything just shown, statically, in flow (see
     the matching width guard in paintAboutReveal, js/main.js, which needs to stop
     setting inline styles here too — CSS alone can't out-specificity that). */
  @media (max-width: 767px), (prefers-reduced-motion: reduce) {
    .about-reveal-stage { height: auto; }
    .about-reveal-sticky { position: static; height: auto; padding: 5rem 0; }
    .about-reveal-photo { transform: none; }
    /* Full opacity so the gradient's own 0.60-0.80 is the darkening. The photo never
       de-dims here (there's no scroll sequence to lift it), so this has to stay at
       readable strength permanently rather than the lighter value that suited the
       old solid-colour scrim. */
    .about-reveal-scrim { opacity: 1; }
    .about-reveal-bio { opacity: 1; transform: none; }
    .about-reveal-content { opacity: 1; transform: none; }
  }

  .masthead-rule { border-top: 1px solid #1E2128; }

  ::selection { background-color: #1C8B57; color: #F5F3EF; }

  /* ---- Mobile nav toggle (hamburger / close) ----
     Two hairlines that rotate into an X, matching the site's line-based visual
     language instead of a filled icon glyph. */
  .nav-toggle-icon,
  .nav-toggle-icon::before,
  .nav-toggle-icon::after {
    content: '';
    position: absolute;
    left: 50%;
    width: 20px;
    height: 1px;
    background-color: #1E2128;
    transform: translateX(-50%);
    transition: transform 0.35s var(--ease-premium), opacity 0.2s ease, top 0.35s var(--ease-premium);
  }
  .nav-toggle-icon { top: 50%; }
  .nav-toggle-icon::before { top: -6px; }
  .nav-toggle-icon::after { top: 6px; }
  .nav-toggle[aria-expanded="true"] .nav-toggle-icon { background-color: transparent; }
  .nav-toggle[aria-expanded="true"] .nav-toggle-icon::before { top: 0; transform: translateX(-50%) rotate(45deg); }
  .nav-toggle[aria-expanded="true"] .nav-toggle-icon::after { top: 0; transform: translateX(-50%) rotate(-45deg); }

  /* ---- Mobile nav panel ----
     Sits beneath the persistent header (top offset matches its h-20 height) so the
     logo and the now-X toggle stay reachable while the panel is open. Uses
     visibility (not display) so the close transition can play, and a delayed
     visibility switch keeps it out of the tab order once fully hidden. */
  .mobile-nav {
    position: fixed;
    top: 80px;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 40;
    background-color: #F5F3EF;
    overflow-y: auto;
    opacity: 0;
    visibility: hidden;
    transform: translateY(-12px);
    transition: opacity 0.35s var(--ease-premium), transform 0.35s var(--ease-premium), visibility 0s linear 0.35s;
  }
  .mobile-nav.is-open {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
    transition: opacity 0.35s var(--ease-premium), transform 0.35s var(--ease-premium), visibility 0s linear 0s;
  }
  html.menu-open,
  html.menu-open body {
    overflow: hidden;
  }
  .mobile-nav-row {
    display: flex;
    align-items: baseline;
    gap: 1.5rem;
    padding: 1.5rem 0;
  }
  .mobile-nav-link { display: inline-block; }
  @media (prefers-reduced-motion: reduce) {
    .mobile-nav { transition: opacity 0.01s linear, visibility 0.01s linear; transform: none; }
    .nav-toggle-icon,
    .nav-toggle-icon::before,
    .nav-toggle-icon::after { transition: none; }
  }

  /* ---- Scroll cue ---- */
  .scroll-cue-line {
    width: 1px; height: 22px; background: rgba(30,33,40,0.55);
    animation: scroll-cue-move 1.8s var(--ease-premium) infinite;
  }
  @keyframes scroll-cue-move {
    0% { transform: scaleY(0.2); transform-origin: top; opacity: 0.4; }
    50% { transform: scaleY(1); transform-origin: top; opacity: 1; }
    100% { transform: scaleY(0.2); transform-origin: bottom; opacity: 0.4; }
  }
  @media (prefers-reduced-motion: reduce) { .scroll-cue-line { animation: none; } }
