/* Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5
 * Hallmark · genre: editorial · macrostructure: Photographic · theme: custom
 * (vibe: "work-truck signage over real photography" · paper oklch(97.2% 0.005 85)
 *  · accent oklch(67.9% 0.173 51.4) warm · Barlow Condensed + Newsreader)
 * axes — paper band: light · display style: display-condensed · accent hue: warm
 * nav: N9 edge-aligned · footer: Ft2 inline · enrichment: none — photography is the design
 *
 * Studied: barrettmade.com · housebrand.ca · wealdconstruction.com — all three are
 * photography-led with NO bordered cards, NO shadows, NO radius. This build follows
 * that: the image edge is the divider, type annotates, orange appears on the CTA
 * and the call bar only.
 */

/* ── base ─────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }

html {
  overflow-x: clip;
  scroll-behavior: smooth;
  scroll-padding-top: var(--nav-h);
  -webkit-text-size-adjust: 100%;
}

body {
  --nav-h: 62px;
  --callbar-h: 56px;
  margin: 0;
  padding-bottom: var(--callbar-h);
  overflow-x: clip;
  background: var(--color-paper);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-optical-sizing: auto;
  line-height: 1.55;
}
@media (min-width: 900px) { body { --nav-h: 74px; --callbar-h: 0px; } }

img { max-width: 100%; height: auto; display: block; }
ul, ol { margin: 0; padding: 0; list-style: none; }
a { color: inherit; }
button { font: inherit; color: inherit; }
h1, h2, h3 { margin: 0; font-weight: 500; }
/* the two that stay in caps */
.mark__text, .index__n { text-transform: uppercase; letter-spacing: 0.015em; }
p { margin: 0 0 1em; }
p:last-child { margin-bottom: 0; }
figure { margin: 0; }

/* Sentence case, weight 500, looser tracking, more leading. Two typefaces
   were rejected before the diagnosis landed: it was never the face, it was
   setting every heading in tight near-black caps at the largest size the
   column allowed. The small-caps LABEL device is untouched — that is what
   carries the drawing-sheet language, and it only works while the headings
   around it are not also shouting. */
h1, h2, h3, .mark__text, .index__n, .cta, .contact__tel, .nav__tel, .callbar a {
  font-family: var(--font-display);
  letter-spacing: var(--track-display);
  line-height: 1.06;
  /* break-word, NOT anywhere: `anywhere` breaks wherever it helps the line fit,
     which shattered the hero into BUIL / T / RIGH / T. It only ever kicks in
     for a single word genuinely wider than its container. */
  overflow-wrap: break-word;
  min-width: 0;
}

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.skip-link {
  position: absolute; top: -100px; left: var(--gutter); z-index: 300;
  background: var(--color-accent); color: var(--color-accent-ink);
  padding: var(--space-xs) var(--space-md);
  font-family: var(--font-ui); font-weight: 600; text-decoration: none;
}
.skip-link:focus { top: var(--space-xs); }

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

.icon-phone {
  display: inline-block; width: 0.92em; height: 0.92em; flex: none;
  background: currentColor;
  -webkit-mask: var(--phone) center / contain no-repeat;
          mask: var(--phone) center / contain no-repeat;
}

/* small caps label — the one repeated typographic device on the page */
.label {
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 600;
  letter-spacing: var(--track-label); text-transform: uppercase;
}

/* ── nav ──────────────────────────────────────────────────────────── */
.nav {
  position: fixed; top: 0; left: 0; right: 0; z-index: 100;
  display: flex; align-items: center; gap: var(--space-md);
  height: var(--nav-h);
  padding-inline: var(--gutter);
  color: var(--color-ink-light);
  transition: background-color var(--dur-mid) var(--ease-out),
              color var(--dur-mid) var(--ease-out);
}
.nav.is-solid {
  background: var(--color-paper);
  color: var(--color-ink);
  border-bottom: var(--rule-hair) solid var(--color-rule);
}

.mark { display: inline-flex; align-items: center; gap: var(--space-2xs); text-decoration: none; margin-right: auto; }
.mark__cut {
  width: 26px; height: 5px; flex: none; background: currentColor;
  clip-path: polygon(5px 0, 100% 0, calc(100% - 5px) 100%, 0 100%);
}
.mark__text { font-size: clamp(1.0625rem, 3.6vw, 1.3125rem); font-weight: 700; letter-spacing: 0.01em; }

.nav__links { display: none; }
.nav__links a {
  font-family: var(--font-ui); font-size: var(--text-sm); font-weight: 500;
  letter-spacing: 0.04em; text-transform: uppercase; text-decoration: none;
  white-space: nowrap; opacity: 0.82;
  transition: opacity var(--dur-short) var(--ease-out);
}
.nav__links a:hover { opacity: 1; }

.nav__end { display: flex; align-items: center; gap: var(--space-sm); }
.nav__tel { display: none; font-size: var(--text-md); font-weight: 700; text-decoration: none; white-space: nowrap; }
.nav__tel:hover { color: var(--color-accent-hot); }

.burger {
  width: 40px; height: 40px; display: grid; place-items: center;
  background: none; border: 0; cursor: pointer;
}
.burger__box { display: block; width: 22px; }
.burger__box i {
  display: block; height: 2px; background: currentColor; margin: 5px 0;
  transition: transform var(--dur-short) var(--ease-out);
}
.burger[aria-expanded="true"] i:nth-child(1) { transform: translateY(3.5px) rotate(45deg); }
.burger[aria-expanded="true"] i:nth-child(2) { transform: translateY(-3.5px) rotate(-45deg); }

@media (max-width: 899px) {
  .nav__links.is-open {
    display: flex; flex-direction: column; gap: var(--space-xs);
    position: absolute; top: var(--nav-h); left: 0; right: 0;
    background: var(--color-paper); color: var(--color-ink);
    border-bottom: var(--rule-hair) solid var(--color-rule);
    padding: var(--space-md) var(--gutter) var(--space-lg);
  }
  .nav__links.is-open a { font-size: var(--text-md); opacity: 1; }
}
@media (min-width: 900px) {
  .nav__links { display: flex; align-items: center; gap: clamp(1.25rem, 2.4vw, 2.25rem); }
  .nav__tel { display: inline; }
  .burger { display: none; }
}

/* ── FOLD 1 · the film ────────────────────────────────────────────── */
.film {
  position: relative;
  min-height: 100svh;
  display: flex; align-items: flex-end;
  padding: var(--space-4xl) var(--gutter) var(--space-2xl);
  background: var(--color-navy-deep);
  overflow: hidden;
  isolation: isolate;
}
.film__media {
  position: absolute; inset: 0; z-index: -2;
  width: 100%; height: 100%; object-fit: cover;
  /* The film is decoration behind the type — it must never take a click from
     the phone-number CTA, and if the file is missing or errors, any native
     media chrome the browser draws stays non-interactive. */
  pointer-events: none;
}
/* Bottom-weighted scrim: dark where the type sits, clear where the film plays.
   The top band is NOT decorative — the nav is white text sitting directly on
   the video, and a bright frame would otherwise wipe it out. It has to hold
   regardless of what Joe shot. */
.film__scrim {
  position: absolute; inset: 0; z-index: -1;
  background:
    /* Corner wash. The bottom band alone was tuned against a dark photo and
       failed the moment a pale one went in: white type over fresh pine measured
       2.8:1, and 1.6:1 across the brightest fifth of the headline box. This
       layer is diagonal so the weight lands on the bottom-left CORNER where the
       type actually is, and clears by the middle — the deck stays bright on the
       right, which is the whole reason for choosing the photo. */
    linear-gradient(to top right, oklch(18% 0.04 262 / 0.90) 0%,
                                  oklch(18% 0.04 262 / 0.64) 26%,
                                  oklch(18% 0.04 262 / 0.24) 44%,
                                  oklch(18% 0.04 262 / 0.00) 62%),
    linear-gradient(to top, oklch(18% 0.04 262 / 0.92) 0%,
                            oklch(18% 0.04 262 / 0.62) 26%,
                            oklch(18% 0.04 262 / 0.00) 52%,
                            oklch(18% 0.04 262 / 0.00) 66%,
                            oklch(18% 0.04 262 / 0.74) 100%);
}
/* Sized in rem, not ch: `ch` here resolves against the 17px body font, so 20ch
   was ~200px — nowhere near enough for 120px display type. */
