/* ============================================================================
   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:#FFFFFF;
  --surface-2:#F5F4F1;
  --band:#0E0E0E;
  --band-ink:#FFFFFF;
  --band-ink-2:#B9B9BB;
  --accent-on-band:#FFD400;
  --ink:#0E0E0E;
  --ink-2:#59595A;
  --ink-3:#6A6A6C;
  --accent:#FFD400;
  --accent-2:#826400;
  --accent-ink:#0E0E0E;
  --line:#C3C3C6;
  --maxw:1240px;
  --gutter:22px;
  --radius:0px;
  --accent-a12:rgba(255,212,0,.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:#eae8dd;
  --board-cell:#f8f7f3;
  --board-line:rgba(255,212,0,.30);
  --board-accent:#826400;
  --board-glow:rgba(255,212,0,.13);
  --board-shadow-1:rgba(14,14,14,.08);
  --board-shadow-2:rgba(14,14,14,.26);
  --font-display:'Roboto Slab',Georgia,'Times New Roman',serif;
  --font-body:'Barlow',system-ui,-apple-system,'Segoe UI',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:900;letter-spacing:-.028em}
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(14,14,14,.28)}
.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:#8A2B00;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:60px}
.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}
}


/* ============================================================================
   06 — THE RHYTHM.  site.css

   DIRECTION. Photographs alternate with solid blocks of one accent, hard edge to
   hard edge, so the page has a BEAT you can count: photograph, colour block,
   photograph, ink block, and round again. Nothing overlaps, nothing fades into
   anything, nothing floats in whitespace. Every join between two sections is a
   straight line where two full-bleed rectangles meet — which is why --radius is
   0px and there is not one box-shadow in this file.

   NOT ONE WORD IS SET OVER A PHOTOGRAPH. That is a design decision first (the
   picture is a beat, not a backdrop) and it is also why the contrast on this page
   is arithmetic instead of a measurement: every pair is a flat colour on a flat
   colour and all fifteen are declared in template.json.

   THE TRAP. --accent #FFD400 as text on white is 1.43:1. The yellow is a FILL. It
   is a word in exactly one place — on ink, at 13.49:1. Accent TEXT is --accent-2
   #826400 (5.57:1 on paper), and base.css's `.eyebrow{color:var(--accent)}` is
   overridden below or every eyebrow on this page would disappear.

   base.css is prepended, then shared/rating.css, then this file, then mosaic.css
   and pghnote.css. Everything here SKINS; nothing here rebuilds. Shared component
   classes are only ever touched through a scope (`.prices .bname`), never bare.
   ========================================================================== */

:root{
  --paper:#FFFFFF;
  --font-cond:'Barlow Condensed','Barlow',system-ui,sans-serif;
  --nav-h:60px;
  /* the page's one horizontal measure. Every full-bleed block pads to it and the
     type inside centres to --maxw, so the EDGES are the viewport and the TEXT is
     a column — the two things a hard-edged page has to hold apart. */
  --pad:clamp(18px,4vw,60px);
  --beat:380ms;
  --hair:3px;
  /* THE MASTHEAD's real occupied height: the fixed nav plus the demo-only preview
     bar pinned above it. The hero floors its photograph row at this plus a sliver,
     so the colour block can never begin underneath the bar. */
  --mast:calc(var(--nav-h) + var(--tpl-h,0px));

  /* NO MOSAIC WALL ON THIS SKELETON, deliberately. The house wall is the right
     answer for a template whose photographs sit in one place; the Rhythm's
     photographs sit in TWELVE, interlocked with the priced cells, and a separate
     twelve-tile wall on top of that would need twenty-odd pictures from a pool of
     fourteen and would start repeating them. The checkerboard IS this template's
     photo wall, and it is what the approved mockup draws. */

  /* THE GOOGLE RATING, skinned through its five tokens and nothing else. The
     figure has no ground of its own: it IS the colour block it sits on, so the
     ground is transparent, there is no shadow (this page has none) and the rim is
     a hairline of ink. The review slots are paper rectangles standing on the
     yellow — the same figure/ground swap the headline makes. */
  --rating-bg:transparent;
  --rating-shadow:none;
  /* NO RIM. shared/rating.css draws `.rating-fig::after` as an inset 1px highlight
     for a figure sitting on its own card ground; this figure has no card - it IS
     the block - so that highlight rendered as an orphaned rectangle ruled around
     the 4.9 with nothing inside it. Transparent, not overridden with a colour. */
  --rating-rim:transparent;
  /* the review slots now stand in the SAME block as the score, on paper, so a
     paper card would be invisible. They are the second paper, cut into the first
     and ruled in ink - the same well the contact fields are. */
  --rq-bg:var(--surface-2);
  --rq-ink:var(--ink);
  /* THE THREE GROUNDS, named once so the beat can be read off the sheet:
     --paper #FFFFFF, --surface-2 #F5F4F1, --band #0E0E0E, plus --accent #FFD400
     as the punctuation. No two adjacent sections take the same one. */
}

/* --------------------------------------------------------------- the floor -- */
body{background:var(--paper);color:var(--ink)}
.cond{font-family:var(--font-cond)}

/* A yellow focus ring on white paper is 1.43:1 and unusable, and a black one is
   invisible on the ink blocks. So both: an ink ring with a yellow halo behind it,
   which reads on paper, on yellow and on ink. */
:focus-visible{outline:3px solid var(--ink);outline-offset:0;
  box-shadow:0 0 0 6px var(--accent);border-radius:0}

h1{font-size:clamp(34px,7.4vw,92px);line-height:1.24;text-transform:uppercase;max-width:22ch}
h2{font-size:clamp(27px,5vw,58px);line-height:1.04;text-transform:uppercase;max-width:17ch}
h3{font-size:clamp(18px,2vw,23px);line-height:1.16;letter-spacing:-.012em;
  text-transform:uppercase}

/* THE CONDENSED VOICE carries every label on the page and never a sentence. */
.eyebrow{font-family:var(--font-cond);font-size:13.5px;font-weight:700;letter-spacing:.3em;
  color:var(--accent-2);margin:0 0 clamp(10px,2vw,22px)}
.lead{font-size:17.5px;line-height:1.55;color:var(--ink-2);max-width:52ch}
@media(min-width:760px){.lead{font-size:19px}}

.btn{border-radius:0;border-width:2px;font-family:var(--font-cond);font-weight:700;
  font-size:15.5px;letter-spacing:.16em;text-transform:uppercase;padding:15px 26px}
.btn:active{transform:none}
.btn-primary{background:var(--ink);color:var(--accent);border-color:var(--ink)}
.btn-primary:hover{background:var(--accent);color:var(--ink);border-color:var(--ink)}
.btn-ghost{background:transparent;color:var(--ink);border-color:var(--ink)}
.btn-ghost:hover{background:var(--ink);color:var(--accent)}
.btnrow{gap:10px}

/* ------------------------------------------------------------ preview bar --
   Demo-only chrome, and the comment that used to sit here was wrong: the shared
   floor makes #nav `position:fixed`, not sticky, so in normal flow the ink
   masthead COVERED this bar completely — the strip was never once visible, and it
   still charged the document 38px at 1440 and 57px at 390. It is now pinned ABOVE
   the nav and the nav is given a matching padding-top, published as --tpl-h,
   which is what 01 and 05 already do. No `position` and no `top` is declared on
   #nav here: the shared floor owns those and houserules.py enforces it. */
.tplbar{position:fixed;top:0;left:0;right:0;z-index:130;
  background:var(--ink);color:var(--band-ink-2);font-family:var(--font-cond);
  font-size:12px;letter-spacing:.06em;text-transform:uppercase;line-height:1.5;
  padding:9px var(--pad);text-align:center}
