/* ============================================================
   8builders.us — holding page

   Ported from the production system at
   studiopractice/clients/eightbuilders/site/shared/. Tokens are COPIED,
   not linked, so this project has zero cross-repo dependencies — they
   must be synced by hand if the palette moves.

   LAYOUT: brand bar on top (lockup + CSLB classifications), photograph
   beneath it carrying the message. The bar is a real page header, not the
   bottom band relocated.

   The previous blank-page version of this file is preserved at
   _parked/blank/ and is not deployed.
   ============================================================ */

:root {
  /* ── brand gold ──
     One gold. Every surface on this page is dark, so --gold carries type
     freely (10.6:1 on the ground). The #735c00 light-surface variant from
     the production tokens is deliberately absent — there is no light
     surface here to make it legal for. */
  --gold:      #C9A020;
  --gold-rgb:  201, 160, 32;

  --ground:    #0B0A08;   /* brand ground — warm near-black, not pure */
  --band:      #0D0C09;   /* surface-1: the bar lifts a single step */

  --ink:       #E8E6E0;
  --ink-soft:  #A8A090;

  --on-gold:   #0B0A08;

  --font-display: 'Montserrat', system-ui, sans-serif;
  --font-cond:    'Barlow Condensed', 'Montserrat', sans-serif;

  /* Floor is 1.75rem and it is load-bearing. "Construction." is one
     unbreakable 13-character word; in Montserrat 800 at -0.02em it measures
     195px at this floor, against 280px of usable width at 320px. Raising the
     floor amputates the word on small iPhones — the exact bug the production
     hero token was rewritten to fix. Verified by measurement, not estimated. */
  --t-title:   clamp(1.75rem, 7vw, 4.5rem);
  --t-lede:    clamp(1rem, 1.6vw, 1.1875rem);

  --lh-tight:  0.92;
  --lh-body:   1.6;       /* body-copy readability standard */
  --measure:   46ch;

  --grid-max:  1440px;
  --inset: max(20px, calc((100% - var(--grid-max)) / 2 + 80px));

  --ease-out:  cubic-bezier(0.16, 1, 0.3, 1);
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* The ground goes on <html>, not only <body>. The root element paints the
   canvas on the first frame — leaving it to body shows the browser's own
   white until body is laid out. */
html {
  background: var(--ground);
  -webkit-text-size-adjust: 100%;
}

/* Body is the page grid: the bar takes the height it needs, the photograph
   takes everything left. No magic number ties the two together, so the bar
   can grow on a narrow screen without the media having to be re-tuned. */
body {
  display: grid;
  grid-template-rows: auto 1fr auto;   /* bar · photograph · credit strip */
  min-height: 100vh;
  min-height: 100dvh;           /* iOS toolbar must not expose a seam */
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-display);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img, video { display: block; max-width: 100%; }

/* ── the gold struck rule ─────────────────────────────────────────────────
   Carried over from the blank page — the one brand gesture this domain has
   already shipped. With the bar on top it now reads as the bar's own upper
   rule, bracketing it against the seam below.

   Fixed, so it holds at the top edge if the page is scrolled or
   rubber-banded on iOS rather than sliding away. Brightest at the centre
   and falling off toward the edges, so it reads as a struck line rather
   than browser chrome or a loading indicator — both of which a flat
   full-width band gets mistaken for. */
html::before {
  content: '';
  position: fixed;
  top: 0;
  left: 0;
  right: 0;                     /* not 100vw — that includes the scrollbar */
  height: 3px;
  z-index: 4;
  background: linear-gradient(90deg,
    rgba(var(--gold-rgb), 0.35) 0%,
    rgba(var(--gold-rgb), 0.9) 30%,
    rgba(255, 244, 214, 1) 50%,
    rgba(var(--gold-rgb), 0.9) 70%,
    rgba(var(--gold-rgb), 0.35) 100%);
  box-shadow:
    0 0 10px rgba(var(--gold-rgb), 0.5),
    0 0 26px rgba(var(--gold-rgb), 0.22);
}

/* ── BRAND BAR ───────────────────────────────────────────────────────────── */
.brandbar {
  position: relative;
  z-index: 3;
  background: var(--band);
  padding: clamp(1.125rem, 3vw, 1.75rem) var(--inset);
}

.brandbar__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: clamp(1rem, 3vw, 2rem);
}