.film__type { position: relative; max-width: min(100%, 46rem); }
.film h1 {
  font-size: var(--text-display);
  color: var(--color-ink-light);
  margin-bottom: var(--space-md);
}
.film__line {
  font-size: var(--text-md); color: var(--color-ink-light);
  opacity: 0.9; max-width: 34ch; margin-bottom: var(--space-lg);
}
.film__caption {
  position: absolute; right: var(--gutter); bottom: var(--space-2xl);
  margin: 0; display: none;
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 500;
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--color-ink-light); opacity: 0.72;
}
/* "Licensed & insured" is a trust signal and the city is a local-search signal;
   both were desktop-only. On the phone the film becomes a column so the caption
   can sit under the CTA in flow instead of being absolutely positioned into the
   fixed call bar. Bottom padding grows by the call bar's height to keep it clear. */
@media (max-width: 899px) {
  .film { flex-direction: column; align-items: flex-start; justify-content: flex-end;
          padding-bottom: calc(var(--space-2xl) + var(--callbar-h)); }
  .film__caption {
    display: block; position: static;
    margin-top: var(--space-sm);
    letter-spacing: 0.1em;   /* 0.16em overruns 390px by a few pixels */
    opacity: 0.8;
  }
  .film__caption span { white-space: nowrap; }
}

@media (min-width: 900px) {
  .film__caption { display: block; }
  /* Joe's photo overhangs up into the bottom-left of the film. Reserve the same
     height here so the h1 and the phone CTA clear it. */
  .film { padding-bottom: calc(var(--space-2xl) + var(--meet-break)); }
}

/* the single button on the page */
.cta {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2xs);
  padding: var(--space-sm) var(--space-lg);
  /* The LIT blue, not the deep one. This button only ever appears on the film,
     and the deep blue against a dark video is a button you have to hunt for —
     the shape disappeared into the scrim and only the white type survived.
     It is the primary conversion element on the page; it has to be the
     brightest thing in the frame after the headline. */
  background: var(--color-accent-hot); color: var(--color-navy-deep);
  border: 0; border-radius: var(--radius);
  font-size: var(--text-md); font-weight: 700; letter-spacing: 0.04em;
  text-decoration: none; white-space: nowrap; cursor: pointer;
  transition: background-color var(--dur-short) var(--ease-out),
              color var(--dur-short) var(--ease-out);
}
.cta:hover { background: var(--color-accent); color: var(--color-ink-light); }
.cta:active { transform: translateY(1px); }
.cta--block { width: 100%; }
.cta[disabled] { opacity: 0.5; pointer-events: none; }

/* Ink variant — navy field, accent arrow. Used once, on the drawing sheet.
   The orange fill is the PHONE's colour everywhere else on this page (hero
   CTA, call bar, wordmark cut); giving the form the same fill made the two
   read as equal offers when the phone is the one Joe actually wants rung. */
.cta--ink {
  background: var(--color-navy-deep); color: var(--color-ink-light);
  justify-content: space-between; gap: var(--space-md);
  padding-inline: var(--space-md);
}
.cta--ink:hover { background: var(--color-navy); }
.cta__arrow {
  color: var(--color-accent-hot);
  transition: transform var(--dur-short) var(--ease-out);
}
.cta--ink:hover .cta__arrow { transform: translateX(4px); }

/* ── MEET JOE — the introduction ──────────────────────────────────────
   Replaces a text-only "statement" band that had no face, no colour and no
   anchor in it. A photographic hero followed by a wall of type reads as a
   drop in energy; a person reads as warmth. */
.meet {
  position: relative;
  /* Lifts the whole block over the trades panel below it. Both are positioned
     with z-index auto, so without this the later section in the document wins
     the paint order and swallows the bottom of Joe's overhang. */
  z-index: 1;
  display: grid;
  background: var(--color-navy);
  color: var(--color-ink-light-2);
}
/* Flat navy was the one surface on the page with nothing in it. This is a
   photographic oak grain (ambientCG Wood049, CC0 — see img/README.md) recoloured
   so its average is exactly --color-navy: the panel keeps its brand colour and
   only gains figure. It is a material, not an effect, which is the whole point —
   a drawn grid reads as CSS, sawn timber reads as the trade.
   The source tiles seamlessly, so this repeats rather than stretching; measured
   edge-to-edge delta is ~0.6%, below the noise floor of the grain itself. */
.meet::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  /* Native size of the file. Scaling up would soften the grain to mush, and
     scaling down would shrink it to noise; at 1:1 it reads as one wide board. */
  background: url("img/navy-grain.jpg") repeat center / 1400px auto;
}
@media (max-width: 899px) {
  /* A 1400px board across a 390px screen is just a gradient. Half-scale puts a
     believable amount of figure back in the frame. */
  .meet::before { background-size: 760px auto; }
}
@media (min-width: 900px) {
  /* Narrowed from 0.9fr, which is a reversal. 0.9fr let the landscape frame
     arrive nearly intact — and intact is the problem: Joe stands in the left
     third of his own photograph and the other two thirds are trusses, so an
     uncropped frame makes the man a detail in a picture of a roof. A narrower
     column forces `cover` to take the crop off the RIGHT (see object-position
     below), which spends the roof to buy Joe. The earlier note here warned a
     narrow column slices his head off; that was the vertical anchor's fault,
     and it is fixed at the img rather than by widening the column back. */
  .meet { grid-template-columns: minmax(0, 0.62fr) minmax(0, 1.38fr); align-items: stretch; }
}
.meet__photo { position: relative; z-index: 2; overflow: hidden; }
/* Joe travels in from the left like every other photograph on the page. */
.js .meet__photo { opacity: 0; transform: translateX(-64px); }
.js .meet__photo.in {
  opacity: 1; transform: none;
  transition: opacity 1s var(--ease-out), transform 1.2s var(--ease-out);
}
@media (prefers-reduced-motion: reduce) {
  .js .meet__photo { opacity: 1; transform: none; }
}
/* The picture is TALLER than the band it stands on and breaks out of it at
   both ends — up into the film, down into the trades panel. That is what keeps
   the grain reading as a stripe behind Joe rather than a field around him, and
   it makes him the tallest thing in the block without the section growing to
   match. The band's own height is set by the text column alone. */
@media (min-width: 900px) {
  .meet__photo { margin-block: calc(-1 * var(--meet-break)); }
}
.meet__photo img {
  width: 100%; height: 100%; object-fit: cover;
  /* The crop now lives in the FILE, not here. img/joe.jpg was cut down from
     799x599 to 460x599 — the camera original has Joe's far shoulder outside
     the frame, so no amount of repositioning could ever show all of him, and
     the only thing that makes that edge read as deliberate is framing him as a
     portrait. img/joe-uncropped.jpg is the original, kept for re-cropping.

     What is left here is a safety net: the frame is usually WIDER than the
     file's 0.77, so cover crops vertically and the X value does nothing — but
     it stays at 0 for the narrow-frame case, where it would matter. 8% keeps
     his crown clear of the top edge. */
  object-position: 0% 8%;
  /* 4:5 on phones — close to the file's own shape, so almost nothing is cut
     and the portrait arrives whole on the screen where Joe matters most. */
  aspect-ratio: 4 / 5; background: var(--color-navy-deep);
}
/* Back to filling its own box, which is now the band plus both overhangs — a
   fixed aspect-ratio here would fight the negative margins for the height. */
@media (min-width: 900px) { .meet__photo img { aspect-ratio: auto; min-height: 100%; } }

.meet__text {
  position: relative; z-index: 1;
  display: flex; flex-direction: column; justify-content: center;
  padding: var(--space-xl) var(--gutter);
}
/* 2.5rem, not --space-2xl. The grain band has no height of its own — it is
   exactly as tall as this column — so taking height out of the section means
   taking it out of this padding and the gaps below. Type sizes are untouched;
   only the air between the lines is. */
@media (min-width: 900px) { .meet__text { padding: 2.5rem clamp(2rem, 5vw, 4rem); } }
.meet__text h2 {
  font-size: var(--text-xl); color: var(--color-ink-light);
  margin-bottom: var(--space-xs); max-width: 20ch;
}
.meet__text p { max-width: 52ch; }
/* One paragraph now — the supporting `.meet__body` line was cut 2026-08-09, and
   its rule went with it rather than sitting here unused. The lead is the whole
   voice of this panel, so it keeps the step up in size. */