.tplbar b{color:var(--accent);font-weight:700}
:root:has(.tplbar){--tpl-h:54px}
@media(min-width:760px){:root:has(.tplbar){--tpl-h:36px}}
#nav{padding-top:var(--tpl-h,0px)}

/* --------------------------------------------------------------------- nav --
   A solid ink bar with a straight bottom edge — the first beat of the page. */
#nav{background:var(--ink);color:var(--band-ink)}
.nav-inner{gap:10px 18px;flex-wrap:wrap}
.brand{display:inline-flex;align-items:center;gap:10px;text-decoration:none;
  color:var(--band-ink);min-height:44px}
.brand .mono{display:inline-flex;align-items:center;justify-content:center;
  width:30px;height:30px;background:var(--accent);color:var(--ink);
  font-family:var(--font-cond);font-weight:700;font-size:14px;letter-spacing:.04em}
.brandtype{font-family:var(--font-display);font-weight:900;font-size:18px;
  letter-spacing:-.02em;text-transform:uppercase;white-space:nowrap}
.navhours{display:none}
.navtoggle{color:var(--band-ink)}
.navtoggle .bars{display:block;width:22px}
.navtoggle .bars i{display:block;height:2px;margin:5px 0;background:var(--accent)}
.navtoggle .bars i:nth-child(2){width:14px}
.nav-links{flex:1 0 100%;flex-direction:column;align-items:stretch;gap:0;width:100%;
  padding:4px 0 14px;margin-top:4px;border-top:2px solid var(--band-ink-2)}
.nav-links a{display:flex;align-items:center;width:100%;min-height:46px;padding:10px 0;
  color:var(--band-ink);text-decoration:none;font-family:var(--font-cond);font-weight:600;
  font-size:16px;letter-spacing:.18em;text-transform:uppercase}
.nav-links a:hover{color:var(--accent)}
.drawhours{margin:8px 0 2px;color:var(--band-ink-2);font-family:var(--font-cond);
  font-size:12.5px;letter-spacing:.2em;text-transform:uppercase}
.nav-links .drawtel{color:var(--accent)}
.nav-links .navcta{justify-content:center;margin-top:8px;padding:0 20px;
  background:var(--accent);color:var(--ink);font-weight:700}
.nav-links .navcta:hover{background:var(--paper);color:var(--ink)}
html.menu-open #nav{background:var(--ink)}
@media(min-width:940px){
  .nav-inner{flex-wrap:nowrap}
  .nav-links{flex:0 0 auto;width:auto;padding:0;margin:0;border:0;align-items:center}
  .nav-links a{width:auto;min-height:38px;padding:0;font-size:13.5px;letter-spacing:.18em}
  /* two classes, deliberately: `.drawtel` alone is (0,1,0) and loses to
     `.nav-links a{display:flex}` at (0,1,1), which printed the drawer's phone
     number in every desktop masthead across the set until it was scoped. */
  .nav-links .drawhours,.nav-links .drawtel{display:none}
  .nav-links .navcta{height:40px;margin-top:0}
}

/* ============================================================== THE BEAT ====
   Two primitives and nothing else. `.band` is a photograph, edge to edge, its
   height set by the BEAT rather than by the picture. `.slab` is a solid block of
   one colour, edge to edge, its height set by its type. They alternate.

   ORIENTATION. The height lives on the wrapper — never on a frame, a figure or
   the image — and the picture fills it through `.band>*`. No selector in this
   file pins a proportion or a size on a photographic frame below the hero, so a
   client with only landscape pictures and a client with only portrait ones get
   the same page; the shared wall below takes any mix by construction. */
.band{position:relative;overflow:hidden;background:var(--ink)}
.band>*{display:block;width:100%;height:100%;object-fit:cover}
/* `.band` IS NOW THE HERO AND NOTHING ELSE (§3.15, 2026-08-15). The two mid-page
   bands that used to use `.cut` were full-bleed, 1,425px wide, and that is a bet
   on a source file we did not shoot - at that size every fault in the picture is
   the entire visual experience of the page. Both are contained plates now; see
   THE PLATE below. The declaration is kept so the primitive still exists for a
   template that legitimately wants one, and it is unused on this page. */
.band.cut{height:clamp(250px,33vw,470px)}

.slab{position:relative;overflow:hidden;background:var(--paper);color:var(--ink)}
/* THE FILL is a layer, not a background: it is what slides. See THE SLAM. */
.slab .fill{position:absolute;inset:0;z-index:0;background:var(--accent)}
.slab .cnt{position:relative;z-index:1;max-width:var(--maxw);margin:0 auto;
  padding:clamp(24px,5.2vw,76px) var(--pad)}
.slab .eyebrow{color:var(--ink)}
.slab .lead{color:var(--ink-2)}
.slab.dark{background:var(--accent);color:var(--band-ink)}
.slab.dark .fill{background:var(--band)}
.slab.dark .eyebrow{color:var(--accent)}
.slab.dark .lead{color:var(--band-ink-2)}
/* THE OTHER TWO GROUNDS. A block's `background` is the colour it is BEFORE the
   slam and its `.fill` is the colour it becomes, so every slam is the page
   changing colour rather than a movement laid over it. Paper and paper-2 exist
   because tonal stepping needs more than two values to alternate across nine
   sections without the yellow having to take every other one. */
.slab.paper{background:var(--accent);color:var(--ink)}
.slab.paper .fill{background:var(--paper)}
.slab.paper2{background:var(--accent);color:var(--ink)}
.slab.paper2 .fill{background:var(--surface-2)}

/* THE LABEL LINE. Not a display heading: a header at furniture size, which is
   what §3.8 asks for above the score and what the review beat takes as well.
   Set in the condensed voice so it can never compete with the figure under it. */
.labelline{margin:0;max-width:34ch;font-family:var(--font-cond);font-weight:600;
  font-size:clamp(15px,1.8vw,19px);letter-spacing:.02em;color:var(--ink-2)}
.slab.dark .labelline{color:var(--band-ink-2)}

.blkhead{max-width:none}
.blkhead h2{margin:0}
.blkhead .lead{margin:clamp(10px,2vw,22px) 0 0}

/* the ink line under the hero: hours and address, the two things a visitor wants
   before anything else, in the condensed voice at label scale. */
.strip{margin:0;background:var(--ink);color:var(--band-ink);
  display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 20px;
  padding:7px max(var(--pad),calc((100% - var(--maxw)) / 2 + var(--pad)));
  font-family:var(--font-cond);font-weight:600;font-size:11px;letter-spacing:.08em;
  line-height:1.4;text-transform:uppercase}
.strip b{color:var(--accent);font-weight:700}
.strip a{display:inline-flex;align-items:center;min-height:44px;color:var(--accent);
  text-decoration:underline;text-underline-offset:4px}
.strip a:hover{color:var(--band-ink)}

/* ================================================================== HERO ====
   ONE 100svh budget, three rows: `1fr auto auto`. The colour block and the ink
   strip take their own intrinsic height, untouched, and the PHOTOGRAPH is the 1fr
   that gives up exactly the shortfall and not one pixel more.

   The block is a TWO-COLUMN composition from 1000px — headline left, sentence and
   buttons in the column beside it. Measured at 15ch the headline forced three
   lines, the lead and the buttons stacked under it, and the block ate ~70% of the
   first screen and letterboxed the picture. Two columns is the same type at the
   same size in a block roughly 40% shorter, and the photograph gets the rest. */