/* Height-driven, with the intrinsic width/height attributes on the <img>
   reserving the correct box before the PNG decodes. The asset is 1383x346,
   a fixed 4:1 — update this if the lockup is ever redrawn. */
.brandbar__lockup {
  height: clamp(2.125rem, 4.4vw, 3.25rem);
  width: auto;
}

.brandbar__seam {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;                     /* not 100vw — excludes the scrollbar */
  height: 1px;
  background: linear-gradient(90deg,
    rgba(var(--gold-rgb), 0.95) 0%,
    rgba(var(--gold-rgb), 0.95) 72%,
    rgba(var(--gold-rgb), 0.35) 100%);
  transform-origin: left center;
  animation: seam 1.4s var(--ease-out) 1.15s both;
}

@keyframes seam {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* ── the credential block — DORMANT, DO NOT DELETE ────────────────────────
   The markup this styles is commented out in index.html, pulled 2026-08-08
   pending confirmation from the client. See the note there for why.

   This is not dead code to be swept: it is verified, measured (7.95:1 and
   7.54:1 on the brand ground) and expected back. Restoring the block is
   deleting two comment markers. */
.cred {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  /* Moved out of the brand bar and into the copy, so it needs its own
     separation from the lede above it. A hairline rather than whitespace: it
     marks the shift from statement to specification. */
  margin-top: clamp(1.5rem, 3.5vh, 2.25rem);
  padding-top: clamp(1.25rem, 3vh, 1.75rem);
  border-top: 1px solid rgba(var(--gold-rgb), 0.22);
  /* Matched to --measure, the same line length the lede is set to, so the rule
     ends where the copy above it ends. It was 34rem, which stopped at neither
     the text nor the headline and read as an arbitrary width. */
  max-width: var(--measure);
}

.cred__label {
  font-family: var(--font-cond);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--gold);
}

.cred__label span { opacity: 0.5; margin: 0 0.25em; }

/* Two columns, so four items read as a set rather than a paragraph, and so
   the bar stays a strip instead of growing into a section. Collapses to one
   column on narrow screens. */
.cred__classes {
  display: grid;
  grid-template-columns: repeat(2, auto);
  gap: 0.375rem 1.5rem;
  list-style: none;
}

.cred__classes li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--font-cond);
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  line-height: 1.2;
  color: var(--ink-soft);
  white-space: nowrap;
}

/* NOT a plate. Filled gold rectangles with dark centred labels read as
   buttons — a false affordance on a page whose only interactive element is the
   email link, and four of them in a grid compounds it.

   The production site's stamp device works there because it is ONE stamp used
   sparingly. Repeated four times it stops being a stamp and becomes a toolbar.

   So the box is gone and alignment does the structuring instead: gold code in a
   fixed-width column, classification name in ink beside it. Reads as a spec
   list, which is what it is. The gold still carries the brand, it just no
   longer offers to be pressed. */
.cred__code {
  flex: none;
  min-width: 3.375rem;
  color: var(--gold);
  font-weight: 700;
  font-size: 0.75rem;
  letter-spacing: 0.1em;
}

/* ── PHOTOGRAPH + MESSAGE ─────────────────────────────────────────────────
   min-height:0 is required, not cosmetic: a grid item defaults to min-content
   in the block axis, so without it the 1fr row refuses to shrink below the
   media's intrinsic height and the page grows a scrollbar on short screens. */
.holding {
  position: relative;
  display: grid;
  min-height: 0;
  overflow: hidden;
}

.holding__media {
  position: absolute;
  inset: 0;
  z-index: 0;
}

.holding__img {
  width: 100%;
  height: 100%;
  object-fit: cover;            /* fills without ever stretching one axis */
  object-position: center 38%;  /* holds the silhouette and the bridge line */
}

/* The still is the reduced-motion twin of the video. Only one is ever
   displayed, swapped by media query at the foot of this file. */
.holding__still { display: none; }