/* Written as `.meet__text .meet__lead`, not `.meet__lead`, on purpose: the
   `.meet__text p` rule above is a class+type selector and outranked the bare
   class, so the 26ch this used to declare never applied and the lead has been
   running at 52ch. Narrowing the photo column widens this one, so the measure
   now has to be held deliberately — 36ch keeps Joe's line reading as speech
   rather than as a paragraph stretched across the panel. */
.meet__text .meet__lead {
  font-size: var(--text-lg); line-height: 1.34;
  color: var(--color-ink-light); max-width: 36ch;
  margin-bottom: var(--space-xs);
}
.meet__sign {
  margin-top: var(--space-2xs); margin-bottom: 0;
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 600;
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--color-ink-light);
}

/* Facts as one spec line. Four rows in a 2x2 with accent numerals gave four
   unremarkable claims the visual weight of the headline; this states them once
   and gets out of the way, which is all they were ever worth. */
.meet__spec {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--space-2xs) var(--space-sm);
  margin-top: var(--space-md); padding-top: var(--space-xs);
  border-top: var(--rule-hair) solid var(--color-rule-light);
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 600;
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--color-ink-light-2);
}
.meet__spec li { display: flex; align-items: baseline; gap: var(--space-sm); }
/* The tick trails its item rather than leading the next one. The row wraps at
   middling widths, and a divider stranded at the START of a line reads as a
   mistake — at the end of one it reads as "continues". This is also the only
   job the orange has left in this section now the numerals are gone. */
.meet__spec li:not(:last-child)::after {
  content: ""; width: var(--rule-hair); height: 0.7em;
  background: var(--color-accent-hot); opacity: 0.9;
}
/* One column: the facts stack and the ticks come off entirely. */
@media (max-width: 560px) {
  .meet__spec { display: block; }
  .meet__spec li + li { margin-top: var(--space-3xs); }
  .meet__spec li::after { display: none; }
}

/* ── TRADES — one typographic block ───────────────────────────────────
   Third attempt. Two columns of small rows read as a spec table; eight big
   rows read as a wall. Eight items presented flatly is the problem, not the
   styling — so they stop being a list and become one graphic: the trades set
   as a single field of display type, with the description revealed on
   approach rather than printed eight times.

   The premium version of this section is a list paired with an image that
   swaps per trade. That needs eight photos and Joe has four, so this does the
   same job with type alone. */
.index { padding: var(--space-2xl) var(--gutter) var(--space-2xl); }
.index__h {
  font-size: var(--text-xs); font-weight: 600; letter-spacing: var(--track-label);
  font-family: var(--font-ui); color: var(--color-ink-2);
  padding-bottom: var(--space-sm);
  border-bottom: var(--rule-hair) solid var(--color-rule);
}

.wall { position: relative; padding-top: var(--space-lg); }

/* ── the shifting field ────────────────────────────────────────────────
   Each trade carries a photo of that work. It fades in behind the type on
   hover, and cycles on its own every 5s so the section is alive before you
   touch it. Desktop only — the images are injected by JS, so a phone never
   downloads 570KB of decoration. */
/* ── TRADES — words on the photograph ─────────────────────────────────
   The words sit ON the picture and highlight as it changes.

   The reason an earlier pass buried the photo: a FLAT 80% scrim across the
   whole panel. Contrast is only needed where the type is, so the scrim is now
   weighted — heavy across the top third behind the words and the caption, then
   clearing to 16% so the lower half of the photograph is genuinely visible.
   The section is also taller, so there is real picture to see below the type. */
.index { position: relative; padding: var(--space-2xl) var(--gutter); }
/* Joe's photo overhangs DOWN into the top-left of this panel. Same reserve as
   the film uses above it, so the trades heading never sits under him. */
@media (min-width: 900px) {
  .index { padding-top: calc(var(--space-2xl) + var(--meet-break)); }
}

.picker__bg { display: none; }
.wall { position: relative; }
.wall__list { display: block; }
.wall__list li { display: inline; }
.wall__list a { text-decoration: none; }
.picker__desc-src { display: none; }

.wall__name {
  font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  font-size: clamp(1.375rem, 0.3rem + 2vw, 2.25rem);
  letter-spacing: -0.005em;
  line-height: 1.2;
  color: var(--color-ink);
  white-space: nowrap;          /* never orphan a separator onto a new line */
  transition: color var(--dur-short) var(--ease-out), opacity var(--dur-short) var(--ease-out);
}
/* The orange dashes that used to sit between names are gone. In a grid they
   land at the END of a row with nothing on the other side, which stops reading
   as punctuation and starts reading as damage. The tick added at 900+ replaces
   them and does a job the dashes never did: it says which trade is showing. */

.picker__desc {
  margin: var(--space-lg) 0 0; max-width: 46ch;
  font-size: var(--text-md); line-height: 1.45; color: var(--color-ink-2);
  min-height: 3em;              /* a fixed slot — the block must not resize as it cycles */
}
.wall__prog { display: none; }

/* ── the right-hand rail ──────────────────────────────────────────────
   NOT a card. A filled, bordered box floating on a photograph is the single
   most template-looking object you can put on a page — it reads as a widget
   someone dropped on top rather than part of the design. So this has no
   background, no border box and no padding: a hairline above it (the same
   hairline the titleblock uses) and then type sitting directly on the picture,
   carried by the same text-shadow as the trade names. */
.wall__aside {
  border-top: var(--rule-hair) solid var(--color-rule);
  padding-top: var(--space-sm);
  /* Stacked (under 1120) it lands below the countdown bar, and two hairlines
     40px apart read as one indecisive divider. Push it clear. */
  margin-top: var(--space-2xl);
  max-width: 30rem;
}
.wall__aside__label {
  margin: 0; font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 600;
  letter-spacing: var(--track-label); text-transform: uppercase; color: var(--color-ink-2);
}
.wall__aside__tel {
  display: inline-block; margin-top: var(--space-3xs);
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(1.5rem, 1rem + 1.15vw, 2rem); line-height: 1;
  letter-spacing: -0.02em; white-space: nowrap;
  color: var(--color-ink); text-decoration: none;
  transition: color var(--dur-short) var(--ease-out);
}
.wall__aside__tel:hover { color: var(--color-accent-hot); }
.wall__aside__p {
  margin: var(--space-2xs) 0 0; max-width: 30ch;
  font-family: var(--font-body); font-size: var(--text-sm); line-height: 1.5;
  color: var(--color-ink-2);
}

/* ── ONE alignment rule, two columns ──────────────────────────────────
   Fourth attempt, and the one that fixes the actual defect. The previous pass
   ran four columns and forced the last one flush right (`text-align: right` on
   :nth-child(4n)) to tidy the ragged edge. That traded a ragged edge for a
   broken one: FLOORING and GENERAL REPAIRS jammed into the gutter and read as
   words cut off by the window, while the gaps between the short names blew
   open. A field of display type gets ONE alignment rule. If the right edge is
   the problem, constrain the measure or change the column count — never flip
   the last column.

   So: two columns, both left-aligned, both inside a measure that stops well
   short of the gutter. The longest name (PRESSURE WASHING, ~295px at the 2.85rem
   cap) clears a 373px column at 900 and a 394px column at 1440, so the grid
   costs no type size. Nothing can touch an edge because nothing reaches one. */
@media (min-width: 900px) {
  .wall__list {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--space-2xl);
    row-gap: 0.3em;
  }
  .wall__list li { display: block; }

  /* Which trade is showing has to be legible at a glance. The old tell was 72%
     white going to 100% white on a busy photograph, which is not a state
     change — it made a working feature look like a rendering glitch. */
  .wall__list a { position: relative; display: block; padding-left: 0.86em; }
  .wall__list a::before {
    content: ""; position: absolute; left: 0; top: 0.5em;
    width: 0.44em; height: 0.11em; background: var(--color-accent-hot);
    transform: scaleX(0); transform-origin: left;
    transition: transform var(--dur-mid) var(--ease-out);
  }
  .wall__list .is-active a::before,
  .wall__list a:hover::before,
  .wall__list a:focus-visible::before { transform: scaleX(1); }
}

/* 1120+: the card comes up beside the list. It waits until here because below
   it the type column is too narrow for PRESSURE WASHING at two columns, and
   shrinking the names to fit a card is the wrong trade. */
