/* ============================================================================
   BASE — identical on every Pittsburgh IT build. The visitor never sees any of it.
   Token NAMES are fixed; only the VALUES change per client. Everything below the
   token block is structural: the accessibility floor, the form skeleton, tap
   targets, the motion contract. Roughly 20 lines get rewritten per client and
   several hundred lines of quality are inherited.
   MOBILE FIRST — every rule here is the small-screen rule. Breakpoints add.
   ========================================================================== */
:root{
  --surface:#121212;
  --surface-2:#1B1B1B;
  --band:#F4F4F2;
  --band-ink:#121212;
  --band-ink-2:#4E4E48;
  --accent-on-band:#A81050;
  --ink:#F4F4F2;
  --ink-2:#B4B4AF;
  --ink-3:#8C8C88;
  --accent:#E01E6E;
  --accent-2:#F4649B;
  --accent-ink:#FFFFFF;
  --line:#2A2A2A;
  --maxw:1180px;
  --gutter:22px;
  --radius:0px;
  --accent-a12:rgba(224,30,110,.12);
  /* THE BOARD's surfaces. All four are DERIVED from --surface by an HSL lightness
     step in build.py, so the services section separates from the page by TONE and
     can never be a white card on a grey page. --board-accent is --accent unless it
     cannot hold 4.5:1 on --board, in which case build.py has already swapped it. */
  --board:#121212;
  --board-cell:#1B1B1B;
  --board-line:#2A2A2A;
  --board-accent:#F4649B;
  --board-glow:rgba(224,30,110,.20);
  --board-shadow-1:rgba(244,244,242,.08);
  --board-shadow-2:rgba(244,244,242,.60);
  --font-display:'Familjen Grotesk',system-ui,-apple-system,'Segoe UI',Helvetica,Arial,sans-serif;
  --font-body:'Familjen Grotesk',system-ui,-apple-system,'Segoe UI',Helvetica,Arial,sans-serif;
}

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
body{margin:0;background:var(--surface);color:var(--ink);
  font-family:var(--font-body);font-size:17px;line-height:1.62;
  -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
  overflow-x:hidden}
img,svg,video{max-width:100%;height:auto;display:block}
h1,h2,h3,h4{font-family:var(--font-display);margin:0;line-height:1.08;
  font-weight:700;letter-spacing:-.03em}
p{margin:0 0 1em}
a{color:inherit}
ul{margin:0;padding:0;list-style:none}

/* --- container ------------------------------------------------------------ */
.container{width:100%;max-width:var(--maxw);margin:0 auto;padding:0 var(--gutter)}
@media(min-width:760px){:root{--gutter:34px}}
@media(min-width:1180px){:root{--gutter:48px}}

/* --- accessibility floor -------------------------------------------------- */
.skip{position:absolute;left:-9999px;top:0;z-index:200;background:var(--accent);
  color:var(--accent-ink);padding:12px 18px;border-radius:0 0 var(--radius) 0;
  font-weight:700;text-decoration:none}
.skip:focus{left:0}
:focus-visible{outline:3px solid var(--accent);outline-offset:3px;border-radius:2px}
.vh{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);
  clip-path:inset(50%);white-space:nowrap}
/* honeypot: OFF-SCREEN, never display:none — a hidden field some bots skip,
   and a screen reader is told to leave it alone. */
.hp{position:absolute!important;left:-9999px;top:-9999px;width:1px;height:1px;
  opacity:0;pointer-events:none}

/* --- tap targets ---------------------------------------------------------- */
a,button,input,select,textarea,summary{font-family:inherit}
.btn,button,input[type=submit]{min-height:48px}
nav a,footer a{display:inline-block;min-height:24px}

/* --- button skeleton (templates restyle the skin, not the mechanics) ------- */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:.55em;
  padding:14px 26px;border:1px solid transparent;border-radius:var(--radius);
  font-weight:650;font-size:16px;line-height:1.1;text-decoration:none;cursor:pointer;
  transition:transform .18s ease,background .18s ease,color .18s ease,border-color .18s ease,box-shadow .18s ease}
.btn:active{transform:translateY(1px)}
.btn-primary{background:var(--accent);color:var(--accent-ink);border-color:var(--accent)}
.btn-ghost{background:transparent;color:var(--ink);border-color:var(--line)}

/* --- form skeleton -------------------------------------------------------- */
.form{display:grid;gap:16px}
.field{display:grid;gap:7px}
.field label{font-size:12.5px;font-weight:700;letter-spacing:.09em;
  text-transform:uppercase;color:var(--ink-2)}
.field input,.field select,.field textarea{
  width:100%;min-height:50px;padding:13px 15px;border:1px solid var(--line);
  border-radius:var(--radius);background:var(--surface);color:var(--ink);
  font-size:16px;/* 16px stops iOS zooming the page on focus */
  font-family:inherit;transition:border-color .18s ease,box-shadow .18s ease}
.field textarea{min-height:140px;resize:vertical;line-height:1.55}
.field input:focus,.field select:focus,.field textarea:focus{
  outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(224,30,110,.20)}
.field ::placeholder{color:var(--ink-3);opacity:1}
.frow{display:grid;gap:16px}
@media(min-width:680px){.frow{grid-template-columns:1fr 1fr}}
.thanks{display:none;border:1px solid var(--line);border-radius:var(--radius);
  padding:28px;background:var(--surface-2)}
.thanks.show{display:block}
.thanks h3{font-size:24px;margin-bottom:10px}
.formerror{color:#A81050;font-size:15px;font-weight:600;margin-top:10px}

/* --- MOTION CONTRACT ------------------------------------------------------
   Nothing starts hidden unless JavaScript has already run. The `js` class is set
   by an inline <head> script, and ONLY when prefers-reduced-motion is not set.
   No JS, blocked JS, old browser, or reduced motion => the page is simply visible.
   This is the inverse of how most sites do it, and it is why a script failure
   here cannot produce a blank page on a lead-generating site. */
html.js .reveal{opacity:0;transform:translateY(18px);
  transition:opacity .7s cubic-bezier(.22,.61,.36,1),transform .7s cubic-bezier(.22,.61,.36,1)}
html.js .reveal.in{opacity:1;transform:none}
html.js .reveal.d1{transition-delay:.08s}
html.js .reveal.d2{transition-delay:.16s}
html.js .reveal.d3{transition-delay:.24s}
@media(prefers-reduced-motion:reduce){
  html.js .reveal,html.js .reveal.in{opacity:1!important;transform:none!important;transition:none!important}
  *{animation-duration:.001ms!important;animation-iteration-count:1!important;
    transition-duration:.001ms!important}
}

/* --- nav skeleton --------------------------------------------------------- */
#nav{position:fixed;top:0;left:0;right:0;z-index:100;
  transition:background .25s ease,box-shadow .25s ease,border-color .25s ease}
.nav-inner{display:flex;align-items:center;justify-content:space-between;
  gap:16px;min-height:62px}
.navtoggle{display:inline-flex;align-items:center;justify-content:center;
  width:48px;height:48px;margin-right:-10px;background:none;border:0;
  color:inherit;cursor:pointer}
@media(min-width:940px){.navtoggle{display:none}}
.nav-links{display:none}
.nav-links.open{display:flex}
.nav-links{gap:0}
/* Desktop nav reset. Deliberately scoped to #nav so its specificity (1,1,0) beats any
   later `.nav-links{position:absolute…}` a template writes for its mobile drawer —
   equal specificity later in the file wins over a media query, which silently left the
   drawer stacked at the left edge on six of seven templates before this was scoped.
   It sets ONLY the structural properties that must not leak from mobile to desktop;
   gap, colour and type stay the template's business. */
@media(min-width:940px){
  #nav .nav-links,#nav .nav-links.open{display:flex;position:static;inset:auto;
    flex-direction:row;align-items:center;padding:0;margin:0;border:0;background:none;
    width:auto;height:auto;min-height:0;max-height:none;box-shadow:none;transform:none;
    visibility:visible;opacity:1;overflow:visible;
    gap:var(--nav-gap,32px)}
}
html.menu-open,html.menu-open body{overflow:hidden}

/* --- footer skeleton ------------------------------------------------------ */
footer a{text-decoration:none}
.credit{font-size:13.5px}
.credit a{text-decoration:underline;text-underline-offset:3px}

/* --- button rows ----------------------------------------------------------
   On a phone two CTAs sitting side by side at different intrinsic widths look
   accidental, and neither is a comfortable thumb target. They go full width until
   there is room for a row. Scoped as `.btnrow > .btn` (0,2,0) so a template's own
   `.btn{}` rule later in the file cannot silently undo it — the same cascade trap
   that left the desktop nav stacked. */
.btnrow{display:flex;flex-wrap:wrap;gap:12px}
.btnrow > .btn{flex:1 1 100%}
@media(min-width:560px){.btnrow > .btn{flex:0 0 auto}}