/* ── scrim ──
   Values taken from the production hero rather than tuned fresh. The first
   pass used a lighter two-stop wash to keep the photograph open; measured
   across the 10-second loop, the gold eyebrow fell to 3.25:1 at t≈7.5s as
   the camera moved a bright area under it. AA needs 4.5:1 for type that
   size. The production values already solve this on the real site, so they
   are reused verbatim instead of re-derived.

   Mobile is a vertical wash, because the copy sits over the middle of the
   frame. Desktop switches to horizontal, because the copy moves left.

   DELIBERATELY LIGHT. This started at the production hero's values, which
   are heavy because there the scrim is the only layer and has to carry the
   type on its own. Here it does not: the copy pool and the credit wash each
   guarantee their own local floor, so this layer is free to go back to being
   atmosphere rather than a contrast device.

   It was worth reclaiming. At the heavy values, three dark layers stacked and
   the golden-hour light and the Bay Bridge — the reason this frame was chosen
   — had gone muddy. Type is protected where type actually is; everywhere else
   the photograph is allowed to be a photograph.

   Every number here is held by audit.mjs, so this can be pushed lighter with
   evidence rather than nerve. */
.holding__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to bottom,
    rgba(11, 10, 8, 0.58) 0%,
    rgba(11, 10, 8, 0.34) 32%,
    rgba(11, 10, 8, 0.48) 62%,
    rgba(11, 10, 8, 0.66) 100%
  );
}

/* ── copy ──────────────────────────────────────────────────────────────── */
/* Centred in the photograph, not bottom-pinned. It was `end` when the brand
   band sat at the foot of the page, where copy just above it read as one
   unit. With the band moved to the top, bottom-pinning left a dead zone
   through the middle of the frame and pushed the copy against the edge. */
.holding__inner {
  position: relative;
  z-index: 1;
  align-self: center;
  padding: clamp(2rem, 5vh, 4rem) var(--inset);
}

/* ── the copy pool ────────────────────────────────────────────────────────
   A guaranteed contrast floor that TRAVELS WITH THE COPY.

   This exists because of a specific failure. The scrim is anchored to the
   frame, so its dark zone sat wherever the frame's bottom was. When the copy
   moved from bottom-pinned to centred, it slid off that dark zone and the
   gold eyebrow fell to 2.52:1 on a 320px screen — from 7.39:1 — without a
   single line of the scrim changing. Re-tuning the scrim would have fixed
   that instance and left the same trap for the next move.

   Anchoring the floor to the copy's own box removes the class of bug: the
   backdrop cannot drift away from the type again, at any viewport, because
   it is drawn relative to the type.

   Generous negative insets and a long tail to transparent, so it reads as
   depth in the photograph rather than a panel behind the words. z-index -1
   sits it behind the copy but still above the media, because .holding__inner
   establishes its own stacking context. */
.holding__inner::before {
  content: '';
  position: absolute;
  z-index: -1;
  top: -70%;
  bottom: -70%;
  left: 0;
  right: -25%;
  pointer-events: none;
  /* The vertical radius MUST land the transparent stop inside the box, or the
     box edge shears the gradient and paints a visible horizontal seam across
     the photograph. The first version used 112% with the last stop at 90%,
     which reaches zero alpha at ~101% of the box height from the centre —
     outside the box — and cut two hard lines through the frame.
     46% resolves to 0.46 of the box height from centre against a half-height
     of 0.5, so it finishes with room to spare. Horizontally it is allowed to
     run past the edges: those edges are the viewport's own, off-frame. */
  background: radial-gradient(125% 46% at 12% 50%,
    rgba(11, 10, 8, 0.95) 0%,
    rgba(11, 10, 8, 0.88) 40%,
    rgba(11, 10, 8, 0.55) 72%,
    rgba(11, 10, 8, 0) 100%);
}

.holding__eyebrow {
  font-family: var(--font-cond);
  font-size: 0.875rem;
  font-weight: 500;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--gold);
  margin-bottom: clamp(0.75rem, 2vh, 1.25rem);
}

.holding__eyebrow span { opacity: 0.55; margin: 0 0.2em; }

.holding__title {
  /* balance, not pretty: with the <br> gone this is what decides where the
     phrase breaks on a narrow screen, and balance keeps the two lines close in
     length instead of leaving one orphaned word. */
  text-wrap: balance;
  font-size: var(--t-title);
  font-weight: 800;
  line-height: var(--lh-tight);
  letter-spacing: -0.02em;
  color: var(--ink);
}

.holding__lede {
  max-width: var(--measure);
  margin-top: clamp(1rem, 2.5vh, 1.5rem);
  font-size: var(--t-lede);
  font-weight: 400;
  line-height: var(--lh-body);
  color: var(--ink-soft);
}