@media (min-width: 1120px) {
  .wall {
    display: grid;
    grid-template-columns: minmax(0, 1.5fr) minmax(16rem, 0.5fr);
    column-gap: var(--space-2xl);
    align-items: start;
  }
  .wall__aside { margin-top: 0; }
}

/* Desktop shows each trade as a full-bleed photograph; the phone showed eight
   lines of text and nothing else, so the section's only PROOF was invisible to
   most of the traffic. These thumbnails put the work back without importing the
   desktop mechanic: no hover, no rotation, no type sitting on a picture at
   390px. Separate 240x180 files, not the backgrounds — all eight come to 72KB
   against ~570KB, so the "a phone never downloads the decoration" rule above
   still holds. At 4.75rem displayed they downscale ~2.9x, which is why the two
   799px sources (painting, pressure washing) look fine here and nowhere else. */
.wall__thumb { display: none; }

/* small screens keep the plain readable list — no photo, no hover */
@media (max-width: 899px) {
  .wall__list li { display: block; border-bottom: var(--rule-hair) solid var(--color-rule); }
  .wall__list a {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 4.75rem;
    column-gap: var(--space-sm);
    align-items: center;
    padding-block: var(--space-sm);
  }
  .wall__thumb {
    display: block;
    grid-column: 2; grid-row: 1 / span 2;
    width: 100%; height: auto; aspect-ratio: 4 / 3;
    object-fit: cover;
    background: var(--color-navy-deep);
  }
  .wall__name { display: block; grid-column: 1; grid-row: 1; }
  /* the nowrap upstairs exists to protect a separator that does not exist in
     this layout, and "Pressure Washing" has 4.75rem less room here */
  .wall__name { white-space: normal; }
  .picker__desc-src { grid-column: 1; grid-row: 2; }
  .wall__list li:not(:last-child) .wall__name::after { display: none; }
  .picker__desc { display: none; }
  .picker__desc-src {
    display: block; margin-top: var(--space-3xs);
    font-family: var(--font-body); font-size: var(--text-sm);
    text-transform: none; letter-spacing: 0; line-height: 1.5; color: var(--color-ink-2);
  }
}

@media (min-width: 900px) {
  .index.has-bg {
    background: var(--color-navy-deep);
    /* The photograph IS the content here — each trade is shown, not just named.
       So the panel stays tall on purpose: the area below the type is picture,
       not padding, and shortening it to "close a gap" throws away the only
       proof the section offers. If the lower half ever reads as empty, the
       scrim is too heavy — fix the scrim, not the height. */
    min-height: clamp(32rem, 80svh, 52rem);
    padding-bottom: var(--space-3xl);
    display: flex; flex-direction: column;
    overflow: hidden;
    isolation: isolate;
  }
  .picker__bg { display: block; position: absolute; inset: 0; z-index: -2; }
  /* Crossfade only. This used to run a 7s scale(1.06) -> scale(1) push-in
     behind the trade list, which is the same zoom tic pulled everywhere else
     and the worst place for it: the type sits ON the photograph, so a moving
     background drags the words with it. */
  .picker__bg img {
    position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: cover; opacity: 0;
    transition: opacity 900ms var(--ease-out);
  }
  .picker__bg img.on { opacity: 1; }

  /* weighted, not flat: dark only where the type actually sits */
  .index.has-bg::after {
    content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
    background: linear-gradient(
      oklch(18% 0.04 262 / 0.82) 0%,
      oklch(18% 0.04 262 / 0.74) 40%,
      oklch(18% 0.04 262 / 0.54) 56%,
      oklch(18% 0.04 262 / 0.16) 72%,
      oklch(18% 0.04 262 / 0.04) 100%);
  }

  /* Contrast that travels WITH the type instead of a wash over the picture.
     A shadow costs the photograph nothing; every extra point of scrim costs it
     everything. This is what buys the lighter gradient above. */
  .index.has-bg .wall__name,
  .index.has-bg .picker__desc,
  .index.has-bg .titleblock__no,
  .index.has-bg .titleblock__label,
  .index.has-bg .wall__aside__label,
  .index.has-bg .wall__aside__tel,
  .index.has-bg .wall__aside__p {
    text-shadow: 0 1px 2px oklch(14% 0.03 262 / 0.55),
                 0 2px 14px oklch(14% 0.03 262 / 0.45);
  }

  .index.has-bg .titleblock__label,
  .index.has-bg .titleblock__no { color: var(--color-ink-light); }
  .index.has-bg .titleblock { border-bottom-color: var(--color-rule-light); }
  .index.has-bg .titleblock::before { background: var(--color-rule-light); }
  .index.has-bg .wall__name { color: var(--color-on-photo); }   /* inactive */
  .index.has-bg .is-active .wall__name,
  .index.has-bg a:hover .wall__name,
  .index.has-bg a:focus-visible .wall__name { color: var(--color-ink-light); }
  .index.has-bg .picker__desc { color: var(--color-ink-light); }
  .index.has-bg .wall__aside { border-top-color: var(--color-rule-light); }
  .index.has-bg .wall__aside__tel { color: var(--color-ink-light); }
  .index.has-bg .wall__aside__tel:hover { color: var(--color-accent-hot); }
  .index.has-bg .wall__aside__label,
  .index.has-bg .wall__aside__p { color: var(--color-on-photo); }

  /* countdown, hairline across the foot of the type block */
  .index.has-bg .wall__prog {
    display: block; margin-top: var(--space-md);
    width: 180px;            /* an indicator, not a divider across the page */
    height: 2px; background: var(--color-rule-light);
  }
  .wall__prog i { display: block; height: 100%; background: var(--color-accent-hot);
    transform: scaleX(0); transform-origin: left; }
  .wall.is-running .wall__prog i { animation: fillbar 5s linear forwards; }
  .wall.is-held .wall__prog i { animation-play-state: paused; }
  @keyframes fillbar { from { transform: scaleX(0); } to { transform: scaleX(1); } }
}

@media (prefers-reduced-motion: reduce) {
  .picker__bg img { transition: opacity 1ms linear; }
  .wall__prog { display: none !important; }
}

/* ── MORE RECENT WORK · the thumbnail strip ───────────────────────────
   Deliberately subordinate to the two featured jobs above it: smaller plates,
   inset instead of full-bleed, quieter captions. Full-bleed here competed with
   the sliders and the eye couldn't tell which set of photos mattered.
   Caption is two lines — what it was, then the town. The town is the line a
   homeowner in Melbourne actually scans for. */
/* no bottom padding — .ba already closes the section, and stacking both left
   a 144px hole above the contact form */
/* Sub-head inside the same section, not a new one — 4xl read as a section
   break and left a band of empty paper between the two jobs and the strip.
   A flat 3rem rather than 2xl: 2xl is larger than 4xl resolves to on a phone,
   so it would have made the strip further away on the screen that can least
   afford it. */
.more { margin-top: var(--space-lg); }

/* A contact sheet, not four cards. Gaps, gutters and captions-underneath were
   what made this strip read as a stock template: four identical boxes on
   paper, each one small, each one framed by more empty page than photograph.
   Edge to edge with hairline splits, the four photos become one band of work
   and the paper stops competing with them. */