/* --- long-string guard ----------------------------------------------------
   A client email address or a compound word set in a heavy display face at
   20px uppercase does not wrap, and pushes a 390px page sideways. Found on the
   Ironworks contact page: dispatch@vantagemechanical.com forced the document to
   462px. Plumbing, so every template inherits the fix. */
/* Headings are deliberately NOT in this guard. break-word on a display heading
   shatters a word mid-syllable the moment its container is narrower than the word —
   it turned "Everything we make," into "Everyth / ing we / make," on Marquee. A
   heading that does not fit is a LAYOUT bug; fix the column, do not hyphenate it. */
h1,h2,h3,h4{overflow-wrap:normal;word-break:normal}
a[href^="mailto:"],a[href^="tel:"]{overflow-wrap:anywhere}
.crow span,.crow b,.cinfo span,address{overflow-wrap:anywhere}

/* --- utility -------------------------------------------------------------- */
.sec{padding:64px 0}
@media(min-width:760px){.sec{padding:96px 0}}
@media(min-width:1180px){.sec{padding:124px 0}}
.eyebrow{font-size:12px;font-weight:750;letter-spacing:.16em;text-transform:uppercase;
  color:var(--accent);margin:0 0 14px}
.lead{font-size:19px;line-height:1.6;color:var(--ink-2)}
@media(min-width:760px){.lead{font-size:21px}}

/* ============================================================ THE BOARD =====
   THE ONE services section for every one-page build, current and future.
   Jon, 2026-08-14: this replaces the board/menu section on every skeleton.
   Ruleset and worked examples: _system/onepage/SERVICES-SECTION.md.

   It renders BOTH shapes the contract carries, through one loop, because
   build.py normalises them into board.groups before this file ever sees them:
     services[]  4-6 headline offerings          -> one untitled group
     menu[]      a grouped price list            -> one titled group each
   A barber with six services and a restaurant with eight menu groups get the
   same instrument, and the skeleton does not know which it got.

   WHY IT LIFTS BY TONE, NOT BY SHADOW. The page background is the brand's
   neutral base. The container is the SAME neutral at a different value, so it
   separates even before the shadow renders — on a low-contrast screen, in
   forced-colours, and in print, where the shadow is simply gone. A white card
   on a grey page is the default this exists to replace: it is the one move that
   makes a spec site look like a template, because it belongs to no brand.
   --board / --board-cell / --board-line are DERIVED FROM --surface in build.py
   by an HSL lightness step, never hand-typed, so they cannot drift off-palette
   when a client's logo repaints the page.

   THE PRICE IS A COLUMN, NOT THE POINT. A row with no price omits the cell and
   the other two tracks take the space; the section still renders, because the
   reason it exists is the list of services, not the figures beside them. One
   hook note prints above the grid when anything is unpriced — that is the only
   moment the page speaks to its own owner, and it is why a prospect writes back.

   TOKENS a skeleton sets (all have working defaults):
     --bd-cols      columns in the grid          default 1, 2 from 820px
     --bd-gap       gutter between cells         default 10px
     --bd-pad       padding inside the container default 20px, 32px from 820px
     --bd-cell-pad  padding inside a cell        default 16px
     --bd-icon      the icon plate               default 46px, 52px from 820px
     --bd-glyph     the glyph inside it          default 24px, 26px from 820px
     --bd-price     the price size               default 21px, 24px from 820px
     --bd-edge      the accent edge on a cell    default a 2px gradient bar
     --bd-radius    corner radius                default calc(var(--radius)*1.4)
     --bd-glow      accent halation around the panel  DEFAULT OFF - opt in only
                    on a skeleton whose idea is light
     --bd-accent    the accent that is SAFE here — build.py falls back to
                    --accent-2, then to --ink, if --accent cannot hold 4.5:1 on
                    the container. Small uppercase text in a brand colour on a
                    tonal surface is the exact place a palette goes unreadable.
   Parts to skin: .board .bgrid .bitem .bicon .bname .bdesc .btag .bprice .bgh
   ========================================================================== */
.board{
  --bd-cols:1;
  --bd-gap:10px;
  --bd-pad:20px;
  --bd-cell-pad:16px;
  --bd-icon:46px;
  --bd-glyph:24px;
  --bd-price:21px;
  --bd-radius:calc(var(--radius) * 1.4);
  --bd-accent:var(--board-accent);
  --bd-glow:transparent;
  --bd-edge:linear-gradient(90deg,var(--accent) 0%,var(--accent-2) 62%,transparent 100%);
  background:var(--board);
  border:1px solid var(--board-line);
  border-radius:var(--bd-radius);
  padding:var(--bd-pad);
  /* THE GLOW IS OPT-IN AND OFF BY DEFAULT. Jon, 2026-08-14: "glow isn't right
     on all sites, we have a template that does glow... everything else is
     accurate." A halation is a LIGHT effect and it belongs to the skeleton
     whose organising idea is light (04 Lightbox). On a ruled ledger or a flat
     midday page it is a borrowed trick, and a device used everywhere is
     decoration. A skeleton opts in with --bd-glow:var(--board-glow).
     The lift itself does NOT depend on it: the colour step and the accent
     hairline carry it, which is why they are derived and not optional. */
  box-shadow:0 0 46px -6px var(--bd-glow),
             0 1px 2px var(--board-shadow-1),
             0 26px 56px -24px var(--board-shadow-2);
}
@media(min-width:820px){.board{--bd-pad:32px;--bd-icon:52px;--bd-glyph:26px;--bd-price:24px}}

/* THE GRID. Two columns from 820px, one below it, and the cell layout is the
   SAME at every width — icon, then the flexible middle, then the price. It does
   not restack on mobile, because a row that reflows into a different shape is a
   second design to maintain and it always reads as the cheaper one. */
.bgrid{display:grid;grid-template-columns:repeat(var(--bd-cols),minmax(0,1fr));
  gap:var(--bd-gap);margin:0;padding:0;list-style:none}
@media(min-width:820px){.board{--bd-cols:2}}

.bitem{position:relative;display:grid;
  grid-template-columns:auto minmax(0,1fr) auto;
  align-items:start;column-gap:14px;
  padding:var(--bd-cell-pad);
  border-radius:calc(var(--bd-radius) * .7);
  background:var(--board-cell);
  overflow:hidden}
/* The edge accent is a PSEUDO-ELEMENT, never a border-top: a border changes the
   box and a gradient cannot live on one side of it. ::before also means a
   skeleton can move the edge (left rail, full underline) by restyling one rule
   without touching the grid. */
.bitem::before{content:"";position:absolute;inset:0 0 auto 0;height:2px;
  background:var(--bd-edge)}

.bicon{display:flex;align-items:center;justify-content:center;
  width:var(--bd-icon);height:var(--bd-icon);flex:none;
  border-radius:calc(var(--bd-radius) * .55);
  background:var(--accent-a12);color:var(--bd-accent)}
/* The glyph is sized HERE and nowhere else. Every icon on the page is one
   library at one stroke weight (icons.py writes the wrapper), so the only thing
   a template may change is the size and the colour of the whole set. */
.bicon > svg{width:var(--bd-glyph);height:var(--bd-glyph);display:block}

/* the flexible middle. min-width:0 or a long unbroken word (a URL in a blurb,
   an email address) blows the track out and pushes the price off the row. */
.bmain{grid-column:2;min-width:0}
.bname{margin:0;font-family:var(--font-body);font-size:16.5px;font-weight:700;
  line-height:1.3;color:var(--ink)}
.bdesc{margin:5px 0 0;font-size:14.5px;line-height:1.5;color:var(--ink-3)}
/* justify-self:start or the chip fills its grid cell — a grid item stretches by
   default, and "45 MIN" drew a 350px rounded rectangle across the row the first
   time this was built. Found on the render, not in the markup. */
.btag{display:inline-block;justify-self:start;margin:8px 0 0;
  padding:3px 9px;border:1px solid var(--board-line);
  border-radius:999px;font-size:11.5px;font-weight:700;letter-spacing:.09em;
  text-transform:uppercase;color:var(--ink-2)}
.bprice{grid-column:3;align-self:center;font-family:var(--font-display);
  font-size:var(--bd-price);font-weight:700;line-height:1.1;
  font-variant-numeric:tabular-nums;white-space:nowrap;color:var(--ink)}

/* A GROUP HEADING IN THE BRAND COLOUR, and it stays with its list. */
.bgroup + .bgroup{margin-top:26px}
.bgh{margin:0 0 10px;line-height:1.2;font-family:var(--font-body);font-size:12.5px;font-weight:750;
  letter-spacing:.14em;text-transform:uppercase;color:var(--bd-accent)}

/* THE REPLY HOOK. Printed once, above the grid, only when something is unpriced.
   It is not an empty state — the cells with no price are already complete — it
   is the page asking its owner for the one thing only they have. */
.bhook{margin:0 0 16px;padding:13px 15px;border:1px dashed var(--board-line);
  border-radius:calc(var(--bd-radius) * .6);background:transparent}