/* THE BUDGET, AND WHY THE FIRST ONE COULD NOT HOLD.
   `1fr` NEVER shrinks a track below its base size, and the base size of the
   photograph's row was the picture's own intrinsic height: `height:100%` on an
   image whose row height is still being resolved computes to `auto`, so the row
   measured 1425 x 1539/800 = 2,741px and the budget was decoration. #top came out
   at 3,187px at 1440x900 — three and a half viewports, both CTAs at y=3,138 — and
   1,265px at 390x844. No cap on the type could have fixed that, because the type
   was never what overflowed.
   SO THE FIX IS THE PICTURE. The image is taken OUT OF FLOW (position:absolute,
   inset:0): the row then has no in-flow content, no base size, and minmax(floor,
   1fr) is free to hand it whatever is left. `min-height:100svh` makes the space
   definite, the colour block and the ink strip take their intrinsic height, and
   the photograph gets exactly the remainder and not one pixel more.
   THE FLOOR IS THE MASTHEAD PLUS 72px, not a bare number: the nav is fixed and
   the preview bar is pinned above it, so anything shorter lets the colour block
   start underneath the bar. Below that floor the hero simply grows past one
   screen — 320x568 measures 763px — and nothing is clipped to protect a budget.
   MEASURED on the deployed page, in the browser, at five sizes:
     1440x900  463 + 398 + 39 = 900   CTAs bottom 805, kicker top 514, photo 367
      390x844  335 + 448 + 61 = 844   CTAs bottom 761, kicker top 357, photo 221
      768x1024 587 + 399 + 39 = 1024  CTAs bottom 952
     2560x1440 899 + 502 + 39 = 1440
      320x568  186 + 516 + 61 = 763   the hero GROWS past the screen, nothing cut
   (photo = the part of the band below the masthead. The rows are the computed
   grid-template-rows, not an estimate.) */
/* FIX 1, 2026-08-15: THE PHOTOGRAPH TAKES THE LARGER SHARE. It measured 463/398
   at 1440x900 and 335/448 at 390x844 - on a phone the colour block outweighed the
   picture, which is backwards on a photo-forward template. Two changes, and the
   floor is the one that guarantees it:
     1. the photograph's track has a FLOOR of 60svh (53 below 760px, where the type
        genuinely needs more), so the picture cannot be squeezed under that share
        whatever the headline does. It was 56/50 and Jon asked for more of the
        picture; the block gave up the difference rather than the hero growing;
     2. every vertical measure in the block is tightened below - padding, the
        headline's cap and leading, the two margins and the strip - so the block
        FITS in what is left rather than pushing the hero past one screen.
   The floor is a `max()` against the masthead clearance, never instead of it: the
   colour block can still never begin underneath the fixed bar. */
#top{min-height:100svh;
  display:grid;
  grid-template-rows:minmax(max(calc(var(--mast) + 72px),52svh),1fr) auto auto;
  overflow:hidden}
@media(min-width:760px){
  #top{grid-template-rows:minmax(max(calc(var(--mast) + 72px),60svh),1fr) auto auto}
}
#top .hero-band{position:relative;height:auto;min-height:0;overflow:hidden}
#top .hero-band picture{position:absolute;inset:0;display:block;width:auto;height:auto}
/* THE FOCAL POINT IS SET FOR THE VISIBLE BAND, NOT THE BAND. The nav is fixed
   and the preview bar is pinned above it, so the top 114px of the phone band (60px
   on a client build) sits BEHIND the masthead and is never seen. Framing against
   the whole 335px box put the barber above the bar and left a rectangle of black
   cape on screen. Both plates were cropped to their visible slice at 0/8/15/22%
   and compared before this was changed; 0% is the only value that holds the
   subject in both. Re-point a client photograph here and nowhere else. */
#top .hero-band img{position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;object-position:50% 0%}
#top h1{max-width:24ch;margin:0;line-height:1.12;
  font-size:min(clamp(30px,6vw,92px),10.2vh)}
/* two lines, not three: at 1000px+ the left column is ~61% of the page and this
   face needs about 13px of column per 1px of setting to hold the sentence on two. */
@media(min-width:1000px){#top h1{font-size:min(clamp(30px,6vw,92px),10.2vh,3.95vw)}}
#top .cnt{padding-top:min(clamp(12px,2.6vw,34px),3.3vh);
          padding-bottom:min(clamp(12px,2.6vw,34px),3.3vh)}
#top .eyebrow{margin-bottom:min(clamp(8px,1.4vw,16px),1.6vh)}
/* the hero lead stays at the small-screen size at every width: base.css steps
   .lead up to 19px from 760px and in this composition that is one more line in
   the side column, which the photograph pays for out of the 1fr. */
#top .lead{margin:min(clamp(8px,1.6vw,18px),1.7vh) 0 0;max-width:38ch;
  font-size:16.5px;line-height:1.45}
@media(min-width:1000px){#top .lead{font-size:17.5px;line-height:1.5}}
#top .btnrow{margin-top:min(clamp(9px,1.8vw,20px),2.1vh)}
#top .btn{padding:12px 22px;font-size:14.5px}
@media(min-width:1000px){#top .btn{padding:14px 26px;font-size:15.5px}}
@media(min-width:1000px){
  #top .cnt{display:grid;grid-template-columns:minmax(0,1.42fr) minmax(0,.58fr);
    column-gap:clamp(26px,3vw,56px);align-items:end}
  #top .eyebrow{grid-column:1 / -1}
  #top h1{grid-column:1}
  #top .side{grid-column:2;padding-bottom:6px}
  #top .lead{margin-top:0;max-width:34ch}
}

/* THE DRAWN MOMENT — INVERSION. The payload phrase is knocked out of the page: a
   solid ink rectangle painted behind it, the words inside it set in the accent,
   box-decoration-break:clone so it survives the line break. The same two colours
   as everything else, in the opposite order, for exactly the length of the phrase.
   line-height stays at 1.24 because an inline background box is as tall as the
   face's ascent plus descent and would otherwise overprint the line above. */
.knock{font-style:normal;background:var(--ink);color:var(--accent);
  padding:.03em .13em .07em;
  box-decoration-break:clone;-webkit-box-decoration-break:clone}

/* ============================================ THE RATING, MINIMAL AND HUGE ==
   RULE 4, §3.8. Two elements stand above the score and no more: the eyebrow and
   one label line. The display heading, the body paragraph and the three-reason
   ledger that used to sit on top of it are gone - by the time the number rendered
   it was the fourth thing in the section and it had been argued for rather than
   shown. The review slots are not here either; they are their own beat after the
   prices (§3.7, once each).

   PAPER GROUND, and that is the point of it. The figure is the only thing on this
   page set at that scale and near-black on white is the highest contrast the
   palette can give it - 18.1:1 against #FFD400's 1.43:1. It also costs the page
   nothing in yellow, which the failed build spent 1,667px of here. */
/* THE PADDING WENT WITH THE WORDS. `.slab .cnt` pads 76px at 1440 because every
   other block on this page opens with an eyebrow and a heading; this one opens on
   a 280px numeral, and 76px of top padding plus the component's own 32px margin
   was 108px of white above a figure that needs none. The section starts on the
   score. Measured above the figure after this: 26px at 1440, 14px at 390. */
.rated .cnt{padding-top:clamp(14px,1.8vw,26px)}
.rated .rating{margin-top:0}
.rated .rs-on{color:var(--ink)}
.rated .rs-off{color:rgba(14,14,14,.20)}
.rated .rnum{letter-spacing:-.05em;font-size:clamp(104px,21vw,280px)}
.rated .rsay{font-family:var(--font-display);font-weight:700;text-transform:uppercase;
  letter-spacing:-.02em}
/* the pill is drawn for a dark ground by the shared sheet: on paper it needs an
   ink rim and ink lettering or it disappears. Google's own asset is untouched. */
.rated .gmpill{background:var(--surface-2);border:2px solid var(--ink)}
.rated .gmpill:hover{background:var(--accent)}
.rated .gmpill .gmgo{color:var(--ink)}
.rated .gmpill:hover .gmgo{color:var(--ink)}
.rated .gmpill img{filter:invert(1)}

/* ====================================================== THE REVIEW SLOTS ====
   IN THE SAME BLOCK AS THE SCORE, and only there. They used to be a section of
   their own further down the page - two review moments, which reads as a page
   repeating itself, and is the duplicate RULE 3 / §3.7 exists to stop.

   HIERARCHY IS THE POINT OF PUTTING THEM TOGETHER. The figure runs to 280px of
   type; a slot's quote is 16.5px. One is the fact, the other three are the
   colour, and at a 17:1 size ratio nobody can mistake which is which.
   THREE ACROSS from 900px - never one under another. The shared component is a
   single 660px column by default and both of those are overridden here. */
/* the heading over the QUOTE ROW. `proof.title` used to be a label line ABOVE the
   score, with an eyebrow above that; both are gone and the section now opens on
   the figure itself (Jon, 2026-08-15 - §3.8 caps the preamble, it does not require
   one, and the nav label and the Google pill were already saying it twice). The
   token is not deleted, it is demoted and moved below the component, which is what
   §3.8 prescribes. The rule above it is what separates the fact from the colour. */
.rqhead{margin:clamp(24px,3.2vw,40px) 0 0;padding-top:clamp(16px,2.2vw,26px);
  border-top:var(--hair) solid var(--ink)}
.rqhead h2{max-width:20ch;font-size:clamp(21px,2.5vw,32px)}
.rqhead p{margin:9px 0 0;max-width:46ch;font-family:var(--font-cond);font-weight:600;
  font-size:14.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-2)}