.plates {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin-top: var(--space-md);
}
@media (min-width: 700px) { .plates { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* The split is the paper colour, so the leftmost cell's border simply
   disappears against the page — no :first-child exception needed. */
.plate { position: relative; border-left: 2px solid var(--color-paper); }
.plate__btn {
  display: block; width: 100%; padding: 0; border: 0; cursor: pointer;
  background: var(--color-navy-deep); overflow: hidden;
}
/* Tall crops. A 4:3 thumbnail of a room is mostly floor and ceiling; a
   portrait crop at this width holds one subject and stacks into a band with
   real vertical presence. */
.plate__btn img { display: block; width: 100%; height: 100%; object-fit: cover; aspect-ratio: 4 / 5; }
@media (min-width: 700px) { .plate__btn img { aspect-ratio: 4 / 5; } }

/* Printed on the photograph, over a gradient — always visible, because a
   phone has no hover to reveal it. */
.plate figcaption {
  position: absolute; left: 0; right: 0; bottom: 0; pointer-events: none;
  padding: var(--space-xl) var(--space-sm) var(--space-sm);
  background: linear-gradient(to top, oklch(12% 0.03 262 / 0.88), oklch(12% 0.03 262 / 0));
  font-family: var(--font-ui); line-height: 1.4;
}
.plate figcaption b {
  display: block; font-size: var(--text-sm); font-weight: 600;
  color: var(--color-ink-light);
  transition: color var(--dur-short) var(--ease-out);
}
.plate figcaption span {
  display: block; font-size: var(--text-xs);
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-on-photo);
}
/* accent-hot, not accent: plain orange on a dark scrim is the wrong end of
   the ramp and goes muddy against a shadowed photograph. */
.plate:hover figcaption b, .plate:focus-within figcaption b { color: var(--color-accent-hot); }

.more__cta {
  margin-top: var(--space-lg); padding-inline: var(--gutter); text-align: center;
}

/* ── FOLD 6 · contact — THE DRAWING SHEET ─────────────────────────────
   The ornament family that annotates the rest of the page (title blocks,
   plate numbers, crop marks, small-caps labels) becomes the structure here.
   The fold is one sheet off a drawing set: ruled border, crop marks at the
   four corners, a registration cross outside the trim, the site's coordinates
   set up the right edge, and a ghosted elevation behind the type.

   Everything is drawn in CSS or inline SVG. No images, no box-shadows, no
   radius — a bevelled "blueprint card" would undo the whole idea. */
.contact {
  padding: var(--space-2xl) var(--gutter) var(--space-xl);
}

/* The trim line, and the crop marks sitting on its corners. Eight gradients
   rather than eight elements: two arms per corner, drawn on the border box so
   they land exactly on the trim rather than near it. */
.sheet {
  --crop: 30px;
  --crop-w: 2px;
  position: relative;
  display: grid; gap: var(--space-2xl);
  padding: clamp(1.5rem, 3vw, 2.5rem) clamp(1.25rem, 3.5vw, 3rem);
  border: var(--rule-hair) solid var(--color-rule);
  background-origin: border-box;
  background-repeat: no-repeat;
  background-image:
    linear-gradient(var(--color-accent) 0 0), linear-gradient(var(--color-accent) 0 0),
    linear-gradient(var(--color-accent) 0 0), linear-gradient(var(--color-accent) 0 0),
    linear-gradient(var(--color-accent) 0 0), linear-gradient(var(--color-accent) 0 0),
    linear-gradient(var(--color-accent) 0 0), linear-gradient(var(--color-accent) 0 0);
  background-size:
    var(--crop) var(--crop-w), var(--crop-w) var(--crop),
    var(--crop) var(--crop-w), var(--crop-w) var(--crop),
    var(--crop) var(--crop-w), var(--crop-w) var(--crop),
    var(--crop) var(--crop-w), var(--crop-w) var(--crop);
  background-position:
    left top, left top,
    right top, right top,
    left bottom, left bottom,
    right bottom, right bottom;
}
@media (min-width: 900px) {
  .sheet {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: clamp(2.5rem, 5vw, 5rem);
    align-items: start;
  }
}

/* Registration cross, outside the trim on the top right — the mark a printer
   lines the plates up on. Hidden on phones, where there is no room outside
   the sheet for anything to sit. */
.sheet__reg { display: none; }
@media (min-width: 1100px) {
  .sheet__reg {
    display: block; position: absolute; top: -9px; right: -34px;
    width: 18px; height: 18px; pointer-events: none;
  }
  .sheet__reg::before, .sheet__reg::after {
    content: ""; position: absolute; background: var(--color-accent);
  }
  .sheet__reg::before { top: 8px; left: 0; width: 18px; height: 2px; }
  .sheet__reg::after  { left: 8px; top: 0; width: 2px; height: 18px; }
}

/* Coordinates up the right edge. writing-mode rather than a rotate, so the
   glyphs stay upright to the baseline and the box reserves its own width. */
.sheet__coord { display: none; }
@media (min-width: 1100px) {
  .sheet__coord {
    display: block; position: absolute; top: clamp(3rem, 8vw, 7rem); right: -2.35rem;
    writing-mode: vertical-rl;
    font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 500;
    letter-spacing: var(--track-label); text-transform: uppercase;
    color: var(--color-ink-2); opacity: 0.75; white-space: nowrap;
  }
}

/* The backdrop. A real drawing sheet — elevation, truss detail, dimension
   grids, contour lines — sitting behind the whole fold instead of the single
   hand-drawn house that was here before.

   It rides a pseudo-element rather than .sheet's own background because that
   slot is already taken by the eight gradients that draw the crop marks. Its
   own frame was cropped off in the file (see img/README.md): the sheet draws
   crop marks in CSS where they adapt to any height, and two frames would
   never have lined up.

   Opacity, not a lightened file — one number to tune, and it keeps working if
   the artwork is ever swapped for a different drawing. */
.sheet::before {
  content: ""; position: absolute; z-index: 0; pointer-events: none;
  /* The backdrop is confined to the LEFT of the sheet and faded out before it
     reaches the form. Lowering the opacity was tried first and was the wrong
     fix: at any opacity the drawing still crossed every input underline, and a
     field rule you have to visually separate from a background line is a field
     rule that has stopped doing its job. The elevation lives in the artwork's
     left half, so cropping to the left keeps the good part anyway.

     Belt and braces on purpose — the inset confines it even where the mask is
     not supported, and the mask softens what would otherwise be a hard edge
     down the middle of the paper. */
  inset: 0 42% 0 0;
  background: url("img/sheet-blueprint.jpg") left bottom / cover no-repeat;
  opacity: 0.5;
  -webkit-mask-image: linear-gradient(to right, #000 0 58%, transparent 96%);
          mask-image: linear-gradient(to right, #000 0 58%, transparent 96%);
}
/* Below the two-column breakpoint the sheet is a tall narrow box and `cover`
   magnifies the drawing into unreadable stray lines. */
@media (max-width: 899px) { .sheet::before { display: none; } }

.sheet > .contact__intro, .sheet > .sheet__side { position: relative; z-index: 1; }
.sheet__side { display: grid; align-content: start; gap: var(--space-lg); }
/* Plate number. The one place on the page a section numbers itself. */
.sheet__idx {
  display: inline-block; margin: 0 0 var(--space-md);
  padding-bottom: var(--space-2xs);
  border-bottom: 2px solid var(--color-accent);
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 600;
  letter-spacing: var(--track-label); text-transform: uppercase; color: var(--color-ink);
}

.contact__intro h2 {
  font-size: var(--text-2xl); line-height: 1.06;
  margin-bottom: var(--space-md); max-width: 20ch;
}
.contact__lede { color: var(--color-ink-2); max-width: 34ch; margin-bottom: 0; }

/* Short accent rule between the pitch and the phone number — the drawing-set
   equivalent of a paragraph break. */
.sheet__tick {
  display: block; width: 42px; height: 2px;
  margin: var(--space-md) 0;
  background: var(--color-accent);
}

.contact__callto {
  margin: 0 0 var(--space-3xs);
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 600;
  letter-spacing: var(--track-label); text-transform: uppercase; color: var(--color-ink-2);
}
.contact__tel {
  display: inline-block; margin: 0;
  font-size: var(--text-xl); text-decoration: none;
  transition: color var(--dur-short) var(--ease-out);
}
.contact__tel:hover { color: var(--color-accent-hot); }

/* The trust row under the trim — the title block along the bottom of a
   drawing sheet. Centred, not left-aligned: it is the last thing on the page
   before the footer and it summarises rather than continues, so it reads as a
   stamp across the sheet instead of one more left-ranged column.

   Icons rather than dots between items. The dots were doing separation only;
   a mark per claim does that AND tells you what kind of claim it is at a
   glance, which is the whole job of a trust row. */
.trust {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: var(--space-sm) clamp(1.25rem, 4vw, 3.25rem);
  margin-top: var(--space-md); padding-top: var(--space-md);
  border-top: var(--rule-hair) solid var(--color-rule);
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 600;
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--color-ink);
  text-align: center;
}
.trust li { display: flex; align-items: center; gap: 0.55em; }
/* Stroked, never filled, at the weight of the hairlines around them —
   a filled glyph here would read as an app icon dropped on a drawing. */
.trust svg {
  width: 1.55em; height: 1.55em; flex: none;
  fill: none; stroke: var(--color-accent-hot); stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}

.facts { margin: 0; display: grid; gap: var(--space-sm);
  padding-top: var(--space-sm); border-top: var(--rule-hair) solid var(--color-rule); }
/* Three across only once there is room for it. A two-column tier was tried at
   380px and made the block TALLER, not shorter — the hours and the service-area
   strings both wrap to five lines in a half column, which costs more than
   stacking them ever did. */
@media (min-width: 560px) { .facts { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-md); } }
.facts dt {
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 600;
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--color-ink-2); margin-bottom: var(--space-3xs);
}
.facts dd { margin: 0; font-size: var(--text-sm); }
.facts dd a { text-underline-offset: 3px; }
.facts dd a:hover { color: var(--color-accent-hot); }