.bhook-l{display:block;margin:0 0 3px;font-size:11.5px;font-weight:750;
  letter-spacing:.14em;text-transform:uppercase;color:var(--bd-accent)}
.bhook-t{display:block;font-size:14px;line-height:1.5;color:var(--ink-2)}


/* ============================================================================
   THE GOOGLE RATING — shared structural CSS for partials/rating.html.
   Canonical source. Skinned per skeleton by declaring these five tokens in the
   template's own site.css; never by editing the rules below.

     --rating-bg      the figure's ground
     --rating-shadow  its cast shadow, in that skeleton's light logic
     --rating-rim     its inner hairline / lit edge
     --rq-bg          the review slot's ground
     --rq-ink         the review slot's body colour

   Jon's standing requirement: the rating is NEVER a stat in a card. It is the
   only hard checkable fact on the page, so it takes display scale and air.
   ========================================================================= */
.rating{margin-top:38px;display:block}
.rating-fig{position:relative;isolation:isolate;overflow:hidden;
  padding:clamp(26px,4vw,54px) clamp(24px,4vw,56px) clamp(28px,4vw,52px);
  border-radius:calc(var(--radius) * 2);
  background:var(--rating-bg, color-mix(in srgb, var(--accent) 8%, var(--surface-2)));
  box-shadow:var(--rating-shadow, 0 30px 64px -32px rgba(0,0,0,.5))}
.rating-fig::after{content:"";position:absolute;inset:0;z-index:-1;border-radius:inherit;
  box-shadow:inset 1px 1px 0 var(--rating-rim, rgba(255,255,255,.14))}
.rstars{position:relative;display:inline-block;font-size:clamp(24px,3.6vw,40px);
  letter-spacing:.14em;line-height:1;white-space:nowrap}
.rstars i{font-style:normal;display:block}
.rs-off{color:color-mix(in srgb, var(--accent) 26%, transparent)}
/* filled to the EXACT fraction: a discrete half-star claimed 4.5 while the figure beside
   it said 4.8, and both cannot be true. 4.8 of 5 is 96% of the row. */
.rs-on{position:absolute;inset:0;overflow:hidden;width:var(--fill,100%);color:var(--accent)}
.rnum{display:block;font-family:var(--font-display);font-weight:700;letter-spacing:-.055em;
  font-size:clamp(120px,22vw,300px);line-height:.8;color:var(--ink);margin-top:10px}
.rsay{margin:18px 0 0;font-family:var(--font-display);
  font-size:clamp(19px,2.4vw,31px);line-height:1.24;letter-spacing:-.015em;
  color:var(--ink-2);max-width:24ch}
.rsay .rcount{color:var(--ink);font-weight:700}
/* Google Maps attribution as a PILL, under the sentence and inside the SAME container as
   the rating - Google's placement guidance - and the pill is itself the "visually
   distinguish this content" cue they ask for. Logo AND the words. Min 16dp / max 19dp
   high with Google's clear space. The asset is official and is never restyled or redrawn. */
.gmpill{display:inline-flex;align-items:center;gap:9px;margin:18px 0 0;
  padding:8px 14px 8px 12px;border-radius:99px;text-decoration:none;
  background:rgba(255,255,255,.07);border:1px solid var(--rating-rim, rgba(255,255,255,.16));
  transition:background .2s,border-color .2s}
.gmpill:hover{background:rgba(255,255,255,.12)}
.gmpill img{display:block;height:17px;width:auto}
.gmpill .gmgo{font:600 13px/1 system-ui,-apple-system,'Segoe UI',sans-serif;
  letter-spacing:.01em;color:var(--ink);white-space:nowrap}
.gmpill:hover .gmgo{color:var(--accent)}
.rquotes{display:grid;gap:14px;margin-top:22px;max-width:660px}
.rq{margin:0;padding:20px 22px;border-radius:var(--radius);
  background:var(--rq-bg, var(--surface-2))}
.rq blockquote{margin:0;font-family:var(--font-display);font-size:18px;line-height:1.42;
  color:var(--rq-ink, var(--ink))}
.rq figcaption{margin-top:9px;font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--ink-2)}
.rq-empty{background:none;border:1px dashed var(--line)}
.rq-empty blockquote{color:var(--ink-2);font-size:16px}
@media(min-width:900px){
  .rating-fig{display:grid;grid-template-columns:auto minmax(0,1fr);
    column-gap:clamp(36px,5vw,76px);align-items:center}
  .rstars{grid-column:1;grid-row:1;margin-bottom:2px}
  .rnum{grid-column:1;grid-row:2;margin-top:0}
  .rsay{grid-column:2;grid-row:1 / span 2;align-self:center;margin:0;max-width:15ch}
}


/* ============================================================================
   04 — THE WALL.  site.css

   base.css is PREPENDED to this file, then shared/rating.css; mosaic.css and
   pghnote.css are appended after it. So everything below is a SKIN, never a
   rebuild, and nothing here re-declares a shared floor item.

   THE IDEA, and the correction of 2026-08-15.

   The wall is a FEATURE, not the whole page. The tile texture — surfaces
   mortared at a 5px joint, hard 0-radius corners — exists in exactly three
   places and nowhere else:

       1. THE MASTHEAD WALL   full bleed, photographic
       2. THE BOARD           contained, because a price board IS a grid of cells
       3. THE PLATE BAND      full bleed, photographic

   Everything else sits inside the shared 1180px .container with base.css's
   34/48px gutter — the same distance from the window as 01-nightwork — and is
   ranked by TYPE and by the ACCENT rather than by a border.

   WHY. The previous build applied the grid to all nine sections, edge to edge.
   Nine sections then had one identical box texture separated only by a 5px
   line: no hierarchy, nowhere for the eye to rest, and content running into the
   browser frame on a wide screen. The identity became noise. The mortar is a
   device, and a device used everywhere is a texture nobody sees.

   WHAT CARRIES THE IDENTITY NOW, with no boxes to lean on:
     - the MONO LABEL opens every fact, on every section
     - the ACCENT punctuates a figure, a rule or a link — it never outlines a cell
     - the 0-radius corner survives on every surface that still exists
     - the 5px mortar is untouched inside the three tiled moments
   Take the boxes away and the page should still be unmistakably this template.

   THE TWO VOICES. Familjen Grotesk carries every sentence and every headline;
   JetBrains Mono carries every LABEL — the masthead, the eyebrows, the day
   names, the durations, the odometer. Neither ever does the other's job.

   THE PALETTE, and the one trap in it. --accent #E01E6E is a FILL. As text it
   is 4.09:1 on the ground and 3.75:1 on a tile, so it never carries a word:
   --accent-2 #F4649B does. base.css defaults .eyebrow to --accent, which is why
   .eyebrow is re-pointed below. White on the accent fill is 4.59:1, so anything
   printed on a solid accent surface is #FFFFFF (--accent-ink) and never --ink,
   which would be 4.44 and would fail.
   ========================================================================== */

:root{
  --g:5px;                       /* THE MORTAR — inside the three tiled moments only */
  --navh:62px;
  --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  --ground:var(--surface);
  --tile:var(--surface-2);
  --hair:var(--line);
  /* THE GROUND PALETTE (SITE-BUILD-POLICY.md 3.5, tonal stepping).
     Three neutral values plus the accent and one plaster surface. Nine
     sections used to share ONE #121212 and the page read as a single slab
     with nothing ranked; these step 9 L* between the darks, which is a
     visible boundary without a border, and the accent and the plaster are
     the two big moves. Every ink was re-checked against every ground: the
     worst pairing on the page is --ink-3 on gC at 4.88:1, still AA. */
  --gA:#0A0A0A;                  /* deepest.  prices, the asks, the close  */
  --gB:#121212;                  /* base.     the masthead                 */
  --gC:#2A2A2A;                  /* LIFTED.   the shop, find us            */
  --gD:#1A1A1A;                  /* the footer, half a step off the close  */
  /* THE STEP IS 14.3 L*, NOT 9. gC was #1F1F1F and every gate passed: the
     values differed, no two neighbours matched, and the page still read as one
     near-black slab below the fold because a 9 L* step between two very dark
     tones is measurably different and perceptually nothing. A numeric check on
     "are these equal" is the wrong check; the rule in SITE-BUILD-POLICY.md 3.5
     is that the boundary must be VISIBLE. #2A2A2A is 14.3 L* off #0A0A0A and
     is a boundary you can see. --ink-3 does NOT survive the move — 4.25:1 on
     gC — so secondary text on a lifted ground is re-pointed to --ink-2 below. */
  /* THE SCREEN FIGURE the masthead is built from, never read as taller than
     900px. On a 900px screen the cap changes nothing; on a taller one — or in
     any harness whose svh reports large — it is what holds the hero's two
     buttons above the fold instead of trusting the unit. */
  --hero-screen:min(100svh,900px);
}

body{font-size:16px;line-height:1.55;overflow-x:clip}
::selection{background:var(--accent);color:var(--accent-ink)}