.rated .rquotes{max-width:none;gap:2px;margin-top:clamp(12px,1.8vw,20px)}
@media(min-width:620px) and (max-width:899px){
  .rated .rquotes{grid-template-columns:1fr 1fr}
}
@media(min-width:900px){
  .rated .rquotes{grid-template-columns:repeat(3,minmax(0,1fr));gap:2px}
}
/* a well cut into the paper, ruled in ink - the same object as a contact field,
   at the other end of the page. On the old ink ground these were paper cards; on
   paper a paper card is invisible, which is why the token changed with them. */
.rated .rq{padding:clamp(15px,1.9vw,22px);border-radius:0;
  box-shadow:inset 0 0 0 2px var(--ink)}
.rated .rq blockquote{font-size:16.5px;line-height:1.42}
.rated .rq figcaption{font-family:var(--font-cond);letter-spacing:.18em;margin-top:10px;
  color:var(--ink-3)}
.rated .rq-empty{border:2px dashed var(--ink);box-shadow:none;background:none}
.rated .rq-empty blockquote{color:var(--ink-2)}

/* ====================================================== THE CHECKERBOARD ====
   THE PRICES, and the template's central device. Price cells and photo cells
   interlock in ONE grid - block, picture, block, picture - so the beat runs
   across the page as well as down it. This is the mockup's move and it is what
   makes 06 a rhythm rather than a stack of sections.

   IT IS THE SHARED SERVICES COMPONENT, NOT A COPY. `.board`, `.bgroup` and
   `.bgrid` are set to display:contents, which drops the component's own boxes out
   of the layout and makes every `.bitem` a direct cell of `.beats` - so the list
   is still built, iconed and normalised by shared/partials/board.html and a fix
   there still lands here. Nothing about the component is forked.

   THE INTERLEAVE IS `order`, NOT A PINNED GRID POSITION. Price cell n takes
   2n-1, photo cell n takes 2n. In a three-track grid an alternating sequence
   lands as a checkerboard by arithmetic - for FOUR services, for six, for ten -
   which is why no cell is pinned to a row or a column and a client with a shorter
   list gets the same device rather than a hole. */
/* FIX 4, 2026-08-15: CONTAINED, NOT FULL BLEED. Edge to edge on a 1440 window
   each cell was 480px wide against a 290px floor - a 1.65:1 stretched rectangle,
   and stretched is the opposite of what a beat is. The grid now sits in the
   page's own measure (--maxw 1240, less the gutter, so 1120 across) and a cell
   comes out about 373 wide; with the floor raised below it lands near square.
   The ink either side of it is the section's ground, so the header above still
   runs straight into the grid with no join. */
.prices .beats{display:grid;grid-template-columns:1fr;
  width:100%;max-width:var(--maxw);margin:0 auto;padding:0 var(--pad)}
@media(min-width:620px){.prices .beats{grid-template-columns:1fr 1fr}}
@media(min-width:960px){.prices .beats{grid-template-columns:repeat(3,1fr)}}
/* the field the contained grid stands on. Without it the margins either side of
   the grid would be the body's white and the section would fall apart. */
.prices{background:var(--band)}
/* THE HEADER LEADS INTO THE GRID. It was a 76px shared bottom padding plus the
   block's own, so the eye crossed an empty black rectangle between "what a cut
   costs here" and the first cell. Now it stops just above the grid. */
.prices .slab .cnt{padding-bottom:clamp(14px,1.9vw,26px)}
.prices .board,.prices .bgroup,.prices .bgrid{display:contents}
/* THE MENU STATE, designed rather than broken. A client whose priced list arrives
   as titled groups gets group headings, and a heading is not a checkerboard cell.
   The board then falls back to its own grid inside one full-width cell and the
   photographs run beneath it - a second state, not a collapse. */
.prices .beats:has(.bgh){grid-template-columns:1fr}
.prices .beats:has(.bgh) .board{display:block}
.prices .beats:has(.bgh) .bgroup{display:block}
.prices .beats:has(.bgh) .bgrid{display:grid;gap:2px;
  grid-template-columns:repeat(var(--bd-cols),minmax(0,1fr))}
@media(min-width:620px){.prices .beats:has(.bgh){grid-template-columns:1fr 1fr}}

/* the cell. Both kinds are the same rectangle: no radius, no shadow, no gutter,
   and a floor that is measured against the WIDTH so a phone gets a shallow cell
   and a desktop a deep one. The 2px gap is the straight join, not a gutter. */
/* NO GAP AND NO GROUND OF ITS OWN. The mockup butts the cells - the join between
   two of them is the same straight line the sections have, not a gutter - and it
   also settles the tonal read: measured on the deployed page the checkerboard's
   host, the block above it and the strip below it all computed #0E0E0E, three
   consecutive equal grounds, which is a §3.5 fail however good the cells look.
   The host is transparent now and the strip below takes the accent, so the read
   is INK -> (mixed) -> ACCENT. The cells themselves never touch a cell of their
   own colour: odd blocks are accent, even blocks are ink, and a photograph stands
   between every pair in both directions. */
.prices .beats{gap:0;background:transparent}
.prices .bitem,.prices .cellpic{position:relative;overflow:hidden;margin:0;
  border-radius:0;background:transparent}
/* SQUARER CELLS. In the contained grid a track is about 373px at 1440, so a 355px
   floor puts a cell at roughly 1.05:1 instead of the 1.65:1 it was at full bleed.
   Measured against the WIDTH, so a phone gets a cell it can hold and a desktop
   gets the square one - and it is a clamp, never a pinned pixel height, which is
   the distinction orientcheck.py enforces. */
/* the floor only governs BELOW ~865px (above it 25.5vw is larger and above 1392 the
   355 cap takes over), so this number is a phone number: 212px against a 339px
   track is 1.6:1, the same shape as the two contained figures further down, and
   the eight pixels it gives back are what keep the page inside 7,500 at 390. */
.prices .bitem,.prices .cellpic{min-height:clamp(212px,25.5vw,355px)}