/* Inputs are ruled lines, not boxes — the same reason there are no cards. */
.field { margin-bottom: var(--space-sm); }
.field-row { display: grid; gap: var(--space-sm); }
@media (min-width: 540px) { .field-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.field label {
  display: block; margin-bottom: var(--space-3xs);
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 600;
  letter-spacing: var(--track-label); text-transform: uppercase; color: var(--color-ink);
}
.req { color: var(--color-accent-hot); }
.field input, .field select, .field textarea {
  width: 100%; padding: var(--space-2xs) 0;
  font-family: var(--font-body); font-size: 1rem; font-weight: 500;
  color: var(--color-ink);
  background: transparent;
  /* 2px in ink at 40%, not a 1px hairline at 16%. The faint rule was the
     single biggest reason the form read as an afterthought next to the
     headline — the line you are asked to write on has to look like a line. */
  border: 0; border-bottom: 2px solid oklch(28.9% 0.062 263.9 / 0.4);
  border-radius: 0;
  transition: border-color var(--dur-short) var(--ease-out);
}
.field select { padding-inline-end: var(--space-md); }
.field textarea { resize: vertical; min-height: 76px; }
.field input:hover, .field select:hover, .field textarea:hover { border-bottom-color: var(--color-ink-2); }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: 0; border-bottom-color: var(--color-accent);
}
.field input:focus-visible, .field select:focus-visible, .field textarea:focus-visible {
  outline: 3px solid var(--color-focus); outline-offset: 3px;
}
.field input[disabled], .field select[disabled], .field textarea[disabled] { opacity: 0.5; }
.field.is-invalid input, .field.is-invalid select { border-bottom-color: var(--color-accent-hot); }
.err { margin: var(--space-3xs) 0 0; font-family: var(--font-ui); font-size: var(--text-sm); color: var(--color-ink); }
.err::before { content: "▸ "; color: var(--color-accent); }

.quote .cta { margin-top: var(--space-sm); }
.form-note {
  margin: var(--space-md) 0 0; padding: var(--space-sm);
  background: var(--color-navy); color: var(--color-ink-light); font-size: var(--text-sm);
}
.form-note a { color: var(--color-accent-hot); }
.form-fine { margin: var(--space-2xs) 0 0; font-size: var(--text-xs); color: var(--color-ink-2); }

/* ── footer ───────────────────────────────────────────────────────── */
.foot {
  background: var(--color-navy-deep); color: var(--color-ink-light-2);
  padding: var(--space-2xl) var(--gutter) var(--space-lg);
  font-size: var(--text-sm);
}
.foot__row { display: grid; gap: var(--space-lg); align-items: start; }
@media (min-width: 860px) { .foot__row { grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--space-2xl); } }
.foot__trades { margin: 0; line-height: 1.9; max-width: 52ch; }
.foot__reach { margin: 0; }
.foot a { color: var(--color-ink-light); text-underline-offset: 3px; }
.foot a:hover { color: var(--color-accent-hot); }
.foot__base {
  display: flex; flex-wrap: wrap; gap: var(--space-3xs) var(--space-md);
  justify-content: space-between;
  margin-top: var(--space-2xl); padding-top: var(--space-sm);
  border-top: var(--rule-hair) solid var(--color-rule-light);
  font-size: var(--text-xs);
}
.foot__base p { margin: 0; }
/* --color-steel would be 3.01:1 on navy-deep and fail AA at this size. */
.credit a {
  display: inline-flex; align-items: center; gap: 0.45em;
  color: var(--color-ink-light-2); text-decoration-color: transparent;
}
.credit a:hover { color: var(--color-accent-hot); }
/* Deliberately small — this is a signature, not a second logo on Joe's site.
   alt="" because the sentence beside it already names Hatwebs; announcing the
   mark as well would read the credit twice. */
.credit__mark {
  width: 18px; height: auto; flex: none;
  opacity: 0.85;
  transition: opacity var(--dur-short) var(--ease-out);
}
.credit a:hover .credit__mark { opacity: 1; }

/* ── mobile call bar ──────────────────────────────────────────────── */
.callbar {
  position: fixed; inset: auto 0 0 0; z-index: 95;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  height: var(--callbar-h);
}
.callbar a {
  display: flex; align-items: center; justify-content: center; gap: var(--space-3xs);
  font-size: var(--text-md); font-weight: 700; letter-spacing: 0.04em;
  text-decoration: none; white-space: nowrap;
}
.callbar__call  { background: var(--color-navy-deep); color: var(--color-ink-light); }
.callbar__quote { background: var(--color-paper); color: var(--color-ink);
                  box-shadow: inset 1px 0 0 var(--color-rule); }
@media (min-width: 900px) { .callbar { display: none; } }

/* ── lightbox ─────────────────────────────────────────────────────── */
.lightbox {
  position: fixed; inset: 0; z-index: 200;
  display: flex; align-items: center; justify-content: center; gap: var(--space-2xs);
  padding: var(--gutter);
  background: oklch(18% 0.04 262 / 0.97);
}
.lightbox[hidden] { display: none; }
.lightbox__fig { max-width: min(1200px, 100%); }
.lightbox__fig img { max-height: 78svh; width: auto; margin-inline: auto; }
.lightbox__fig figcaption {
  margin-top: var(--space-sm); text-align: center;
  color: var(--color-ink-light-2); font-family: var(--font-ui);
  font-size: var(--text-xs); letter-spacing: var(--track-label); text-transform: uppercase;
}
.lightbox__close, .lightbox__nav {
  background: transparent; border: var(--rule-hair) solid var(--color-rule-light);
  color: var(--color-ink-light); cursor: pointer; line-height: 1; flex: none;
  transition: background-color var(--dur-short) var(--ease-out), color var(--dur-short) var(--ease-out);
}
.lightbox__close { position: absolute; top: var(--space-sm); right: var(--space-sm); width: 46px; height: 46px; font-size: 1.75rem; }
.lightbox__nav { width: 46px; height: 64px; font-size: 2rem; }
.lightbox__close:hover, .lightbox__nav:hover {
  background: var(--color-accent-hot); border-color: var(--color-accent-hot); color: var(--color-navy-deep);
}

/* ── motion ───────────────────────────────────────────────────────────
   No scroll reveals. The Photographic macrostructure is explicit: let the
   photographs do the work. The only motion is the film itself. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important; animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important; scroll-behavior: auto !important;
  }
}

@media print {
  .nav, .callbar, .lightbox, .film__media, .film__scrim, .meet::before
    { display: none !important; }
  /* an overlapping print bleeds badly on paper — unstack the pair so both
     photographs print at full size, one under the other */
  .job__stack { padding: 0; }
  .job__before { position: static; width: 100%; margin-top: 4px; box-shadow: none; padding: 0; }
  .job__before img { filter: none; }
  .job__before figcaption, .job__after figcaption { position: static; display: inline-block; }
  body { padding: 0; background: none; }
  .film { min-height: auto; padding-bottom: var(--space-2xl); }
  /* the overlap has nothing to overlap once the film collapses */
  .meet__photo { margin-block: 0; }
}

/* ── ORNAMENT · "the drawing sheet" ───────────────────────────────────
   One family, drawn from what the reference sites actually use: index
   counters (Haven Constructions numbers its projects 1/5 … 5/5), small-caps
   labels, and rules. Rendered as an architect's drawing sheet — title blocks,
   plate numbers, crop marks, a stamp — because that's the right vocabulary for
   a contractor and it keeps every ornament in one hand.

   Nothing here states a fact Joe hasn't given us. No years-in-business, no job
   counts, no awards — the reference sites carry those, we don't have them. */