/* --------------------------------------------------------- SECTION RHYTHM ---
   The breathing room the boxed version had none of. Matched to 01-nightwork so
   the two sit at the same pace, and restated at both desktop steps because
   base.css's media queries sit EARLIER in the built file and would otherwise
   win on equal specificity. */
/* TONAL STEPPING. A ground class per section, alternated so no two
   neighbours match. The order down the page is gB, accent, gA, gC, gA, gC,
   plaster, gA — checked on the render, not asserted here.

   TWO CLASSES, NOT ONE, AND THAT IS THE WHOLE POINT. `.gA` and `.crs` are
   both (0,1,0), so whichever is written LATER wins — and `.crs` carries a
   background because a section with no ground class still needs one. Written
   as bare classes these lost to it and the page rendered flat #121212 with
   only the footer stepping, which is the exact defect this block exists to
   remove. `.crs.gA` is (0,2,0) and cannot lose to it whatever the order. */
.crs{padding:46px 0;background:var(--gB)}
.crs.gA{background:var(--gA)}
.crs.gB{background:var(--gB)}
.crs.gC{background:var(--gC)}
.crs.gD{background:var(--gD)}
.crs.gAcc{background:var(--accent);color:var(--accent-ink)}
/* ON THE LIFTED GROUND the dim ink fails: #8C8C88 on #2A2A2A is 4.25:1. Every
   secondary string in a gC section takes --ink-2 (6.90:1) instead. Text sitting
   on a TILE inside a gC section is unaffected — a tile is #1B1B1B and the inks
   are contrasted against it exactly as they were designed. */
.gC .lbl,.gC .fact p,.gC .askcall,.gC .bfoot{color:var(--ink-2)}
@media(min-width:760px){.crs{padding:92px 0}}
@media(min-width:1180px){.crs{padding:124px 0}}
/* the two photographic feature moments run to the window edge; the container
   inside them still holds their heading. */
.crs.hero{padding:0}
.bleed{width:100%}

/* THE SECTION HEAD. One object, every section, so a heading always reads as a
   heading — the ranking the uniform grid destroyed. */
.shead{max-width:820px;margin:0 0 clamp(26px,3.4vw,48px)}
.shead:last-child{margin-bottom:0}
.eyebrow{font-family:var(--mono);font-size:10.5px;font-weight:700;letter-spacing:.22em;
  color:var(--accent-2);margin:0 0 14px}
.lead{font-size:16px;line-height:1.58;color:var(--ink-2);margin:0}
.lead-wide{max-width:62ch}
.h-lg{font-size:clamp(28px,4.4vw,52px);line-height:1.06;margin:0 0 16px}
.h-md{font-size:clamp(23px,3vw,36px);line-height:1.08;margin:0 0 14px}

/* THE MONO LABEL. With the boxes gone this is the single most repeated object
   on the page and the thing that still makes forty facts read as one system. */
.lbl{display:block;font-family:var(--mono);font-size:10px;font-weight:700;
  letter-spacing:.2em;text-transform:uppercase;color:var(--ink-3);margin:0 0 10px}
.mk{display:inline-flex;align-items:center;justify-content:center;
  font-family:var(--mono);font-weight:700;letter-spacing:.02em;
  width:30px;height:30px;background:var(--accent);color:var(--accent-ink);font-size:12px}
.mk.big{width:46px;height:46px;font-size:17px}

/* ---------------------------------------------------------- THE TILE GRID ---
   .wall / .brick, and they now appear in THREE places only. Kept exactly as
   they were inside those three: a flex course whose bricks carry a percentage
   basis in the wall's own column units, so a full row lands on the column
   rhythm and a short row grows to flush rather than leaving a gap. */
.wall{display:flex;flex-wrap:wrap;gap:var(--g);
  --h:calc(50% - var(--g) / 2);
  --t:calc(33.3333% - var(--g) * 2 / 3);
  --q:calc(25% - var(--g) * 3 / 4);
  --x:calc(16.6667% - var(--g) * 5 / 6);
  --tw:calc(66.6667% - var(--g) / 3)}
.brick{position:relative;flex:1 1 100%;min-width:0;
  background:var(--tile);padding:clamp(16px,2vw,26px)}
.brick > p:last-child{margin-bottom:0}

/* THE PHOTO TILE, and the designed empty cell.
   .hb-tile is HERO-SCOPED BY NAME and the orientation rule exempts the hero
   deliberately — it is the one art-directed course on the page. Below it the
   only frame is .wplate, written up in orientcheck-allow.txt.
   .wgap is what a masthead cell with nothing behind it becomes: mortar, drawn
   as a brick-shaped absence with a hairline round it. Never the same picture
   again. */
.herowall .hb-tile{padding:0;overflow:hidden;background:var(--tile)}
.herowall .hb-tile img{display:block;width:100%;height:100%;object-fit:cover}
.wgap{background:var(--gB);outline:1px solid var(--hair);outline-offset:-1px;
  padding:0;min-height:clamp(96px,12vw,150px)}

/* --------------------------------------------------------- preview bar ----
   Demo chrome, pinned above the masthead, with the masthead given a matching
   padding-top published as --tpl-h. No `top` and no `position` is declared on
   #nav anywhere in this file: the shared floor owns both. */
.tplbar{position:fixed;top:0;left:0;right:0;z-index:130;
  background:var(--tile);color:var(--ink-3);border-bottom:1px solid var(--hair);
  font-family:var(--mono);font-size:11px;line-height:1.5;letter-spacing:.02em;
  padding:9px var(--gutter);text-align:center}
.tplbar b{color:var(--accent-2)}
/* --tpl-h is RESERVED SPACE and errs large on purpose: if the reservation is
   short the bar covers the brand and the toggle. */
:root:has(.tplbar){--tpl-h:72px}
@media(min-width:760px){:root:has(.tplbar){--tpl-h:44px}}

/* -------------------------------------------------------------- masthead ---- */
#nav{background:var(--gB);padding-top:var(--tpl-h,0px)}
#nav.scrolled{border-bottom:1px solid var(--hair)}
.nav-inner{flex-wrap:wrap;gap:14px}
.brand{display:inline-flex;align-items:center;gap:10px;text-decoration:none;
  color:var(--ink);min-height:44px}
.brandtype{font-family:var(--mono);font-size:13px;font-weight:700;
  letter-spacing:.06em;text-transform:uppercase;white-space:nowrap}
.navtoggle .bars{display:block;width:22px}
.navtoggle .bars i{display:block;height:2px;margin:4px 0;background:var(--ink)}
.navtoggle .bars i:nth-child(2){width:14px;background:var(--accent-2)}
.nav-links{flex:1 0 100%;flex-direction:column;align-items:stretch;gap:2px;
  width:100%;padding:0 0 12px}
.nav-links a{font-family:var(--mono);font-size:12px;font-weight:700;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-2);text-decoration:none;
  display:flex;align-items:center;min-height:46px;border-bottom:1px solid var(--hair)}
.nav-links a:hover{color:var(--accent-2)}
.nav-links .drawtel{color:var(--accent-2)}
.navcta{background:var(--accent);color:var(--accent-ink) !important;
  justify-content:center;border-bottom:0 !important;margin-top:8px}
@media(min-width:940px){
  .nav-inner{flex-wrap:nowrap;gap:8px}
  .nav-links{flex:0 0 auto;width:auto;padding:0;gap:4px;flex-direction:row}
  .nav-links a{min-height:38px;font-size:11px;letter-spacing:.13em;padding:0 12px;
    border-bottom:0}
  .nav-links .drawtel{display:none}
  .navcta{margin-top:0;padding:0 16px}
}
@media(min-width:1200px){.nav-links .drawtel{display:flex}}

/* ============================================================ 1. THE MASTHEAD WALL
   FULL BLEED. HERO ARCHETYPE 5. The first screen IS the wall: six columns,
   four rows, filling the screen exactly. 6 cells of speech + 4 of plate + 2 of
   odometer + 12 single frames = 24.

   WHY THE ROWS ARE DECLARED AS TRACKS. As bare flex lines the courses had no
   declared share of the screen and align-content:stretch handed each line half
   of whatever spare height existed, carrying the speech brick's buttons 87px
   BELOW the fold at 1440x900. The rows are explicit fr tracks on a fixed
   1.55 : 1.55 : 1 : 1 share, so the buttons sit at a known fraction of the
   screen. min-height, not height, so nothing is ever clipped; --hero-screen
   bounds the floor. */
.hero{padding-top:calc(var(--navh) + var(--tpl-h,0px))}
.herowall{align-items:stretch}
.herowall .hb-tile,.herowall .wgap{flex:1 1 var(--t);aspect-ratio:1 / 1}
/* THE PHONE FLOW. On the six-column desktop grid the two single frames at c6
   must precede the odometer in the DOM or auto-placement leaves r1c6 and r2c6
   empty. That same order on a phone put two frames on a line of their own and
   broke the remaining ten into rows of 3,3,3,1. Reordering the odometer ahead
   of the frames here leaves TWELVE — four flush rows of three. Order only, so
   the desktop placement that needs the DOM sequence is untouched. */