/* THE PRICE CELL. `.bmain` is display:contents too, so the name, the sentence,
   the price and the duration chip are all cells of the cell and the price can sit
   on the baseline with the chip beside it - the mockup's composition - without
   the skeleton touching the component's markup. */
.prices .bitem{display:grid;grid-template-columns:minmax(0,1fr) auto;
  grid-template-rows:auto 1fr auto;column-gap:14px;
  padding:clamp(18px,2.4vw,32px);color:var(--ink)}
.prices .bmain{display:contents}
.prices .bicon{display:none}
.prices .bitem::before{display:none}
.prices .bname{grid-column:1 / -1;grid-row:1;margin:0;font-family:var(--font-display);
  font-weight:900;font-size:clamp(19px,2.1vw,26px);line-height:1.06;
  letter-spacing:-.018em;text-transform:uppercase;color:inherit}
.prices .bdesc{grid-column:1 / -1;grid-row:2;margin:9px 0 0;font-size:15px;
  line-height:1.46;color:inherit;opacity:.82;align-self:start}
.prices .bprice{grid-column:1;grid-row:3;align-self:end;justify-self:start;
  margin-top:16px;font-family:var(--font-display);font-weight:900;
  font-size:clamp(30px,3.4vw,46px);line-height:.9;letter-spacing:-.03em;color:inherit}
.prices .btag{grid-column:2;grid-row:3;align-self:end;justify-self:end;margin:0 0 4px;
  border:2px solid currentColor;background:none;color:inherit;opacity:.72;
  font-family:var(--font-cond);font-weight:700;font-size:12.5px;letter-spacing:.2em;
  text-transform:uppercase;padding:2px 9px;border-radius:0}
/* the unpriced state's hook is a full-width cell, first in the beat. */
.prices .bhook{grid-column:1 / -1;order:0;margin:0;padding:clamp(16px,2vw,24px);
  border:2px dashed var(--band-ink-2);background:var(--band);color:var(--band-ink-2)}
.prices .bhook-l{font-family:var(--font-cond);letter-spacing:.24em;color:var(--accent)}
.prices .bhook-t{color:var(--band-ink-2)}
.prices .bgh{grid-column:1 / -1;font-family:var(--font-cond);font-size:13px;
  letter-spacing:.24em;color:var(--accent);margin:22px 0 10px}

/* THE FILL IS A PSEUDO-ELEMENT here rather than a `.fill` span, because the cells
   come out of a shared component this template does not author. Same behaviour,
   same curve: parked one width off the side it sits on and slid to 0. Odd cells
   take the accent, even cells the ink, so the yellow is HALF of the blocks in this
   section rather than all of them. */
.prices .bitem::after{content:"";position:absolute;inset:0;z-index:0;
  background:var(--cellfill,var(--accent))}
/* `.bmain` is display:contents, so it generates NO BOX - a z-index or an opacity
   set on it does nothing at all and its children would paint UNDER the fill. The
   leaves are named instead. Found by reading the cascade, not by hoping. */
.prices .bitem > *,.prices .bname,.prices .bdesc,.prices .btag,.prices .bprice{
  position:relative;z-index:1}
.prices .bitem:nth-child(even){--cellfill:var(--band);color:var(--band-ink)}

/* THE PHOTO CELL. The caption is DATA - `gallery[].cap` and `.meta` out of the
   same slot as the picture (RULE 5, §3.9) - laid on an ink footer so it is read
   against a flat colour and never against the photograph. */
.prices .cellpic{display:block}
.prices .cellpic img{position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;object-position:50% 50%}
.prices .cellpic figcaption{position:absolute;left:0;right:0;bottom:0;z-index:2;
  padding:11px clamp(14px,1.6vw,20px) 12px;background:var(--band);color:var(--band-ink)}
.prices .cellpic figcaption b{display:block;font-family:var(--font-display);
  font-weight:900;font-size:14.5px;line-height:1.2;letter-spacing:-.01em;
  text-transform:uppercase}
.prices .cellpic figcaption span{display:block;margin-top:3px;font-family:var(--font-cond);
  font-weight:600;font-size:11.5px;letter-spacing:.2em;text-transform:uppercase;
  color:var(--accent)}
/* the curtain: an ink layer sitting ON the picture that wipes off to the side,
   so a photo cell lands on the same beat as the block beside it. */
.prices .cellpic::after{content:"";position:absolute;inset:0;z-index:3;
  background:var(--band)}

.prices .bitem:nth-child(1){order:1;transition-delay:0ms}
.prices .beats > figure:nth-of-type(1){order:2}
.prices .beats > figure:nth-of-type(1)::after{transition-delay:45ms}
.prices .bitem:nth-child(2){order:3;transition-delay:90ms}
.prices .beats > figure:nth-of-type(2){order:4}
.prices .beats > figure:nth-of-type(2)::after{transition-delay:135ms}
.prices .bitem:nth-child(3){order:5;transition-delay:180ms}
.prices .beats > figure:nth-of-type(3){order:6}
.prices .beats > figure:nth-of-type(3)::after{transition-delay:225ms}
.prices .bitem:nth-child(4){order:7;transition-delay:270ms}
.prices .beats > figure:nth-of-type(4){order:8}
.prices .beats > figure:nth-of-type(4)::after{transition-delay:315ms}
.prices .bitem:nth-child(5){order:9;transition-delay:360ms}
.prices .beats > figure:nth-of-type(5){order:10}
.prices .beats > figure:nth-of-type(5)::after{transition-delay:405ms}
.prices .bitem:nth-child(6){order:11;transition-delay:450ms}
.prices .beats > figure:nth-of-type(6){order:12}
.prices .beats > figure:nth-of-type(6)::after{transition-delay:495ms}
.prices .bitem:nth-child(7){order:13;transition-delay:540ms}
.prices .beats > figure:nth-of-type(7){order:14}
.prices .beats > figure:nth-of-type(7)::after{transition-delay:585ms}
.prices .bitem:nth-child(8){order:15;transition-delay:630ms}
.prices .beats > figure:nth-of-type(8){order:16}
.prices .beats > figure:nth-of-type(8)::after{transition-delay:675ms}
.prices .bitem:nth-child(9){order:17;transition-delay:720ms}
.prices .beats > figure:nth-of-type(9){order:18}
.prices .beats > figure:nth-of-type(9)::after{transition-delay:765ms}
.prices .bitem:nth-child(10){order:19;transition-delay:810ms}
.prices .beats > figure:nth-of-type(10){order:20}
.prices .beats > figure:nth-of-type(10)::after{transition-delay:855ms}
.prices .bitem:nth-child(11){order:21;transition-delay:900ms}
.prices .beats > figure:nth-of-type(11){order:22}
.prices .beats > figure:nth-of-type(11)::after{transition-delay:945ms}
.prices .bitem:nth-child(12){order:23;transition-delay:990ms}
.prices .beats > figure:nth-of-type(12){order:24}
.prices .beats > figure:nth-of-type(12)::after{transition-delay:1035ms}
.prices .bitem:nth-child(13){order:25;transition-delay:1080ms}
.prices .beats > figure:nth-of-type(13){order:26}
.prices .beats > figure:nth-of-type(13)::after{transition-delay:1125ms}
.prices .bitem:nth-child(14){order:27;transition-delay:1170ms}
.prices .beats > figure:nth-of-type(14){order:28}
.prices .beats > figure:nth-of-type(14)::after{transition-delay:1215ms}

/* the closing line of the section, and it is the mockup's accent strip: it steps
   off the checkerboard's last row and off the paper-2 block that follows it. */
.prices .end{margin:0;background:var(--accent);color:var(--ink);
  padding:clamp(15px,2.2vw,26px) max(var(--pad),calc((100% - var(--maxw)) / 2 + var(--pad)));
  font-family:var(--font-display);font-weight:700;font-size:clamp(15px,1.9vw,21px);
  line-height:1.35}