/* Title block: label, rule, sheet number. Replaces the bare section label. */
/* Flex with explicit order, not grid: a grid ::before placed in column 2 is
   still the FIRST item in DOM order, so auto-placement pushed the label to
   column 3 and wrapped the sheet number onto a second row. */
.titleblock {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding-bottom: var(--space-sm);
  border-bottom: var(--rule-hair) solid var(--color-rule);
}
.titleblock--inset { margin-inline: var(--gutter); }
.titleblock__label {
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 600;
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--color-ink); white-space: nowrap;
  order: 1;
}
/* the rule that runs between label and sheet number */
.titleblock::before {
  content: ""; order: 2; flex: 1 1 var(--space-md);
  height: 1px; background: var(--color-rule);
}
.titleblock__no {  /* retired — kept harmless for any future use */
  order: 3;
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 500;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--color-ink-2); white-space: nowrap;
}
/* the sheet number is the first thing to go when space runs out */
@media (max-width: 599px) { .titleblock__no { display: none; } }

/* The drop cap was removed: at a 52px statement a two-line cap needs ~123px of
   cap height and it only had ~95px, so the "E" floated free of the baseline and
   "ight trades" read as a broken word. Drop caps belong at body-copy sizes. */
/* (the LICENSED & INSURED stamp was retired: the credentials rail now
   carries the same claim as item 02, and two of them was repetition) */

@media (prefers-reduced-motion: reduce) {
  .ticker__track { animation: none; }
}

/* ── MOTION SYSTEM ────────────────────────────────────────────────────
   Added after a static build read as "generated". The reference that scored
   at Awwwards (Haven Constructions) ships a load animation, parallax and a
   reveal on scroll; a page with none of that reads as output, not craft.

   Rules kept: transform and opacity only, the three named easings, one idea
   per element, and everything collapses under prefers-reduced-motion. */

/* 1 · Load choreography — the hero fades up rather than appearing.
      It used to zoom out from scale(1.07). Every scale-based move on this page
      has been pulled: a photograph that grows or shrinks under the reader is
      the most recognisable template tic there is, and it draws attention to
      the animation instead of to the work. Photographs now arrive by moving
      IN FROM THE SIDE — see 3 below — and nothing on this page scales. */
.film__media { animation: settle 1.4s var(--ease-out) both; }
@keyframes settle { from { opacity: 0; } to { opacity: 1; } }

.ln { display: block; overflow: hidden; }
.ln i {
  display: block; font-style: normal;
  animation: rise 1.05s var(--ease-out) both;
}
.ln:nth-child(1) i { animation-delay: 0.16s; }
.ln:nth-child(2) i { animation-delay: 0.28s; }
@keyframes rise { from { transform: translateY(110%); } to { transform: translateY(0); } }

.slogan--hero { animation: lift 0.8s var(--ease-out) 0.06s both; }
.film__line { animation: lift 0.9s var(--ease-out) 0.46s both; }
.film .cta   { animation: lift 0.9s var(--ease-out) 0.58s both; }
.film__caption { animation: lift 0.9s var(--ease-out) 0.7s both; }
.nav { animation: lift 0.8s var(--ease-out) 0.1s both; }
@keyframes lift { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }

/* 2 · Parallax. --par is written by JS in a rAF loop; 0 when unsupported. */
.film__media { will-change: transform; }
.film.is-par .film__media {
  animation: none;
  transform: translate3d(0, calc(var(--par, 0) * 14vh), 0) scale(1.14);
}

/* 3 · Scroll reveal — photographs travel IN FROM THE SIDE.
      One rule for every picture on the page: it starts offset horizontally and
      slides to its place over about a second. Nothing scales, on scroll or on
      hover. Slow enough to read as deliberate, short enough (56–72px) that the
      page never looks like it is assembling itself.

      --slide-in is the distance and sign; each block below sets it and the
      direction is chosen from where the picture sits, so everything moves
      outward-to-inward rather than all one way. */
.js .plate { opacity: 0; transform: translateX(72px); }
.js .plate.in {
  opacity: 1; transform: none;
  transition: opacity 1.05s var(--ease-out), transform 1.15s var(--ease-out);
}
/* left to right across the strip, so the four read as one band arriving
   rather than four separate things happening at once */
.js .plate:nth-child(2).in { transition-delay: .09s; }
.js .plate:nth-child(3).in { transition-delay: .18s; }
.js .plate:nth-child(4).in { transition-delay: .27s; }

.js .plate figcaption { opacity: 0; }
.js .plate.in figcaption {
  opacity: 1;
  transition: opacity .7s var(--ease-out) .3s, color var(--dur-short) var(--ease-out);
}
/* Hover is a colour change only. The 1.045 scale that used to live here was
   the "lame" one — it re-cropped the photograph under the cursor. */
.js .plate.in:hover figcaption b { color: var(--color-accent-hot); }

/* 4 · Everything else rises once, quietly. */
.js .rv { opacity: 0; transform: translateY(18px); }
.js .rv.in {
  opacity: 1; transform: none;
  transition: opacity .85s var(--ease-out), transform .85s var(--ease-out);
}
.js .rv-d1.in { transition-delay: .08s; }
.js .rv-d2.in { transition-delay: .16s; }
.js .rv-d3.in { transition-delay: .24s; }

/* 5 · The title-block rule draws itself in. */
.js .titleblock::before { transform: scaleX(0); transform-origin: left; }
.js .titleblock.in::before { transform: scaleX(1); transition: transform 1.1s var(--ease-out) .1s; }

@media (prefers-reduced-motion: reduce) {
  .film__media, .ln i, .film__line, .film .cta, .film__caption, .nav, .slogan--hero { animation: none !important; }
  .film.is-par .film__media { transform: none; }
  .js .plate .plate__btn { clip-path: none; }
  .js .plate .plate__btn img,
  .js .plate, .js .plate figcaption, .js .rv { opacity: 1; transform: none; }
  .js .titleblock::before { transform: scaleX(1); }
}

/* ── BEFORE / AFTER ───────────────────────────────────────────────────
   Was two photos side by side. Side by side makes the reader do the work —
   two frames, two viewpoints, spot the difference. A wipe puts both states on
   the same pixels, so the change is the only thing that moves. It is also the
   one interaction on this page worth building: it costs nothing to understand
   and it is the reason someone stays on the section.

   Beside each slider: a title and one paragraph about what was actually done.
   A Location / Scope / Work done / On site ticket used to sit here and was
   cut — the values were placeholders, and a spec table is only worth its
   space when every value in it is true. */
.ba { padding-bottom: var(--space-xl); }

.ba__head {
  display: grid; gap: var(--space-sm);
  margin-top: var(--space-md); padding-inline: var(--gutter);
}
@media (min-width: 900px) {
  /* not 50/50 — the headline leads, the lede sits in the margin beside it */
  .ba__head {
    grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
    gap: var(--space-2xl); align-items: end;
  }
}
.ba__h {
  font-family: var(--font-display); font-weight: 500;
  font-size: var(--text-2xl); line-height: 1.06;
  letter-spacing: var(--track-display);
  color: var(--color-ink); text-wrap: balance;
}
/* the wordmark's cut, reused. Same mark that opens the hero slogan. */
.ba__h::before {
  content: ""; display: block; width: 34px; height: 6px;
  margin-bottom: var(--space-sm);
  background: var(--color-accent);
  clip-path: polygon(6px 0, 100% 0, calc(100% - 6px) 100%, 0 100%);
}
.ba__lede { color: var(--color-ink-2); max-width: 44ch; }

/* ---- the two jobs, stacked and alternating ---------------------------
   Fourth arrangement. The two-up slider layout came out for one reason: the
   drag handle. Nobody who lands here is looking for a control to operate, and
   the people Joe works for skew old enough that a discovered gesture is a
   wall, not a feature. So the comparison stops being an interaction and
   becomes a picture — see .job__stack below.

   One job per row instead of two side by side. A diptych needs the width: the
   finished room has to be big enough to sell the work, and the before print
   has to be small enough that nobody confuses the two. */
.jobs {
  display: grid; gap: var(--space-xl);
  margin-top: var(--space-lg); padding-inline: var(--gutter);
}
@media (min-width: 1000px) {
  .job {
    display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
    gap: var(--space-2xl); align-items: center;
  }
  /* the second job mirrors the first. The column WIDTHS swap too — order
     alone would drop the photograph into the narrow track and shrink it by a
     third, which is the bug that made the first pass look lopsided. */
  .job:nth-child(even) { grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr); }
  .job:nth-child(even) .job__stack { order: 2; }
  .job:nth-child(even) .job__panel { justify-self: end; }
}