/* ── the Studio Practice credit ───────────────────────────────────────────
   A real strip on brand ground, NOT floating over the photograph.

   It floated first, and that cost four rounds of gradient tuning. Dim type
   over moving footage is fragile by nature: every wash adjustment changed the
   credit's own height or position, which moved it to a different part of the
   gradient, and the failure followed it — 4.02, then 4.28 at a new
   coordinate, then again. Chasing it was the wrong move.

   On a solid ground the ratio is a constant. It cannot depend on the frame,
   the viewport, the line length, or where the wash happens to be strong, so
   it never needs tuning again. The cost is about 44px of photograph and the
   gain is a whole class of bug removed.

   It also brackets the page: brand ground at the top carrying the lockup,
   brand ground at the foot carrying the credit, photograph between them —
   which is the approved OG card's own structure.

   Left on the same inset axis as everything else. Every element on this page
   hangs off one left margin; a centred credit would be the only one ignoring
   it. */
.credit {
  background: var(--band);
  padding: clamp(0.75rem, 2vh, 1rem) var(--inset);
}

/* Pushed back deliberately. A studio credit on a client's brand page should
   be findable, not present — it sits at the quietest tier on the page, below
   the classification list, and only the studio name is allowed any weight.

   --credit-ink is its own token rather than --ink-mute so the credit can be
   dimmed without touching anything else that uses that tier.

   HOW DIM IS ACTUALLY POSSIBLE. The first attempt at "pushed back" used
   #7C766A and measured 3.66:1, under the 4.5 AA needs at this size. Adding
   the wash below moved it to 4.33 and no further, which is the tell: the
   ceiling is in the INK, not the backdrop. #7C766A against #0B0A08 tops out
   at 4.39:1 on a perfectly black ground — it cannot reach 4.5 anywhere on
   this page, so no amount of darkening behind it would ever have worked.

   #827C70 is the answer: measurably dimmer than the --ink-mute tier this
   started from (#8A8474), and still clearing AA at ~4.7:1 over the wash.
   That is as far back as this credit can go on this ground. Anything dimmer
   is not restraint, it is unreadable, and the audit will say so. */
.credit {
  --credit-ink: #827C70;
}

/* The wash that used to sit under the floating credit is GONE, not disabled.
   It existed only to fake a floor over the footage, and a solid ground does
   that properly. Nothing here should be reinstated: if the credit ever floats
   again it will need the whole problem re-solved, not this gradient back. */

/* ONE LINE, at every width. It wrapped to two rows on a phone and the stacked
   pair read as a broken footer rather than a credit — and the wrapped row
   opened with a stranded separator dot.

   So it scales instead of wrapping. Both the size AND the tracking are
   clamped against the viewport: tracking is the bigger lever here, because at
   0.13em it adds roughly a third again to the width of a 58-character line in
   a condensed face. nowrap makes any future overflow a loud failure in
   audit.mjs rather than a silent second row. */
.credit__line {
  display: flex;
  align-items: baseline;
  gap: 0.4em;
  max-width: none;
  white-space: nowrap;
  font-family: var(--font-cond);
  font-size: clamp(0.5rem, 2.4vw, 0.75rem);
  font-weight: 400;
  letter-spacing: clamp(0.03em, 0.3vw, 0.13em);
  text-transform: uppercase;
  color: var(--credit-ink);
}

/* Monochrome, per direction. U+2665 + U+FE0E in the markup renders as a text
   glyph rather than the colour emoji, so it inherits --credit-ink like every
   other character in the line. font-variant-emoji is belt-and-braces for
   browsers that would still reach for the emoji font. */
.credit__heart {
  font-variant-emoji: text;
  font-size: 0.9em;
  line-height: 1;
}

.credit__link {
  color: var(--credit-ink);
  border-bottom: 1px solid currentColor;
  padding-bottom: 1px;
  pointer-events: auto;
  transition: color 0.25s var(--ease-out), border-color 0.25s var(--ease-out);
}

/* The gold arrives on hover only. At rest the credit stays out of the way;
   reach for it and it becomes the brand's own colour. */
.credit__link:hover { color: var(--gold); border-bottom-color: var(--gold); }

.credit__link:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 3px;
  color: var(--gold);
}

/* Back inline on one row. It was broken onto its own row purely to keep the
   line short enough to stay inside a dark corner of the photograph. On brand
   ground that constraint is gone, so the credit reads as one tag again. */