.blkfoot{margin:clamp(14px,2.2vw,24px) 0 0;font-family:var(--font-cond);font-weight:600;
  font-size:15px;letter-spacing:.16em;text-transform:uppercase}
.slab.dark .blkfoot{color:var(--accent)}

/* ============================================================== THE SHOP ====
   ONE JOB. Who they are, in their own words, and three facts under it divided by
   rules rather than sat in cards - a card would round a corner and there are none
   on this page. It carries no prices, no hours, no address and no second run at
   the reviews: every one of those has exactly one home and this is not it
   (RULE 3, §3.7).
   PAPER-2 is the quietest ground in the palette and this is the breath between
   the checkerboard above and the ink review block below.

   TWO COLUMNS FROM 900px, and that is a dead-space fix, not a preference. Set in
   one column the heading measures 17ch and the sentence 52ch, so on a 1,120px
   text column the right 45% of the block was empty and the section ran 592px for
   about 320px of type - the same "big flat panel with nothing in half of it" that
   the failed build was rejected for, in paper rather than in yellow. Head left,
   the three facts stacked in the column beside it: the block fills and it loses
   roughly a third of its height. */
.shop .cnt{padding-bottom:clamp(24px,4vw,60px)}
/* THE OLD TWO-COLUMN `.cnt` IS GONE, and it had to go: it split the block before
   `.srow` ever got measured, so the photograph inherited half of half and came out
   266px - 18.7% of the window, which is the marooned tile all over again. The
   block is one column now; `.srow` does the splitting and `.facts` runs the full
   width beneath it in the base three tracks. Measured after the fix: 605px. */
@media(min-width:900px){
  .shop .blkhead h2{max-width:14ch}
}

/* ========================================================= THE FIGURE CELL ==
   A CONTAINED PHOTOGRAPH THAT LIVES IN A SECTION WITH CONTENT IN IT.

   §3.15 (SITE-BUILD-POLICY.md / Non-Skippable-Rules.md 12) is intact and is the
   reason this exists: the hero is the only full-bleed photograph on the page and
   every other one is framed inside the page's own measure. What it does NOT say,
   and what the first attempt got wrong, is that contained means small. A 473px
   frame with a two-word caption beside it, alone in a black band, is a photograph
   marooned in a void - unfinished rather than designed (Jon, 2026-08-15).

   So the frame joins content that already exists rather than having content
   invented around it. It is a COLUMN of a row: the shop's heading and sentence sit
   beside it, Find us's week and address sit beside it, and it takes a little over
   half the measure - 605px in the shop, 519 in Find us, against a 1,425px window.
   Confident, bounded, and nothing on either row is filler.

   THE FRAME CAPS THE WINDOW AND NOTHING ELSE: no aspect-ratio, no object-fit, no
   height, so the picture keeps its own proportion whatever the client sends. Both
   slots take a LANDSCAPE source on purpose - a 4:5 portrait at 605px is 756px tall
   beside 350px of type, which is the same void turned on its side. */
.figcell{margin:0;min-width:0}
.pframe{display:block;position:relative;overflow:hidden;
  box-shadow:0 0 0 2px var(--ink)}
.pframe img{display:block;width:100%;height:auto}
.pcap{margin-top:12px;display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 14px}
.pcap b{font-family:var(--font-display);font-weight:900;font-size:clamp(16px,1.7vw,21px);
  line-height:1.1;letter-spacing:-.014em;text-transform:uppercase;color:var(--ink)}
.pcap span{font-family:var(--font-cond);font-weight:600;font-size:12.5px;
  letter-spacing:.18em;text-transform:uppercase;color:var(--accent-2)}

/* THE SHOP'S ROW. Heading and sentence left, photograph right, and the three facts
   ruled across the FULL width underneath - so the row has two occupied halves and
   the section closes on a band of type rather than on air. End-aligned: the
   heading sits on the photograph's bottom edge, which is the join this page makes
   everywhere else. */
.shop .srow{display:grid;gap:clamp(22px,3vw,36px)}
@media(min-width:900px){
  .shop .srow{grid-template-columns:minmax(0,.46fr) minmax(0,.54fr);
    column-gap:clamp(30px,3.6vw,58px);align-items:end}
}
/* THE THREE FACTS, RULED, ACROSS THE FULL WIDTH. Restored 2026-08-15: the base
   `.facts` / `.fact` rules were lost in an earlier splice of this sheet and the
   three facts had been rendering unstyled and stacked in one column - caught by
   reading the computed `grid-template-columns` off the render (it said `none`),
   not by looking, because unstyled they still LOOK like content.
   They are the band that closes the section under the photo row, which is what
   stops the row's own end-alignment leaving air at the bottom of the block. */
.facts{display:grid;gap:0;padding:0;list-style:none}
.fact{padding:13px 0 15px;border-top:var(--hair) solid var(--ink)}
.fact h3{margin:0 0 4px}
.fact p{margin:0;font-size:15.5px;line-height:1.5;color:var(--ink-2)}
.slab.dark .fact{border-top-color:var(--accent)}
.slab.dark .fact p{color:var(--band-ink-2)}
@media(min-width:820px){
  .facts{grid-template-columns:repeat(3,1fr);column-gap:clamp(20px,2.6vw,38px)}
}
.shop .facts{margin-top:clamp(26px,3.4vw,44px)}

/* ============================================================== FIND US =====
/* ============================================================== FIND US =====
   ONE COMPACT SECTION: the week, the address, the two ways to reach a person and
   the map link. From 900px the week stands BESIDE the address rather than under
   it, which is what keeps this the shortest block on the page - stacked it ran to
   two and a half screens on a desktop and read as three sections in a trenchcoat.
   It is a YELLOW block, the page's third and last spend of the accent. */
.visit .vgrid{display:grid;gap:clamp(22px,3vw,40px);margin-top:clamp(18px,2.8vw,34px)}
/* text on the left, photograph on the right. The week and the address STACK inside
   the text column rather than standing beside each other: at 52% of the measure
   two of them would be 271px each and every day row would wrap. Stacked they come
   to about the height of the frame, which is what makes the row read as one
   composed object instead of two things that happen to be adjacent. */
.visit .vtext{display:grid;gap:clamp(20px,2.6vw,32px);align-content:start;min-width:0}
@media(min-width:900px){
  .visit .vgrid{grid-template-columns:minmax(0,.52fr) minmax(0,.48fr);
    column-gap:clamp(30px,3.6vw,56px);align-items:start}
}
/* on the yellow block the caption's dark gold is the only accent that holds, and
   the frame's rule is ink because a yellow rule on yellow is nothing. */
.visit .pcap span{color:var(--ink-2)}
.hours{margin:0}
.hrow{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;
  gap:4px 20px;padding:9px 0;border-top:2px solid var(--ink)}
.hrow:last-child{border-bottom:2px solid var(--ink)}
.hd{font-family:var(--font-cond);font-weight:600;font-size:14px;letter-spacing:.2em;
  text-transform:uppercase;color:var(--ink-2)}
.ht{font-family:var(--font-display);font-weight:900;font-size:clamp(19px,2.2vw,26px);
  letter-spacing:-.02em;color:var(--ink)}
/* on an ink block the same rules invert - kept here so the component can move. */
.slab.dark .hrow{border-top-color:var(--band-ink-2)}
.slab.dark .hrow:last-child{border-bottom-color:var(--band-ink-2)}
.slab.dark .hd{color:var(--band-ink-2)}
.slab.dark .ht{color:var(--band-ink)}
/* the designed state for a business that has not published a week. One line at
   display scale - the beat is kept, the gap is not. */