@media(max-width:639px){
  .herowall .hb-say{order:0}
  .herowall .hb-plate{order:1}
  .herowall .hb-odo{order:2}
  .herowall .hb-tile,.herowall .wgap{order:3}
}
@media(min-width:640px){
  .herowall{display:grid;grid-template-columns:repeat(4,minmax(0,1fr))}
  .herowall .hb-say{grid-column:span 4}
  .herowall .hb-plate{grid-column:span 2}
  .herowall .hb-odo{grid-column:span 2}
  .herowall .hb-tile,.herowall .wgap{grid-column:span 1;aspect-ratio:1 / 1}
}
@media(min-width:1000px){
  .hero .herowall{grid-template-columns:repeat(6,minmax(0,1fr));
    grid-template-rows:minmax(min-content,1.55fr) minmax(min-content,1.55fr)
                       minmax(min-content,1fr) minmax(min-content,1fr);
    min-height:calc(var(--hero-screen) - var(--navh) - var(--tpl-h,0px))}
  .herowall .hb-say{grid-column:span 3;grid-row:span 2}
  .herowall .hb-plate{grid-column:span 2;grid-row:span 2}
  .herowall .hb-odo{grid-column:span 2;grid-row:span 1}
  /* THE FRAME TAKES ITS HEIGHT FROM THE ROW and must contribute NOTHING to
     sizing it. The tracks are minmax(min-content, Nfr) and an in-flow <img>
     reports its scaled intrinsic height as min-content — a 720x900 plate in a
     234px column claimed 292px, four rows claimed 1,168, and the masthead
     measured 2,002px against a 900px screen. Out of flow, its min-content is
     zero and the rows are sized by the speech brick and the fr share alone. */
  .herowall .hb-tile,.herowall .wgap{grid-column:span 1;aspect-ratio:auto;min-height:0}
  .herowall .hb-tile img{position:absolute;inset:0}
}

/* THE SPEECH BRICK — the one solid accent surface above the fold. Everything on
   it is pure white: --ink is 4.44:1 here and would fail. */
.herowall .hb-say{background:var(--accent);color:var(--accent-ink);
  display:flex;flex-direction:column;justify-content:center;
  padding:clamp(20px,3.4vw,44px)}
.herowall .hkick{font-family:var(--mono);font-size:10.5px;font-weight:700;
  letter-spacing:.22em;text-transform:uppercase;color:var(--accent-ink);margin:0 0 16px}
.herowall h1{font-size:clamp(31px,7.4vw,66px);line-height:1.22;
  color:var(--accent-ink);margin:0}
/* THE DRAWN MOMENT — THE INSET. The emphasised phrase is painted in the ground
   colour, so it becomes a smaller brick set into the brick it sits in: same
   hard corners, same mortar showing at its edge. box-decoration-break:clone
   keeps it whole across a line break, and the h1's line-height is opened to
   1.22 because an inline painted box is as tall as the ascent plus the descent
   and at 1.08 would overprint the line above it. */
.herowall h1 .inset{font-style:normal;background:var(--ground);color:var(--accent-ink);
  padding:.02em .14em;margin-left:-.14em;
  -webkit-box-decoration-break:clone;box-decoration-break:clone}
.herowall .hsub{font-size:15.5px;line-height:1.5;color:var(--accent-ink);
  margin:18px 0 0;max-width:46ch}
.herowall .btnrow{margin-top:22px}
.herowall .btn{border-radius:0;font-family:var(--mono);font-size:11.5px;font-weight:700;
  letter-spacing:.14em;text-transform:uppercase;padding:14px 20px}