.credit__sub {
  color: var(--credit-ink);
  opacity: 0.8;
}

.credit__sub::before {
  content: '·';
  margin-right: 0.5em;
  opacity: 0.6;
}

/* ── contact ──
   Written and verified now so that dropping a real address in later is a
   one-line change and not a design pass. The markup it styles is commented
   out in index.html — see the note there. */
.holding__contact {
  /* No top margin: this now sits in the brand bar, vertically centred against
     the lockup, not stacked under a lede. */
  margin-top: 0;
  font-family: var(--font-cond);
  font-size: 1.0625rem;
  /* Lowercase, deliberately. An email address is a literal string a person may
     retype, not a label — uppercasing it reads as UI chrome and makes the
     domain harder to scan. Every other line of Barlow Condensed on this page is
     uppercase, so this is the one exception and it is an intentional one. */
  letter-spacing: 0.04em;
  text-transform: none;
}

.holding__link {
  color: var(--gold);
  text-decoration: none;
  border-bottom: 1px solid rgba(var(--gold-rgb), 0.4);
  padding-bottom: 2px;
  transition: border-color 0.25s var(--ease-out);
}

.holding__link:hover { border-bottom-color: var(--gold); }

/* Keyboard focus must stay visible and must not be replaced by the hover
   treatment — this link is the only focusable element on the page. */
.holding__link:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 4px;
  border-radius: 1px;
}


/* ── THE BOOT ─────────────────────────────────────────────────────────────
   The 8B mark, a sweep of light across it, then it clears.

   Why the mark and not a spinner: a spinner says "wait". The monogram says
   whose page this is, and it is the only asset on the domain that can do that
   in under a second. The sweep is the surprise-and-delight beat — a specular
   highlight travelling across the mark the way low sun travels across steel,
   which is the same light the photograph underneath is lit by.

   Nothing here waits on the network. It clears at 1.0s regardless, because
   this brand's rule is that a failed asset must never withhold the statement.
   ─────────────────────────────────────────────────────────────────────── */
.boot {
  position: fixed;
  inset: 0;
  z-index: 10;
  display: grid;
  place-items: center;
  background: var(--ground);
  pointer-events: none;
  animation: boot-out 0.7s var(--ease-out) 1.0s both;
}

/* visibility, not just opacity: at opacity 0 the layer would still sit over
   the page, and a fixed transparent sheet swallows text selection. */
@keyframes boot-out {
  from { opacity: 1; visibility: visible; }
  98%  { opacity: 0; }
  to   { opacity: 0; visibility: hidden; }
}

.boot__lock {
  position: relative;
  /* Small and confident. It was clamp(64px, 10vw, 112px) and read as oversized
     — a large centred monogram starts to feel like an app launch screen rather
     than a contractor's site. */
  width: clamp(40px, 5vw, 64px);
  aspect-ratio: 1;
  animation: boot-mark 0.8s var(--ease-out) both;
}

/* Settles down to true size rather than growing. The mark arriving slightly
   oversize and easing in reads as something being set in place — up would
   read as a UI element popping. */
@keyframes boot-mark {
  from { opacity: 0; transform: scale(1.06); }
  to   { opacity: 1; transform: scale(1); }
}

.boot__mark { width: 100%; height: auto; }

/* The sweep. A hard band of near-white gold, masked to the MARK'S OWN SHAPE by
   reusing the PNG as a mask, so the light travels through the monogram's
   counters instead of across a rectangle. That masking is the whole trick —
   without it this is a shine on a box. */
.boot__sheen {
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg,
    transparent 42%,
    rgba(255, 244, 214, 0.92) 50%,
    transparent 58%);
  background-size: 260% 100%;
  background-repeat: no-repeat;
  -webkit-mask: url(/assets/brand/mark-gold.png) center / contain no-repeat;
          mask: url(/assets/brand/mark-gold.png) center / contain no-repeat;
  animation: boot-sheen 0.95s ease-in-out 0.3s both;
}

@keyframes boot-sheen {
  from { background-position: 190% 0; }
  to   { background-position: -90% 0; }
}