.hrow-line{display:block}
.hrow-line .ht{display:block;margin:4px 0 0;font-size:clamp(22px,3vw,34px);
  text-transform:uppercase}
.hask{display:block;margin:10px 0 0;max-width:44ch;font-size:15px;line-height:1.5;
  color:var(--ink-2)}
.slab.dark .hask{color:var(--band-ink-2)}

.vwhere address{font-style:normal;display:block;margin:0 0 14px;
  font-family:var(--font-cond);font-weight:600;font-size:15px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--ink-2)}
.vwhere .cbig{display:block;margin:0 0 6px}
.varea{margin:16px 0 0;max-width:40ch;font-size:15.5px;line-height:1.55;color:var(--ink-2)}

.crow{display:flex;flex-wrap:wrap;gap:8px 34px;margin:0 0 clamp(12px,2vw,18px)}
.cbig{font-family:var(--font-display);font-weight:900;font-size:clamp(19px,2.4vw,28px);
  letter-spacing:-.03em;text-decoration:none;color:var(--ink)}
.cbig:hover{color:var(--accent-2)}
.slab.dark .cbig{color:var(--accent)}
.slab.dark .cbig:hover{color:var(--band-ink)}
.maplink{display:inline-flex;align-items:center;min-height:44px;margin-top:12px;
  font-family:var(--font-cond);font-weight:700;font-size:14px;letter-spacing:.2em;
  text-transform:uppercase;color:var(--accent);background:var(--ink);padding:0 20px;
  text-decoration:none}
.maplink:hover{background:var(--paper);color:var(--ink);box-shadow:inset 0 0 0 2px var(--ink)}

/* ============================================================== THE CLOSE ===
   PAPER, so the page finishes on its brightest ground before the ink footer, and
   deliberately SHORT: a heading, one line, the two ways to reach a person, and the
   form. No repeat of the hours, no second address, no closing pitch that has
   already been made twice above.

   THE FIELDS ARE VISIBLE AT REST (RULE 7, §3.11). The panel is the second paper
   and each field is a WHITE well cut into it with a 2px ink border all the way
   round - a step off the panel you can see on a phone in daylight, not a hairline
   that only appears on focus. Focus adds the yellow halo on top of that. */
/* ONE ROW, TOP-ALIGNED. The heading used to sit ABOVE this grid at its own 17ch
   measure, so the right half of the block was empty beside it and the form began
   below and to the right - a stair-step with a diagonal hole in it. The heading
   is inside the left column now and the grid starts at the top of the block, so
   both columns share one top edge. `align-items:start` is what holds it: with
   `stretch` the shorter column's content centres itself and the step comes back. */
.close .cgrid{display:grid;gap:clamp(22px,3vw,38px);margin-top:0}
@media(min-width:960px){
  .close .cgrid{grid-template-columns:minmax(0,.86fr) minmax(0,1.14fr);
    gap:clamp(28px,3.4vw,56px);align-items:start}
}
.close .cleft,.close .cright{align-self:start}
.close .cleft .blkhead{margin:0}
.close .cleft .crow{margin-top:clamp(20px,2.8vw,34px)}
.formpanel{background:var(--surface-2);color:var(--ink);padding:clamp(18px,2.6vw,30px);
  box-shadow:inset 0 0 0 3px var(--ink)}
/* the form's own vertical measures, scoped: base.css owns .form/.field and every
   template legitimately skins them, but the gaps and the textarea floor are the
   single biggest block of height in the closing block on a phone. */
.formpanel .form{gap:12px}
.formpanel .field{gap:5px}
.formpanel .field textarea{min-height:88px}
.formpanel .field label{font-family:var(--font-cond);font-size:13px;letter-spacing:.2em;
  color:var(--ink-2)}
.formpanel .field input,.formpanel .field select,.formpanel .field textarea{
  border:2px solid var(--ink);background:var(--paper);color:var(--ink);border-radius:0}
.formpanel .field ::placeholder{color:var(--ink-3);opacity:1}
.formpanel .field input:focus,.formpanel .field select:focus,
.formpanel .field textarea:focus{border-color:var(--ink);box-shadow:0 0 0 4px var(--accent)}
.formpanel .btn-primary{width:100%;background:var(--ink);color:var(--accent);
  border-color:var(--ink)}
.formpanel .btn-primary:hover{background:var(--accent);color:var(--ink)}
.formpanel .fineprint{margin:10px 0 0;font-size:13.5px;line-height:1.5;color:var(--ink-3)}
.formpanel .thanks{border:2px solid var(--ink);background:var(--paper);padding:26px}
.formpanel .thanks h3{margin:0 0 8px}
.formpanel .thanks p{margin:0;color:var(--ink-2)}

/* ================================================================ FOOTER ====
   The last beat, and it is an ink block: the close above it is a colour block, so
   the page ends on the same alternation it has kept the whole way down. The
   Pittsburgh IT aside below is ours, in our own colours, and the 3px gold rule it
   carries is the final straight join on the page. */
footer{background:var(--ink);color:var(--band-ink)}
footer .container{max-width:var(--maxw);padding:clamp(28px,4.6vw,62px) var(--pad)}
.fgrid{display:grid;gap:clamp(20px,3vw,40px)}
@media(min-width:760px){.fgrid{grid-template-columns:1.3fr 1fr 1fr}}
.fbrand .mono{display:inline-flex;align-items:center;justify-content:center;
  width:46px;height:46px;background:var(--accent);color:var(--ink);
  font-family:var(--font-cond);font-weight:700;font-size:19px;letter-spacing:.04em}
.fname{margin:14px 0 6px;font-family:var(--font-display);font-weight:900;
  font-size:clamp(20px,2.4vw,28px);line-height:1.1;letter-spacing:-.028em;
  text-transform:uppercase;color:var(--band-ink)}
.fabout{margin:0;max-width:34ch;font-size:15.5px;line-height:1.55;color:var(--band-ink-2)}
.fh{margin:0 0 12px;font-family:var(--font-cond);font-size:13px;font-weight:700;
  letter-spacing:.24em;text-transform:uppercase;color:var(--accent)}
.fcol a,.fplain{display:block;padding:5px 0;font-size:15.5px;color:var(--band-ink-2)}
.fcol a:hover{color:var(--accent)}
.fcol address{font-style:normal;padding:5px 0;font-size:15.5px;color:var(--band-ink-2)}
.fnav{display:flex;flex-wrap:wrap;gap:4px 26px;margin-top:clamp(20px,3.2vw,40px);
  padding-top:16px;border-top:var(--hair) solid var(--accent)}
.fnav a{font-family:var(--font-cond);font-weight:600;font-size:14px;letter-spacing:.18em;
  text-transform:uppercase;color:var(--band-ink);min-height:40px;display:inline-flex;
  align-items:center}
.fnav a:hover{color:var(--accent)}
.fbase{display:flex;flex-wrap:wrap;justify-content:space-between;gap:8px 24px;
  margin-top:12px;font-size:13.5px;color:var(--band-ink-2)}
.fbase a{color:var(--accent)}

/* ================================================================== 404 =====
   The same two primitives, one beat each. */
.nf .cnt{padding-top:clamp(44px,7vw,96px);padding-bottom:clamp(44px,7vw,96px)}
.nfnum{display:inline-block;background:var(--ink);color:var(--accent);
  font-family:var(--font-display);font-weight:900;font-size:clamp(40px,7vw,86px);
  line-height:1;letter-spacing:-.04em;padding:.06em .14em .1em;margin:0 0 22px}
.nf h1{font-size:clamp(30px,5.4vw,62px)}
.nf .btnrow{margin-top:clamp(24px,3vw,34px)}