/* Caption, not a panel: the job's name and one thing worth knowing about it.
   The "Featured job" eyebrow and the per-job "Ask Joe" link both came out —
   printed twice side by side they read as furniture, and the section already
   ends in that same link. */
.job__panel { max-width: 44ch; }
@media (max-width: 999px) {
  /* stacked, the panel needs the rule back — with nothing beside it the type
     would otherwise float under the photograph with no edge to sit on */
  .job__panel {
    margin-top: var(--space-md); padding-top: var(--space-sm);
    border-top: var(--rule-hair) solid var(--color-rule);
  }
}
.job__h {
  margin-bottom: var(--space-sm);
  font-family: var(--font-display); font-weight: 600;
  font-size: var(--text-lg); line-height: 1.14;
  letter-spacing: var(--track-display);
  color: var(--color-ink); text-wrap: balance;
}
.job__note { font-size: var(--text-base); line-height: 1.65; color: var(--color-ink-2); }
.job__meta {
  margin-top: var(--space-md); padding-top: var(--space-sm);
  border-top: var(--rule-hair) solid var(--color-rule);
  font-family: var(--font-ui); font-size: var(--text-xs);
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-ink-2);
}
@media (max-width: 999px) { .job__meta { border-top: 0; padding-top: 0; } }

/* Navy text, accent arrow. Orange type on paper is ~3.1:1 and fails at this
   size; the arrow carries the accent instead and it is decorative. */
.more__cta a {
  display: inline-flex; align-items: baseline; gap: var(--space-2xs);
  font-family: var(--font-ui); font-size: var(--text-sm); font-weight: 600;
  color: var(--color-ink); text-decoration: none;
  border-bottom: 1px solid var(--color-rule);
  padding-bottom: 2px;
}
.more__cta a span {
  display: inline-block; color: var(--color-accent);
  transition: transform var(--dur-short) var(--ease-out);
}
.more__cta a:hover, .more__cta a:focus-visible { border-bottom-color: var(--color-ink); }
.more__cta a:hover span { transform: translateX(5px); }

/* ---- the diptych ----------------------------------------------------
   Both states are on the page at once, and neither one is hiding. The
   finished room is the plate; the room Joe walked into is a smaller print
   laid over its bottom corner, on paper, the way a builder actually shows
   you. No control, no gesture, nothing to discover — it reads the same in a
   browser, in a screenshot, on a printed flyer and in a Google listing.

   The before is deliberately weaker than the after in three ways at once —
   size, saturation and the paper mount — because a viewer has to be able to
   tell which is which in the quarter-second before they read a label. */
.job__stack {
  position: relative;
  /* Only the DOWNWARD overhang is reserved here, and only part of it — the
     rest is borrowed from the row gap below (see .job__before). The sideways
     overhang used to be reserved the same way, as padding-left, which meant
     every pixel the print hung out by was a pixel taken off the width of the
     finished room. It now hangs into the page gutter instead and the after
     photo gets the whole column. */
  padding-bottom: clamp(2rem, 4vw, 3.5rem);
}

.job__after { position: relative; background: var(--color-navy-deep); }
.job__after img {
  display: block; width: 100%; object-fit: cover; aspect-ratio: 4 / 3;
}
/* 5:3, between the source's own 3:2 and the 16:9 this was briefly set to.
   16:9 bought the most scroll back and cost the most: a bathroom is a TALL
   room, and the letterbox took out the floor and the ceiling — in an
   accessible conversion the curbless floor is half the point. 5:3 keeps the
   room and still returns ~35px a job. */
@media (min-width: 700px) { .job__after img { aspect-ratio: 5 / 3; } }
/* accent marks the payoff. 4.6:1, navy on orange. */
.job__after figcaption {
  position: absolute; top: 0; right: 0;
  padding: 0.55em 0.9em;
  background: var(--color-accent); color: var(--color-accent-ink);
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 600;
  letter-spacing: var(--track-label); text-transform: uppercase;
}

/* The print hangs OFF the corner rather than sitting on it. It used to overlap
   the finished room by about 85% of its own area, which buried the thing the
   section exists to show. Now roughly a fifth of its width and a third of its
   height are outside the frame, so it clears the after photo's bottom corner
   and only its inner quadrant sits over the picture.

   The downward offset is negative on purpose: it borrows from the row gap
   below instead of adding to the row's height, so the print moves further out
   without making the section any longer to scroll. */
.job__before {
  position: absolute;
  /* Sideways offset stops just inside var(--gutter) — past that the print
     bleeds off the page edge and reads as a mistake rather than a margin. */
  left: calc(-1 * clamp(1.25rem, 3.75vw, 3rem));
  bottom: calc(-1 * clamp(1.25rem, 2.75vw, 2.5rem));
  width: clamp(30%, 27vw, 34%);
  /* the paper border is the whole trick: it makes the small photo read as a
     physical print sitting ON the big one rather than a second crop of it */
  padding: clamp(6px, 0.7vw, 10px);
  padding-bottom: clamp(1.6rem, 2.4vw, 2.2rem);
  background: var(--color-paper);
  box-shadow: 0 clamp(10px, 2vw, 26px) clamp(20px, 4vw, 50px) oklch(20% 0.04 262 / 0.22);
}
/* The mirrored job hangs its print off the OUTER corner too — anchored left on
   a right-hand photo would push the print in toward the text column. */
@media (min-width: 1000px) {
  .job:nth-child(even) .job__before {
    left: auto; right: calc(-1 * clamp(1.25rem, 3.75vw, 3rem));
  }
}
.job__before img {
  display: block; width: 100%; object-fit: cover; aspect-ratio: 4 / 3;
  filter: saturate(0.72) contrast(0.96);
}
.job__before figcaption {
  position: absolute; left: clamp(6px, 0.7vw, 10px); bottom: clamp(0.45rem, 0.8vw, 0.7rem);
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 600;
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--color-ink-2);
}

/* reveal, matching the gallery plates: both photographs come in from the side
   the job sits on — job 01 from the left, job 02 from the right — and the
   before print follows the finished room in a beat later, travelling further
   so the two do not arrive as one flat block. */
.js .job .job__after  { opacity: 0; transform: translateX(-64px); }
.js .job .job__before { opacity: 0; transform: translateX(-96px); }
.js .job:nth-child(even) .job__after  { transform: translateX(64px); }
.js .job:nth-child(even) .job__before { transform: translateX(96px); }
.js .job.in .job__after, .js .job.in .job__before {
  opacity: 1; transform: none;
}
.js .job.in .job__after {
  transition: opacity 1s var(--ease-out), transform 1.2s var(--ease-out);
}
.js .job.in .job__before {
  transition: opacity .9s var(--ease-out) .28s, transform 1.1s var(--ease-out) .28s;
}
.js .job .job__panel { opacity: 0; transform: translateY(14px); }
.js .job.in .job__panel {
  opacity: 1; transform: none;
  transition: opacity .7s var(--ease-out) .2s, transform .7s var(--ease-out) .2s;
}
@media (prefers-reduced-motion: reduce) {
  .js .job .job__after, .js .job .job__before, .js .job .job__panel {
    opacity: 1; transform: none;
  }
}

/* ── SLOGAN ───────────────────────────────────────────────────────────
   Joe's own line. It travels with the wordmark, so it sits in the two places
   the brand signs itself: the top of the film and under the footer wordmark.
   Deliberately NOT the h1 — "Built right. Done once." is the sales message;
   this is the signature. */
.slogan {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  line-height: 1;
}
.slogan--hero {
  display: flex; align-items: center; gap: var(--space-2xs);
  margin-bottom: var(--space-md);
  font-size: var(--text-sm);
  color: var(--color-ink-light);
}
.slogan--hero::before {
  content: ""; width: 30px; height: 5px; flex: none;
  background: var(--color-accent-hot);
  clip-path: polygon(5px 0, 100% 0, calc(100% - 5px) 100%, 0 100%);
}
.foot__brand { display: block; }
.slogan--foot {
  margin: var(--space-2xs) 0 var(--space-md);
  font-size: var(--text-sm);
  color: var(--color-accent-hot);   /* 6.0:1 on navy-deep */
}