/* ── THE ENTRANCE ─────────────────────────────────────────────────────────
   What a "loader" is on this page. There is no spinner and nothing waits on
   the network, deliberately:

     · The page paints its brand ground on the first frame from the <html>
       background, so there is never a white flash to cover.
     · The one heavy asset is 695K of footage, and its poster frame stands in
       until it decodes. A loading gate would hold back a page that is
       otherwise instant.
     · This brand has a standing rule from the production site: hero copy
       animates on a CSS TIMER, never on the video's `playing` event. A slow,
       blocked or failed video must never withhold the brand statement.

   So the polish is an orchestrated reveal rather than a wait. Pure CSS, no
   JavaScript, nothing to fail: the photograph settles, the seam is struck,
   then the message arrives a beat later. It runs identically whether the
   video loads in 40ms or never loads at all.

   Order is meaning: ground, then brand, then the line that is struck across
   it, then what the company has to say. */
@keyframes eb-rise {
  from { opacity: 0; transform: translateY(26px); }
  to   { opacity: 1; transform: none; }
}

@keyframes eb-settle {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* The photograph settles rather than simply appearing: it starts 6% oversize
   and eases down to true scale while fading up. That slow contraction is what
   makes the open feel like footage arriving instead of an image being swapped
   in. Scaling DOWN, never up — up would reveal the frame edges. */
@keyframes eb-arrive {
  from { opacity: 0; transform: scale(1.06); }
  to   { opacity: 1; transform: scale(1); }
}

/* The first pass ran 14px over 0.8s and finished inside 2s. Verified live and
   effectively invisible — it read as the page simply having loaded. These are
   roughly double: 26px of travel, longer holds, and a wider stagger so each
   element clearly lands on its own beat. The whole sequence resolves by 3.1s,
   which is felt without being a wait. */
.holding__media   { animation: eb-arrive 2.4s var(--ease-out) both; }
.brandbar__lockup { animation: eb-rise 1.0s var(--ease-out) 1.05s both; }
.holding__eyebrow { animation: eb-rise 1.0s var(--ease-out) 1.30s both; }
.holding__title   { animation: eb-rise 1.15s var(--ease-out) 1.50s both; }
.holding__lede    { animation: eb-rise 1.0s var(--ease-out) 1.85s both; }
.cred             { animation: eb-rise 1.0s var(--ease-out) 2.05s both; }
.credit           { animation: eb-settle 1.2s var(--ease-out) 2.40s both; }


/* ── 404 ──────────────────────────────────────────────────────────────────
   The 404 page reuses every component on the holding page rather than being a
   second design. Only two things differ.

   1. It shows the poster still instead of the video. .holding__still is hidden
      by default and revealed only under reduced motion, so the 404 opts in
      explicitly with its own modifier.
   2. Its contact slot holds a navigation link rather than an address, so it
      needs the top margin back that the brand-bar version dropped. */
.holding__media--still .holding__still { display: block; }
.holding__media--still .holding__video { display: none; }

.holding__contact--404 { margin-top: clamp(1.25rem, 3vh, 2rem); }

/* The entrance is COMPRESSED here, and this is a real fix rather than a tidy-up.
   The delays on the holding page are timed to land as the boot clears at 1.0s.
   The 404 has no boot, so those same delays left the headline and the lede
   invisible for a second and a half behind nothing at all — a dead end that
   makes you wait before telling you it is a dead end.

   Everything still fades rather than snapping, but it is all in place by 0.5s. */
.page-404 .holding__media,
.page-404 .brandbar__lockup,
.page-404 .holding__eyebrow,
.page-404 .holding__title,
.page-404 .holding__lede,
.page-404 .holding__contact,
.page-404 .credit {
  animation-duration: 0.5s;
  animation-delay: 0s;
}

.page-404 .brandbar__seam { animation-duration: 0.8s; animation-delay: 0.05s; }

/* ── MOBILE / NARROW ──────────────────────────────────────────────────────
   The bar stacks rather than shrinking the lockup. Squeezing the brand mark
   to hold one row is the wrong trade on a page whose entire job is
   presenting the brand. */
/* The bar holds two short items now, the lockup and an email address, so it
   stays a single row far longer than it did when it carried the four
   classifications. It only stacks on genuinely narrow screens. */
@media (max-width: 30rem) {
  .brandbar__inner {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.75rem;
  }
}

/* TWO COLUMNS on a phone, per direction — four stacked rows made the bar
   almost a quarter of the screen.

   The trade, stated plainly: "Building Moving & Demolition" is 28 characters
   and, with its plate, cannot fit half of a 390px screen at a readable size.
   Shrinking the type until it did would put it near 9px. So nowrap is lifted
   here and that one label takes two lines. Three lines of height instead of
   four, and the type stays legible.

   minmax(0, 1fr) rather than auto: `auto` columns size to their widest item,
   which lets the long label push the grid past the viewport instead of
   wrapping inside its own cell. */
@media (max-width: 34rem) {
  .cred__classes {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.5rem 0.75rem;
  }
  .cred__classes li {
    align-items: flex-start;
    white-space: normal;
    font-size: 0.75rem;
    letter-spacing: 0.04em;
    line-height: 1.25;
  }
  .cred__code { min-width: 2.75rem; }
}

/* Very narrow: the classification rows are the only thing on the page that
   can still outrun the viewport, because they are nowrap by design (breaking
   "General Engineering" across two lines reads as a layout fault, not as
   copy). Below this width the tracking and size step down instead. */
@media (max-width: 22.5rem) {
  .cred__classes li { font-size: 0.75rem; letter-spacing: 0.04em; }
  .cred__code { min-width: 2.25rem; font-size: 0.6875rem; }
}

/* ── DESKTOP ──────────────────────────────────────────────────────────────
   Copy moves left, so the wash follows it — the production hero's desktop
   scrim, reused verbatim. */
@media (min-width: 48rem) {
  .holding__scrim {
    background: linear-gradient(
      to right,
      rgba(11, 10, 8, 0.62) 0%,
      rgba(11, 10, 8, 0.38) 45%,
      rgba(11, 10, 8, 0.06) 100%
    );
  }
}

/* ── LANDSCAPE PHONES ─────────────────────────────────────────────────────
   A 390x844 phone turned sideways leaves ~390px of height for a bar that
   wants ~150px of it. The bar returns to one row and the copy tightens, so
   the photograph keeps a usable share of a very short viewport. */
@media (max-height: 30rem) and (orientation: landscape) {
  .brandbar { padding-block: 0.625rem; }
  .brandbar__inner { flex-direction: row; align-items: center; gap: 1.25rem; }
  .brandbar__lockup { height: 1.875rem; }
  /* The classifications live in the copy now, so on a very short viewport they
     compete with the headline rather than with the bar. Tighten, don't hide —
     they are the only credential on the page. */
  .cred { margin-top: 0.875rem; padding-top: 0.75rem; }
  .cred__classes { gap: 0.2rem 1rem; }
  .cred__classes li { font-size: 0.6875rem; letter-spacing: 0.03em; }
  .cred__code { min-width: 2rem; font-size: 0.625rem; }
  .holding__inner { padding-block: 1.25rem; }
  .holding__eyebrow { margin-bottom: 0.5rem; }
  .holding__lede { margin-top: 0.625rem; }

  /* This viewport is wide enough to take the desktop horizontal scrim but
     only ~335px tall, so the copy compresses upward into a brighter part of
     the frame and the gold eyebrow measured 4.43:1 — just under the 4.5 it
     needs. The wash holds longer across the left before falling off.
     Verified at 5.9:1 on the worst frame. */
  .holding__scrim {
    background: linear-gradient(
      to right,
      rgba(11, 10, 8, 0.70) 0%,
      rgba(11, 10, 8, 0.46) 45%,
      rgba(11, 10, 8, 0.08) 100%
    );
  }
}

/* ── reduced motion ──
   Swaps the looping footage for its own poster frame and settles the seam
   already drawn. Nothing is withheld: the still IS the video's poster and
   the seam's end state is its resting state. */
@media (prefers-reduced-motion: reduce) {
  .boot { display: none; }
  .holding__video { display: none; }
  .holding__still { display: block; }

  /* Every element of the entrance resolves to its end state immediately.
     `animation: none` alone would be a bug here: these rules use
     fill-mode `both`, so removing the animation without restoring opacity
     leaves the copy at the keyframe's opacity:0 and the page renders blank. */
  .brandbar__seam,
  .holding__media,
  .brandbar__lockup,
  .holding__eyebrow,
  .holding__title,
  .holding__lede,
  .cred,
  .credit {
    animation: none;
    opacity: 1;
    transform: none;
  }

  .holding__link,
  .credit__link { transition: none; }
}