/* ======================================================= THE PHONE CASE =====
   THE CONTRACT CEILING IS 7,500px AT 390 and this page measured 9,907. Half of the
   overage was the hero and the preview bar, both fixed above. The rest is here:
   ONE pass over every vertical measure on the page, taking the phone value down
   and leaving the desktop value exactly where it stood — every clamp above keeps
   its vw term, so nothing in this block is inherited upward past 759px.
   NOTHING IS HIDDEN. No review, no service, no fact and no photograph is dropped
   to make the number; the page is content-dense rather than padding-heavy, which
   is why the last 262px could not be found without deleting something a client
   wrote. MEASURED 390x844: 9,907 -> 7,762px. */
@media(max-width:759px){
  h2{font-size:clamp(24px,5vw,58px)}
  .lead{font-size:16.5px}
  /* the two link columns stand side by side rather than stacked: at 390 each is a
     handful of short lines and one column spent 184px on nothing. */
  .fgrid{grid-template-columns:1fr 1fr}
  .fbrand{grid-column:1 / -1}
  /* the footer nav keeps its 40px row: 18px of page height is not worth taking a
     tap target below the house floor. */
  .fcol a,.fplain,.fcol address{padding:3px 0;font-size:15px}
  .fabout{font-size:15px}
  .fh{margin:0 0 8px}
  /* the checkerboard is one track at 390, so the beat runs block, picture, block
     vertically. The cells lose depth, never a cell and never a photograph. */
  /* the PRICE cell sizes to its own type at one track - there is no neighbour to
     match a row height with, so a floor here is only dead space. The PHOTO cell
     keeps the clamp it was given above (240px at 390), which is a measure against
     the WIDTH and not a pinned pixel height - orientcheck was right to reject the
     pin, and a 354x240 cell is a photograph rather than a strip. */
  .prices .bitem{min-height:0}
  .prices .bitem{padding:16px}
  .prices .bname{font-size:19px}
  .prices .bdesc{font-size:14.5px;line-height:1.42;margin-top:7px}
  .prices .bprice{font-size:30px;margin-top:12px}
  .prices .btag{padding:1px 8px;font-size:11.5px}
  .fact h3{font-size:16.5px}
  .fact p{font-size:15px;line-height:1.45}
  /* the rating keeps its display scale — it is the one hard fact on the page and
     it is never a stat in a card. Only the air around it comes down. */
  .rated .rating-fig{padding:20px 18px 22px}
  .rated .rsay{margin-top:12px}
  .rated .gmpill{margin-top:14px}
  /* 7,498 of a 7,500 ceiling is a coin flip on a different font render, so the
     phone case takes another ~70px out of the joins - never out of the content. */
  .shop .srow{gap:18px}
  /* the restored fact band costs 128px at 390 and put the page 26 over the 7,500
     ceiling. Taken back out of the fact rows' own leading and padding - the phone
     sizes these three rules used to carry were lost in the same splice. */
  .shop .facts{margin-top:16px}
  .fact{padding:10px 0 12px}
  .fact h3{font-size:16.5px}
  .fact p{font-size:15px;line-height:1.45}
  .visit .vgrid{gap:18px}
  .visit .vtext{gap:18px}
  .pcap{margin-top:9px}
  .rqhead{margin-top:18px;padding-top:14px}
  .rated .rq{padding:13px 14px}
  .rated .rq blockquote{font-size:15.5px;line-height:1.38}
  .rated .rq figcaption{margin-top:7px}
  .rated .rquotes{gap:2px}
  .rated .rating{margin-top:14px}
  .rqlead{margin-top:18px;font-size:14px;letter-spacing:.14em}
  .formpanel{padding:18px}
  .formpanel .field label{font-size:12px}
  .formpanel .field input,.formpanel .field select,.formpanel .field textarea{min-height:48px}
  .formpanel .fineprint{font-size:13px}
  .hask{font-size:14.5px}
  .area p{font-size:15px}
}

/* ============================================================= THE SLAM =====
   06's signature, and it is 06's alone.

   A block does not fade in. Its FILL — a separate layer carrying the block's
   colour — is parked one full width off the side the block sits on, and when the
   block enters view the fill slides to 0 and STOPS: cubic-bezier(.86,.01,.24,1)
   spends almost all of the 380ms in the first third of the distance and then dead
   stops. No easing tail, no overshoot, no bounce. The type inside lands one beat
   afterwards on the shared .reveal delay, which is the tested mechanism and not a
   second one invented here.

   Left, right, left, right down the page, so the direction alternates with the
   beat. The observer is attached to the BLOCK — a full-size box — and animates
   the child; the fill itself is translated off its own parent and would report a
   zero-area intersection rect forever.

   THE NO-JS PATH: every rule is gated on html.js, which the inline head script
   sets only when JavaScript runs AND reduced motion is not requested. Without it
   the fill is simply at inset:0 and every block is already its final colour. */
html.js .slab .fill{transform:translateX(-102%)}
html.js .slab.right .fill{transform:translateX(102%)}
html.js .slab.slammed .fill{transform:translateX(0);
  transition:transform var(--beat) cubic-bezier(.86,.01,.24,1)}
/* the photograph settles rather than slides: it is the steady half of the beat. */
html.js .band img{transform:scale(1.035)}
html.js .band.shown img{transform:none;
  transition:transform 900ms cubic-bezier(.2,.75,.28,1)}
/* the type lands after the fill, using base.css's own reveal delays. */
html.js .slab .reveal{transition-delay:.26s}
html.js .slab .reveal.d1{transition-delay:.34s}
html.js .slab .reveal.d2{transition-delay:.42s}

/* THE CHECKERBOARD SLAMS CELL BY CELL. The cells come out of a shared component
   this template does not author, so the fill is a pseudo-element rather than a
   `.fill` span - same layer, same curve, same dead stop. The photo cells carry an
   ink CURTAIN that wipes off to the right instead, so a picture lands on the same
   beat as the block beside it rather than simply appearing. `.beats` is the
   observed host and it is a full-size box; nothing zero-area is ever observed.
   The stagger is written against `order`, not against DOM position, so the cells
   land left-to-right across the board however the two pools interleave. */
html.js .prices .bitem::after{transform:translateX(-102%)}
html.js .prices .beats > figure::after{transform:translateX(0)}
html.js .prices .beats.slammed .bitem::after{transform:translateX(0);
  transition:transform var(--beat) cubic-bezier(.86,.01,.24,1)}
html.js .prices .beats.slammed > figure::after{transform:translateX(102%);
  transition:transform var(--beat) cubic-bezier(.86,.01,.24,1)}
html.js .prices .bname,html.js .prices .bdesc,
html.js .prices .btag,html.js .prices .bprice{opacity:0}
html.js .prices .beats.slammed .bname,html.js .prices .beats.slammed .bdesc,
html.js .prices .beats.slammed .btag,html.js .prices .beats.slammed .bprice{
  opacity:1;transition:opacity .3s ease .22s}

@media(prefers-reduced-motion:reduce){
  html.js .slab .fill,html.js .slab.right .fill{transform:none;transition:none}
  html.js .band img,html.js .band.shown img{transform:none;transition:none}
  html.js .prices .bitem::after,html.js .prices .beats.slammed .bitem::after{transform:none;transition:none}
  html.js .prices .beats > figure::after,html.js .prices .beats.slammed > figure::after{transform:translateX(102%);transition:none}
  html.js .prices .bname,html.js .prices .bdesc,html.js .prices .btag,html.js .prices .bprice{
    opacity:1;transition:none}
}

/* the client's own mark, when they have one. NOTPHOTO by name and by nature: it
   is a fixed-size graphic, not a photographic slot, and it is supposed to have
   one shape. Absent, the header and the footer render the initials monogram. */
.brandmark{display:block;height:32px;width:auto}
.brandmark.big{height:46px}


/* ==================================================== 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}
}