.herowall .btn-primary{background:var(--ground);color:var(--accent-ink);border-color:var(--ground)}
.herowall .btn-primary:hover{background:#000;border-color:#000}
.herowall .btn-ghost{background:transparent;color:var(--accent-ink);
  border-color:rgba(255,255,255,.7)}
.herowall .btn-ghost:hover{background:rgba(255,255,255,.14)}

/* THE PLATE. Hero-scoped: the one art-directed frame on the page, exempt from
   the orientation rule by name.
   TWO CROPS, TWO FOCAL POINTS, and the breakpoint is the <source> element's
   own. hero-tall is a 0.52:1 portrait in a 1.45:1 box on a phone, so it shows
   one horizontal band and WHICH band is the whole decision. At 42% — the wide
   plate's focal — that band was solid black barber's cape and the subject was
   not in frame at all. 10% puts the face and the scissors in it. */
.herowall .hb-plate{padding:0;overflow:hidden;aspect-ratio:16 / 11}
.herowall .hb-plate picture{display:block;width:100%;height:100%}
.herowall .hb-plate img{width:100%;height:100%;object-fit:cover;object-position:50% 10%}
@media(min-width:860px){.herowall .hb-plate img{object-position:50% 42%}}
@media(min-width:640px){.herowall .hb-plate{aspect-ratio:auto;min-height:clamp(200px,26vw,340px)}}

.herowall .hb-odo{display:flex;flex-direction:column;justify-content:space-between;
  min-height:clamp(118px,14vh,180px)}
.herowall .odolink{font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.16em;
  text-transform:uppercase;color:var(--ink-3);text-decoration:none;margin-top:10px}
.herowall .odolink:hover{color:var(--accent-2)}
.herowall .odoask{font-size:14px;line-height:1.5;color:var(--ink-2);margin:0}
@media(min-width:1000px){
  /* THE HEADLINE CAP IS WIDTH-DRIVEN FROM 1000px UP. 66px is right in a 717px
     speech brick and two lines too many in the 497px one a 1000px screen gives
     it. 4.6vw reaches the same 66px at 1440. THIS LIVES HERE, BELOW
     `.herowall h1`: both rules are (0,1,1), so the later one wins and a media
     query does not change that. */
  .herowall h1{font-size:clamp(31px,4.6vw,66px)}
}

/* ================================================================ THE ODOMETER
   THE SIGNATURE, and it is 04's alone. A digit is a column of ten glyphs
   clipped to exactly one line-height; rolling it to `d` is a translateY of
   -d*10%, and the columns are released 90ms apart so the figure lands left to
   right like a counter coming to rest. template.js builds the columns; if it
   never runs — no JS, or prefers-reduced-motion — nothing is built and the
   figure is simply printed, which is why the markup carries the plain number. */
.od{display:inline-flex;align-items:flex-end;font-variant-numeric:tabular-nums}
.od-d{display:block;overflow:hidden;height:1em;line-height:1}
.od-d b{display:block;font-weight:inherit;line-height:1;transform:translateY(0)}
.od-s{display:block;line-height:1}
html.js .od-roll .od-d b{transition:transform 820ms cubic-bezier(.16,.84,.28,1)}
.odofig{font-family:var(--mono);font-weight:700;font-size:clamp(46px,10vw,80px);
  line-height:1;letter-spacing:-.04em;color:var(--accent-2);margin:0}

/* ================================================================ 2. THE FIGURE
   THE ACCENT GROUND, spent once, on the one hard checkable fact on the page.

   THE RIM IS OFF, and this is why. rating.css draws
   `.rating-fig::after { box-shadow: inset 1px 1px 0 var(--rating-rim) }` — a
   1px shadow offset one pixel right and one pixel down, so it paints the TOP
   and LEFT edges only. That is a rim highlight for a raised dark card on a
   dark ground, which is what 01-nightwork uses it as. Here the figure sits
   directly on a flat magenta field with no card behind it, so the same rule
   drew a two-sided line that framed nothing and cut across the block. Setting
   --rating-rim to transparent kills it. The pill reads the same token, so its
   border is re-declared explicitly below or it would vanish with the rim.

   THE HIERARCHY, loudest to quietest, because one flat field of white at one
   weight was the actual complaint:
     1. THE FIGURE      up to 210px, pure white, the loudest thing here
     2. the stars       52px, white on a translucent white track
     3. the count line  up to 26px — deliberately BELOW the figure's scale
     4. proof.body      14px, narrow measure, still pure white
     5. THE REVIEWS     15px on their OWN darker band, a 12 L* step down
   Nothing recedes by losing contrast. White is 4.58:1 on the accent and --ink
   is 4.16:1 and fails, so every word on the accent stays pure white and the
   quiet is made with size, measure and a stepped ground instead. */
.rated{--rating-bg:transparent;--rating-shadow:none;
  --rating-rim:transparent;--rq-bg:transparent;--rq-ink:#fff;
  --reviewband:#AA1754}
.rated .eyebrow{color:rgba(255,255,255,.9)}
.rated .h-lg{color:#fff}
/* proof.body, demoted. Small and narrow, never dimmed. */
.rated .rsub{margin:0;max-width:52ch;font-size:14px;line-height:1.5;color:#fff}
.rated .shead{margin-bottom:clamp(30px,3.6vw,52px)}
.rated .rating{margin:0}
.rated .rating-fig{padding:0;border:0;box-shadow:none}
.rated .rating-fig::after{content:none}
/* THE FIGURE — the loudest object in the section */
.rated .rnum{font-family:var(--mono);letter-spacing:-.06em;color:#fff;
  font-size:clamp(96px,17vw,210px);line-height:.78;margin-top:4px}
.rated .rstars{font-size:clamp(30px,4.4vw,52px)}
.rated .rs-on{color:#fff}
.rated .rs-off{color:rgba(255,255,255,.34)}
/* the count line sits UNDER the figure's scale on purpose */
.rated .rsay{font-size:clamp(16px,1.9vw,26px);color:#fff;max-width:18ch}
.rated .rsay .rcount{color:#fff}
.rated .gmpill{border-radius:0;background:rgba(255,255,255,.16);
  border:1px solid rgba(255,255,255,.6)}
.rated .gmpill:hover{background:rgba(255,255,255,.26)}
.rated .gmpill .gmgo,.rated .gmpill:hover .gmgo{color:#fff}

/* THE REVIEWS — a band of their own, 12 L* below the accent, same hue. They
   support the figure; they do not compete with it. Smaller type, a quieter
   white, and a ground that visibly steps so they read as a distinct block
   rather than more of the same field. White at .88 on this band is 5.76:1 and
   the captions at .8 are 5.00:1 — both clear of AA, so "quiet" costs nothing. */
.rated .rquotes{max-width:none;margin:clamp(34px,4.4vw,56px) 0 0;
  display:grid;gap:clamp(18px,2.2vw,30px);grid-template-columns:1fr;
  background:var(--reviewband);padding:clamp(22px,3vw,34px)}
.rated .rq{padding:0;border:0;background:none}
.rated .rq blockquote{font-family:var(--font-display);font-size:15px;line-height:1.5;
  margin:0 0 9px;color:rgba(255,255,255,.88)}
.rated .rq figcaption{margin:0;font-family:var(--mono);font-size:9.5px;letter-spacing:.16em;
  text-transform:uppercase;color:rgba(255,255,255,.8)}
.rated .rq-empty{border:1px dashed rgba(255,255,255,.5);padding:16px}
@media(min-width:760px){.rated .rquotes{grid-template-columns:repeat(3,1fr);
  gap:clamp(22px,2.6vw,38px)}}

/* ================================================================= 3. THE BOARD
   CONTAINED, and the ONE tiled grid below the masthead. A price board IS a grid
   of cells and reading it as one is the point — this is the texture doing a job
   rather than repeating itself. The shared component's own containers are
   display:contents so each .bitem is a tile in the course directly and lands on
   the wall's rhythm rather than inside a panel.
   Every selector is scoped to .wboard: base.css owns these class names and a
   bare rule would redefine the services section for every build in the set. */
.wboard .board,.wboard .bgroup,.wboard .bgrid{display:contents}
.wboard .bhook{flex:1 1 100%;order:-2;border-color:var(--hair);background:var(--tile);
  padding:clamp(15px,1.7vw,22px)}
.wboard .bhook-l{font-family:var(--mono);letter-spacing:.18em}
.wboard .bgh{flex:1 1 100%;order:-1;font-family:var(--mono);letter-spacing:.18em;
  background:var(--tile);padding:clamp(12px,1.2vw,16px);margin:0}
.wboard .bitem{flex:1 1 100%;background:var(--tile);border-radius:0;
  padding:clamp(15px,1.7vw,22px)}
.wboard .bitem::before{height:3px}
.wboard .bitem.noprice::before{background:var(--hair)}
.wboard .bname{font-family:var(--font-display);font-size:17px;letter-spacing:-.012em}
.wboard .bdesc{font-size:14px;line-height:1.5;color:var(--ink-3)}
.wboard .btag{border-radius:0;border-color:var(--hair);font-family:var(--mono);
  font-size:10px;letter-spacing:.16em;color:var(--ink-3)}
.wboard .bprice{font-family:var(--mono);font-size:clamp(19px,2vw,26px);
  letter-spacing:-.03em;color:var(--accent-2)}
@media(min-width:640px){.wboard .bitem{flex-basis:var(--h)}}
@media(min-width:1000px){.wboard .bitem{flex-basis:var(--t)}}
.bfoot{margin:clamp(20px,2.4vw,30px) 0 0;font-family:var(--mono);font-size:11px;
  font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-3)}

/* ================================================================== 4. THE SHOP
   GROUND gC. THE TILED COURSE — this template's own idiom, which this section
   had stopped using. It was a heading, two large photographs and a three-column
   list: a shape any template could produce, unchanged for four passes.

   It is a 3 x 2 course of tiles at the wall's 5px mortar now, and the words are
   IN the wall rather than above it — the masthead's logic at a smaller scale.
   Row one: a speech tile beside two photo tiles at the same footprint, the
   photographs stretched by the row so all three tiles are one object. Row two:
   three fact tiles.

   THE TILES ARE DARK WELLS ON A LIFTED GROUND, inverting the masthead. That is
   deliberate twice over: it makes the section read differently from the top of
   the page, and it keeps every ink inside a tile at the contrast it was
   designed against (#1B1B1B) rather than against the lifted ground. */
.shopwall{display:grid;gap:var(--g);grid-template-columns:1fr}
@media(min-width:760px){.shopwall{grid-template-columns:repeat(3,1fr)}}
.sw-say{display:flex;flex-direction:column;justify-content:center;
  padding:clamp(20px,2.4vw,32px)}
.sw-say .eyebrow{margin-bottom:12px}
.sw-say .h-md{margin-bottom:12px}
.sw-lead{margin:0;font-size:15px;line-height:1.55;color:var(--ink-2)}
/* ------------------------------------------------------------- THE FRAME ---
   .wplate is THE photographic frame below the masthead — the shop's two tiles
   and the three that close the asks. It lives here, above its first user,
   because the block that used to define it was the one the shop rewrite
   replaced: every frame on the page silently lost its box, the images rendered
   at natural size and two sections doubled in height before the render caught
   it. A shared definition does not belong inside one section's block. */
.wplate{margin:0;padding:0;overflow:hidden;background:var(--tile);aspect-ratio:4 / 3}
.wplate img{display:block;width:100%;height:100%;object-fit:cover}

/* the photo tiles reuse .wplate, so they inherit its declared proportion and
   its written-up allowance rather than introducing another photographic
   selector. In the 3-column course the row stretches them past it. */
.sw-shot{margin:0}
.sw-fact{padding:clamp(18px,2vw,26px)}
.sw-fact h3{font-family:var(--font-display);font-size:16.5px;line-height:1.24;
  letter-spacing:-.012em;margin:0 0 7px}
.sw-fact p{font-size:14.5px;line-height:1.55;color:var(--ink-3);margin:0}

/* ================================================================= 6. THE ASKS
   CONTAINED, NO TILES. A stacked list: each question separated from the next by
   a hairline and nothing else. This was a 2x2 of identical bordered cells and
   looked exactly like the gallery — which is the defect this pass exists to
   remove. The grid echo is the mono plus-mark and the hard corner of the open
   state, not a box. */
.asklist{margin-top:clamp(26px,3vw,40px);border-top:1px solid var(--hair)}
.ask{border-bottom:1px solid var(--hair)}
.ask summary{display:flex;align-items:flex-start;gap:16px;cursor:pointer;
  list-style:none;padding:20px 0;min-height:56px;
  font-family:var(--font-display);font-size:17px;font-weight:700;letter-spacing:-.01em}
.ask summary::-webkit-details-marker{display:none}
.ask summary:hover{color:var(--accent-2)}
.ask summary span{flex:1 1 auto}
.ask summary i{flex:none;width:12px;height:12px;margin-top:7px;position:relative}
.ask summary i::before,.ask summary i::after{content:"";position:absolute;
  background:var(--accent-2);left:0;top:5px;width:12px;height:2px}
.ask summary i::after{transform:rotate(90deg);transition:transform .22s ease}
.ask[open] summary i::after{transform:rotate(0deg)}
.ask > p{margin:0;padding:0 0 22px;max-width:74ch;
  font-size:15px;line-height:1.62;color:var(--ink-2)}
.askcall{margin:0;font-size:14.5px;color:var(--ink-3)}
.askcall a{color:var(--accent-2)}
@media(min-width:900px){
  /* the heading beside the list, not stacked above it — the one composition on
     the page that is asymmetric, so the section cannot be mistaken for another.
     Scoped to the FIRST container: the strip below is its own container and must
     not be dragged into the two-column grid. */
  .asks .container:first-of-type{display:grid;grid-template-columns:.8fr 1.2fr;
    gap:clamp(34px,4vw,60px);align-items:start}
  .asks .shead{margin-bottom:0}
  .asklist{margin-top:0}
}
/* THE STRIP — three frames at the wall's own mortar, closing the section.
   The bottom third had no imagery at all: the page read as photography down to
   the gallery and as a text document from there to the footer, and that half is
   the one that kept getting left alone. This is the texture coming back. It
   reuses .wplate, so it inherits the frame's declared proportion and its
   allowance rather than introducing a second photographic selector. */
.askstrip{display:flex;flex-wrap:wrap;gap:var(--g);align-items:stretch;
  margin-top:clamp(30px,3.6vw,50px)}
.askstrip .wplate{flex:1 1 calc(33.3333% - 3.34px)}
@media(max-width:699px){.askstrip .wplate{flex-basis:calc(50% - 2.5px)}
  .askstrip .wplate:last-child{flex-basis:100%}}

/* ================================================================== 6. FIND US
   GROUND gC. THE IMAGE HALF. The photograph was one of four equal columns —
   same width as a text column, no relationship to anything, which is what made
   it read as a stray portrait rather than a decision. It is half the section
   now: full height of the content beside it, cover-cropped by the row, and the
   address, the map, the week and the two ways to reach them are stacked in the
   other half.
   It declares no proportion at this width — the row sets its box — so the only
   thing written up in orientcheck-allow.txt is the phone case, where there is
   no sibling to take a height from. */
.findgrid{display:grid;gap:var(--g);grid-template-columns:1fr}
@media(min-width:900px){.findgrid{grid-template-columns:.9fr 1.1fr;align-items:stretch}}
.fu-shot{position:relative;margin:0;padding:0;overflow:hidden;background:var(--tile)}
.fu-shot img{display:block;width:100%;height:100%;object-fit:cover;object-position:50% 45%}
@media(max-width:899px){.fu-shot{aspect-ratio:16 / 9}}
/* THE FRAME MUST CONTRIBUTE NOTHING TO THE ROW IT IS STRETCHED BY, and this is
   the second time this exact trap has been paid for on this template. `height:
   100%` against a parent with no definite height resolves to AUTO, so the image
   reports its own scaled intrinsic height as the column's content: a 660x900
   plate in a 485px column claimed 661px and the section measured 1,027px when
   the information beside it needed 425. Out of flow, its min-content is zero
   and the row is sized by the address and the hours, which is the whole idea of
   putting the picture in the row. The masthead frames carry the identical rule
   for the identical reason. */
@media(min-width:900px){
  .fu-shot{min-height:0}
  .fu-shot img{position:absolute;inset:0}
}
.fu-info{display:grid;gap:var(--g);grid-template-columns:1fr}
/* THE HOURS COLUMN GETS THE WIDER SHARE. At an even 1fr 1fr the day and the
   time both wrapped — "TUESDAY -" / "FRIDAY" over "9:00am -" / "7:00pm" — because
   a tile's own padding takes 52px out of a 295px column and the row needs 226.
   1.2 : .8 gives it 287px of content and the rows sit on one line. */
@media(min-width:560px){.fu-info{grid-template-columns:1.2fr .8fr}
  .fu-where{grid-column:1 / -1}}
.fu-where,.fu-open,.fu-reach{background:var(--tile);padding:clamp(16px,1.7vw,22px)}
.findus .dh{text-transform:uppercase;margin:0}
.street{font-size:16.5px;line-height:1.5;color:var(--ink);margin:0 0 16px}
.maplink{display:inline-flex;align-items:center;gap:12px;text-decoration:none;
  border:1px solid var(--hair);padding:10px 14px}
.maplink span{font-family:var(--mono);font-size:10.5px;font-weight:700;letter-spacing:.14em;
  text-transform:uppercase;color:var(--accent-2)}
.maplink:hover{border-color:var(--accent-2)}
.areaLine{margin:18px 0 0;font-size:14px;line-height:1.5;color:var(--ink-3)}
.areaLine .lbl{margin-bottom:6px}
.hourlist{margin:0}
.hrow{display:flex;justify-content:space-between;align-items:baseline;gap:16px;
  padding:10px 0;border-top:1px solid var(--hair)}
.hrow:last-child{border-bottom:1px solid var(--hair)}
.hrow dt{font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-3);margin:0}
.hrow dd{margin:0;font-size:15px;color:var(--ink)}
.fu-reach a{display:block;font-family:var(--mono);font-weight:700;color:var(--accent-2);
  text-decoration:none;letter-spacing:-.02em}
.fu-reach a:hover{color:var(--ink)}
.reachtel{font-size:clamp(18px,1.7vw,22px);margin-bottom:8px}
/* the address is the longest unbreakable string on the page and the reach tile
   is the narrowest column it lands in; at 13px it broke after the dot. */
.reachmail{font-size:11.5px;letter-spacing:0;overflow-wrap:anywhere;line-height:1.45}

/* ================================================================= 7. THE CLOSE
   GROUND gA, and it stays in the dark world. It used to invert to a cream
   plaster panel — the one light surface on the page — and at the very end that
   read as a different website rather than as an arrival.

   AND IT IS MUCH TIGHTER. It was 698px, most of it nothing: a half-empty column
   holding three lines of copy, full-height fields and the section's standard
   124px of padding top and bottom. The copy column is narrower, the fields are
   shorter, the textarea is 100px instead of 140, and this section takes less
   vertical padding than the rest of the page — the close is the one place a
   visitor is trying to DO something rather than read. */
.close{padding-top:clamp(38px,5vw,76px);padding-bottom:clamp(38px,5vw,76px)}
.closegrid{display:grid;gap:clamp(24px,3vw,44px);grid-template-columns:1fr}
@media(min-width:820px){.closegrid{grid-template-columns:.72fr 1.28fr;align-items:start}}
.cleft .lead{font-size:15px;max-width:34ch}
.close .field{gap:5px}
.close .field label{font-size:11.5px;color:var(--ink-3)}
/* the fields lift off the deepest ground rather than sinking into it — #1B1B1B
   on #0A0A0A is a visible well, where base.css's --surface fill would have been
   #121212 on #0A0A0A and all but invisible. */
.close .field input,.close .field select,.close .field textarea{
  min-height:46px;padding:11px 14px;background:var(--tile);border-color:var(--hair);
  color:var(--ink)}
.close .field textarea{min-height:100px}
.close .field input:focus,.close .field select:focus,.close .field textarea:focus{
  border-color:var(--accent-2)}
.close .frow{gap:var(--g)}
.close .form{display:grid;gap:var(--g)}
.close .fineprint{margin:0;font-size:12px;line-height:1.5;color:var(--ink-3)}
.close .btn-primary{border-radius:0;font-family:var(--mono);font-size:11.5px;
  font-weight:700;letter-spacing:.14em;text-transform:uppercase;padding:14px 20px}
.close .thanks{background:var(--tile);border:1px solid var(--hair);color:var(--ink)}
.close .thanks h3{color:var(--ink);font-size:21px}
.close .thanks p{color:var(--ink-2);margin:0}

/* ==================================================================== FOOTER
   CONTAINED, and ranked by type rather than boxed. */
/* the footer is gA, so it steps down from the plaster above it — 93 L*, the
   biggest change on the page, and the reason it needs no top border at all */
footer{background:var(--gD);padding:clamp(36px,4vw,60px) 0 clamp(26px,2.6vw,38px)}
.fgrid{display:grid;gap:clamp(28px,3.4vw,48px);grid-template-columns:1fr}
@media(min-width:760px){.fgrid{grid-template-columns:1.3fr .7fr}}
.fname{font-family:var(--font-display);font-size:22px;font-weight:700;
  letter-spacing:-.02em;margin:16px 0 8px}
.fabout{font-size:14.5px;line-height:1.55;color:var(--ink-3);margin:0;max-width:46ch}
.fh{font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.2em;
  text-transform:uppercase;color:var(--ink-3);margin:0 0 14px}
.freach{display:block;color:var(--ink);font-size:16px;text-decoration:none;margin-bottom:6px}
.freach:hover{color:var(--accent-2)}
footer address{font-style:normal;font-size:14.5px;color:var(--ink-3);margin-top:10px}
.fnav{display:flex;flex-wrap:wrap;gap:8px 26px;margin:clamp(30px,3.4vw,46px) 0 0;
  padding-top:22px;border-top:1px solid var(--hair)}
.fnav a{font-family:var(--mono);font-size:10.5px;font-weight:700;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-2);text-decoration:none}
.fnav a:hover{color:var(--accent-2)}
.fbase{display:flex;flex-wrap:wrap;gap:6px 18px;justify-content:space-between;
  margin:22px 0 0;font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;
  color:var(--ink-3)}
.fbase a{color:var(--ink-2)}

/* ======================================================================= 404 */
.nf{padding-top:calc(var(--navh) + var(--tpl-h,0px) + 40px)}
.nf-say{flex:1 1 420px;padding:clamp(20px,2.6vw,38px)}
.nf-say .lbl{font-size:12px;color:var(--accent-2)}
.nf-gap{flex:1 1 180px;background:var(--gB);min-height:150px;
  outline:1px solid var(--hair);outline-offset:-1px}
.nf-fact{flex:1 1 180px;display:flex;flex-direction:column;justify-content:space-between;
  min-height:150px}
.nf-fact .fbig{font-size:15px;line-height:1.4;color:var(--ink);margin:0}
.nf-call{text-decoration:none}
.cbig{font-family:var(--mono);font-weight:700;font-size:clamp(19px,2.2vw,28px);
  letter-spacing:-.02em;color:var(--accent-2);text-decoration:none;display:block}
.nf-call:hover .cbig{color:var(--ink)}

/* ====================================================================== MOTION
   THE BLOCK REVEALS, NOT THE PAGE.

   What this replaces: every section carried the shared .reveal class, and
   shared/app.js arms a blanket four-second timer that shows EVERY .reveal at
   once regardless of scroll position. That timer is correct for what it was
   written for — a background tab never receives IntersectionObserver callbacks
   and content must never be permanently invisible — but on a long page it meant
   that reading the masthead for four seconds, which is what a masthead is for,
   revealed the entire document before it was ever scrolled to.

   So blocks here are .wrv, this template owns them, and nothing else touches
   them. One observer entry per block, unobserved the moment it fires.

   THE MOTION CONTRACT IS STRICTER THAN THE FLOOR, not looser: hiding is gated
   on html.js AND html.wrv-on, and template.js adds wrv-on one statement before
   anything can be hidden. The script that hides is the script that reveals, so
   a template.js that 404s, fails to parse or is blocked leaves every block
   visible. Keying only on html.js would have made that file the one thing on
   the page able to blank the whole document. */
html.js.wrv-on .wrv > *{opacity:0;transform:translateY(14px)}
html.js.wrv-on .wrv.in > *{opacity:1;transform:none;
  transition:opacity .5s cubic-bezier(.22,.61,.36,1) var(--wd,180ms),
             transform .5s cubic-bezier(.22,.61,.36,1) var(--wd,180ms)}
html.js.wrv-on .wrv.in > :nth-child(1){--wd:0ms}
html.js.wrv-on .wrv.in > :nth-child(2){--wd:26ms}
html.js.wrv-on .wrv.in > :nth-child(3){--wd:52ms}
html.js.wrv-on .wrv.in > :nth-child(4){--wd:78ms}
html.js.wrv-on .wrv.in > :nth-child(5){--wd:104ms}
html.js.wrv-on .wrv.in > :nth-child(6){--wd:130ms}
html.js.wrv-on .wrv.in > :nth-child(7){--wd:156ms}
@media(prefers-reduced-motion:reduce){
  html.js.wrv-on .wrv > *,html.js.wrv-on .wrv.in > *{opacity:1 !important;transform:none !important;
    transition:none !important}
  html.js .od-roll .od-d b{transition:none !important}
}


/* ==================================================== THE MOSAIC COMPONENT ===
   One component, ten hardcoded layouts (see mosaic.py). The layout decides every
   tile's column span and row span; nothing is computed from the photographs, and
   nothing is randomised at runtime. Each layout is pre-validated so its tiles fill
   the 12-column grid exactly — no gaps, no orphan hanging below the rest.

   The tile is the shape. The picture fills it (cover), so any source aspect crops
   cleanly and the wall is a rectangle whatever the client sends.

   Row height is a clamp, so a twelve-tile layout is a wall and not a scroll. */

.mos{display:grid;grid-template-columns:repeat(12,1fr);
  grid-auto-rows:clamp(96px,13.5vw,190px);
  grid-template-rows:repeat(var(--mos-rows,3),clamp(96px,13.5vw,190px));
  gap:var(--mos-gap,10px)}

.mos-t{position:relative;overflow:hidden;border-radius:var(--radius,3px);
  background:var(--surface-2,#171512);margin:0;min-width:0}
.mos-t img{display:block;width:100%;height:100%;object-fit:cover}

/* a caption sits on the tile, so it never changes the tile's height */
.mos-cap figcaption{position:absolute;left:0;right:0;bottom:0;padding:26px 14px 11px;
  display:flex;flex-direction:column;gap:1px;pointer-events:none;
  background:linear-gradient(180deg,rgba(9,8,7,0),rgba(9,8,7,.82))}
.mos-cap figcaption b{font-size:13.5px;font-weight:600;color:#F0EAE0;line-height:1.25}
.mos-cap figcaption span{font-size:11px;letter-spacing:.11em;text-transform:uppercase;
  color:rgba(240,234,224,.72)}

/* a text tile is just another tile — same grid maths, no special case */
.mos-text{display:flex;flex-direction:column;justify-content:center;
  padding:clamp(14px,1.6vw,26px);
  background:var(--mos-text-bg,var(--surface-2,#171512))}
.mos-text b{display:block;margin-bottom:7px;font-family:var(--font-display,inherit);
  font-size:clamp(15px,1.5vw,21px);font-weight:600;line-height:1.2;
  color:var(--ink,#F0EAE0)}
.mos-text p{margin:0;font-size:clamp(12.5px,1.05vw,15px);line-height:1.5;
  color:var(--ink-2,#B8AE9E)}

/* THE PHONE. A twelve-column mosaic is unreadable at 390px, but stacking every tile
   full width is not the answer either: it turned an eight-tile wall into 2,071px of
   scroll and pushed whole pages past the height ceiling. It also stops being a mosaic.

   So the phone gets a TWO-column mosaic. Same tiles, same order, square cells - the wall
   is still a wall and the height roughly quarters.

   THE ODD-COUNT ORPHAN, which is the thing Jon actually objected to: with two columns an
   odd number of tiles leaves the last one alone on its row. `:last-child:nth-child(odd)`
   is true only when the tile is BOTH last and at an odd position - i.e. exactly when the
   count is odd - and widening it to both columns closes the block. Even counts are
   already flush, and the rule cannot fire on them. */
@media(max-width:699px){
  .mos{grid-template-columns:repeat(2,1fr);grid-template-rows:none;
    grid-auto-rows:auto;gap:var(--mos-gap-sm,8px)}
  .mos-t{grid-column:auto/span 1 !important;grid-row:auto !important;
    aspect-ratio:var(--mos-sm-ar,1/1)}
  .mos-t:last-child:nth-child(odd){grid-column:auto/span 2 !important;
    aspect-ratio:var(--mos-sm-ar-wide,16/9)}
  .mos-text{aspect-ratio:auto !important;min-height:132px}
  .mos-cap figcaption{padding:20px 10px 9px}
  .mos-cap figcaption b{font-size:12.5px}
}

/* Very narrow phones: two columns of a 12-photo wall get tight, but one column is still
   worse than a small tile, so the columns hold and only the gap tightens. */
@media(max-width:359px){ .mos{gap:6px} }


/* ============================================ THE PITTSBURGH IT BLURB (bottom) ===
   Jon, 2026-08-14. First the three how-it-works boxes were flagged - "this isn't an
   actual part of their site" - then: "wait, just remove it... maybe just put a blurb
   all the way at the bottom saying what those 3 boxes say, and put it in our branding."

   So the three boxes are GONE from every skeleton, and what they said is now one
   branded block below the footer. It is the only place on the page that is Pittsburgh
   IT speaking rather than the shop, and it looks like it: our gold rule, our name, our
   contact. A prospect can tell in one glance which words are theirs and which are ours.

   Shared, and loaded after every template sheet, so it cannot drift per skeleton. */

.pghblurb{
  /* PITTSBURGH IT GOLD, hardcoded on purpose. Using var(--accent-2) inherited the
     CLIENT's colour, so the block that is supposed to read as US came out in THEIR
     brand - Roberto's printed it orange. Nothing in here may take a client token. */
  border-top:3px solid #e3c05a;
  background:#0E0D0C;color:#B8AE9E;
  padding:26px 0 30px;font-size:14px;line-height:1.62}
.pghblurb .pgb{max-width:min(1160px,92vw);margin:0 auto}
.pghblurb .pgb-mark{
  margin:0 0 7px;font-size:11px;font-weight:700;letter-spacing:.16em;
  text-transform:uppercase;color:#e3c05a}
.pghblurb .pgb-body{margin:0;max-width:64ch;color:#CFC6B8}
.pghblurb .pgb-body b{color:#F0EAE0;font-weight:600}
.pghblurb .pgb-line{
  margin:13px 0 0;font-size:13px;color:#8E8677;
  display:flex;gap:9px;flex-wrap:wrap;align-items:baseline}
.pghblurb .pgb-line a{color:#e3c05a;text-decoration:none;border-bottom:1px solid rgba(227,192,90,.4)}
.pghblurb .pgb-line a:hover{border-bottom-color:#e3c05a}
.pghblurb .pgb-sep{opacity:.45}
@media(max-width:699px){
  .pghblurb{padding:22px 0 26px;font-size:13.5px}
  .pghblurb .pgb-line{gap:6px}
}
