/* ============================================================================
   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:#F3EFE7;
  --surface-2:#EDE8DE;
  --band:#141210;
  --band-ink:#F3EFE7;
  --band-ink-2:#B7AFA1;
  --accent-on-band:#A98CFF;
  --ink:#1A1714;
  --ink-2:#6E6659;
  --ink-3:#706859;
  --accent:#5A3FCF;
  --accent-2:#3F2AA6;
  --accent-ink:#FFFFFF;
  --line:#DCD4C5;
  --maxw:1180px;
  --gutter:22px;
  --radius:2px;
  --accent-a12:rgba(90,63,207,.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:#ded4ca;
  --board-cell:#ebe6dd;
  --board-line:rgba(90,63,207,.30);
  --board-accent:#5A3FCF;
  --board-glow:rgba(90,63,207,.13);
  --board-shadow-1:rgba(26,23,20,.08);
  --board-shadow-2:rgba(26,23,20,.26);
  --font-display:'Gloock',Georgia,'Times New Roman',serif;
  --font-body:'Manrope',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:400;letter-spacing:-.014em}
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(90,63,207,.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:#8C1D2F;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:68px}
.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}
}


/* ============================================================================
   05 — THE CLOSE.  base.css is PREPENDED to this file, then shared/rating.css;
   shared/mosaic.css and shared/pghnote.css load AFTER it. So this sheet SKINS —
   it does not rebuild anything the floor already owns.

   THE DIRECTION. Very few photographs, each one enormous and cropped hard onto a
   detail. The page is long stretches of quiet cream typography and then, without
   warning, a picture the height of the screen with one caption line under it.
   Nothing is boxed, nothing is carded, nothing is gridded. The ONLY contained
   object on the whole page is the shared services board, which is what makes it
   unmistakably the prices; even the hours are set large in Gloock on hairlines.

   THE SIGNATURE is THE FOCUS PULL — see the last block in this file. It is the
   only mechanical move here and no other skeleton may use it.

   NO WORD IS EVER SET ON A PHOTOGRAPH. There is no scrim, no tint, no duotone and
   no gradient over any image in this file: the pictures keep their own colour and
   the type keeps its own ground, so the hero needs no measured floor.
   ========================================================================== */

:root{
  /* the page's own gutter. Wider than the shared --gutter on purpose: this layout
     is full-bleed cream with generous margins, not a centred container. */
  --pad:clamp(20px,5.4vw,86px);
  /* THE CROP WINDOW. An svh figure on .cut, which is NOT a photographic frame —
     it is a window with overflow:hidden. The picture inside keeps its own
     proportion and its own height; see THE CROPS below.

     MEASURED 2026-08-15, and this is the phone figure, not the desktop one. At
     68svh a crop was 574px of an 844px screen and the page ran to 9,340px against
     a 7,500px contract ceiling. TWO crops at most-of-a-screen each is what a phone
     was paying for a device that belongs to a desktop layout, so the phone step is
     34svh (287px) and the desktop steps are untouched. --cut-z goes with it: 1.5
     existed only because a 4:5 plate is 469px tall at 375 wide and could not fill a
     574px window; at 287px it overflows on its own, so the phone zoom — and the
     ±94px of bleed either side of the window that came with it — is gone. */
  --cut-max:34svh;
  --cut-z:1;
}
@media(min-width:620px){ :root{--cut-max:56svh;--cut-z:1} }
@media(min-width:900px){ :root{--cut-max:82svh;--cut-z:1} }
@media(min-width:1400px){ :root{--cut-max:86svh} }

/* --------------------------------------------------------------- the ground */
body{background:var(--surface);color:var(--ink);font-size:16.5px;line-height:1.66}
h1,h2,h3{font-weight:400}
::selection{background:var(--accent);color:var(--accent-ink)}

.pad{padding-left:var(--pad);padding-right:var(--pad)}
.measure{max-width:52ch}

.h-lg{font-size:clamp(30px,5vw,62px);line-height:1.04;letter-spacing:-.014em;max-width:17ch}
.h-md{font-size:clamp(26px,3.6vw,44px);line-height:1.08;letter-spacing:-.012em;max-width:19ch}

/* the eyebrow. base.css's .eyebrow is not used anywhere in this skeleton — this
   is the same idea at this template's tracking, and it is always the violet. */
.elab{margin:0 0 clamp(14px,2vw,22px);font-family:var(--font-body);font-size:10.5px;
  font-weight:700;letter-spacing:.3em;text-transform:uppercase;color:var(--accent)}

.lead{font-size:16.5px;line-height:1.66;color:var(--ink-2)}
@media(min-width:760px){.lead{font-size:18.5px}}

/* base.css's mobile .sec is 64px. Seven sections of it put a 390 page well over
   the contract's 7,500px ceiling, so the phone step is trimmed and the two
   desktop steps are RESTATED — base's media queries sit earlier in the built
   file and would otherwise win on equal specificity. */
.sec{padding:48px 0}
@media(min-width:760px){.sec{padding:92px 0}}
@media(min-width:1180px){.sec{padding:124px 0}}
.shead{max-width:900px}
.shead .h-lg,.shead .h-md{margin:0}
.shead .lead{margin:clamp(16px,2vw,24px) 0 0}

/* ------------------------------------------------------------ template bar */
.tplbar{position:fixed;top:0;left:0;right:0;z-index:130;background:var(--band);
  color:var(--band-ink-2);font-family:var(--font-body);font-size:12px;font-weight:500;
  line-height:1.45;padding:8px var(--pad);text-align:center}
.tplbar b{color:var(--band-ink);font-weight:700}
:root:has(.tplbar){--tpl-h:54px}
@media(min-width:760px){:root:has(.tplbar){--tpl-h:34px}}

/* --------------------------------------------------------------------- nav --
   No `position` and no `top` is declared here: the shared floor owns both and
   houserules.py enforces it. What this template does own is the WIDTH. From
   900px the bar is clamped to exactly the hero's cream column so it can never
   cross onto the photograph, and its right edge carries the same hairline seam
   the split carries — which is why it stays clamped all the way down the page. */
#nav{padding:var(--tpl-h,0px) var(--pad) 0;background:var(--surface)}
#nav.scrolled{box-shadow:0 1px 0 0 var(--line)}
@media(min-width:900px){
  #nav{width:calc(100% * .66 / 1.66);padding-right:0}
  #nav::after{content:"";position:absolute;top:var(--tpl-h,0px);bottom:0;right:0;
    width:1px;background:var(--line)}
}
.nav-inner{gap:10px 16px;flex-wrap:wrap}
.brand{display:inline-flex;align-items:center;min-height:44px;text-decoration:none;
  color:var(--ink)}
.brandtype{font-family:var(--font-display);font-size:19px;line-height:1;
  letter-spacing:-.012em}
.markimg{height:34px;width:auto}
.navtel{font-family:var(--font-body);font-size:12px;font-weight:700;letter-spacing:.14em;
  text-decoration:none;color:var(--ink-2);margin-left:auto}
.navtel:hover{color:var(--accent)}
.navtoggle{color:var(--ink)}
.bars{display:flex;flex-direction:column;gap:5px;width:22px}
.bars i{display:block;height:1px;background:currentColor}

/* THE DRAWER. Below 1180 the links live behind the toggle, later than the house
   940, and the figure is not a preference — it is measured. The bar is clamped to
   .66/1.66 of the viewport and loses var(--pad) off its left edge, so the row has
   0.3436 * W to live in: 405px at 1180 and 495px at 1440. The wordmark, three
   links at 9.5px/.14em and the CTA come to ~393px. At the house 940 the same row
   would have had 323px and would have wrapped the whole drawer onto a second
   line on every desktop between 940 and 1250.
   `display` is the only thing overridden and both rules are scoped to #nav so they
   meet base's desktop reset at its own specificity. */
@media(max-width:1179px){
  #nav .nav-links{display:none}
  #nav .nav-links.open{display:flex}
  .navtoggle{display:inline-flex}
  .nav-links{flex-basis:100%}
}
.nav-links{flex-direction:column;padding:6px 0 16px;border-top:1px solid var(--line)}
.nav-links a{padding:13px 0;font-family:var(--font-body);font-size:12px;font-weight:700;
  letter-spacing:.22em;text-transform:uppercase;text-decoration:none;color:var(--ink)}
.nav-links a:hover{color:var(--accent)}
.drawhours{margin:12px 0 4px;font-family:var(--font-body);font-size:10px;font-weight:700;
  letter-spacing:.24em;text-transform:uppercase;color:var(--ink-3)}
.navcta{align-self:flex-start;margin-top:12px;padding:13px 22px;border-radius:999px;
  background:var(--accent);color:var(--accent-ink)}
.navcta:hover{background:var(--accent-2);color:var(--accent-ink)}
@media(min-width:1180px){
  #nav{--nav-gap:16px}
  /* one line, always. The wordmark is the only shrinkable thing in the row, so a
     long trading name ellipsises instead of pushing the CTA off the bar — which is
     what wrapping would do inside a masthead this narrow. */
  .nav-inner{flex-wrap:nowrap}
  .brand{min-width:0;flex:0 1 auto;overflow:hidden}
  .brandtype{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  #nav .nav-links a{flex:none;padding:0;font-size:9.5px;letter-spacing:.14em}
  /* a display:none that has to beat `.nav-links a` needs two classes — .drawtel
     alone is (0,1,0) and loses. Scoped to #nav, so (1,1,0). */
  #nav .drawhours,#nav .drawtel,#nav .navtel{display:none}
  /* THE PILL IS NOT IN THE BAR. It is in the hero, under the hours and on the map
     link; a fourth one in a 40%-wide masthead is 32px of padding this row does not
     have. The CTA is the violet word with a rule under it, which is louder here
     than a pill would be because nothing else in the bar is coloured. */
  #nav .navcta{align-self:auto;margin:0;padding:0;background:none;border-radius:0;
    color:var(--accent);border-bottom:1px solid var(--accent)}
  #nav .navcta:hover{background:none;color:var(--accent-2);border-bottom-color:var(--accent-2)}
}

/* -------------------------------------------------------------------- hero --
   The split. Below 900px the cream type block sits on top and the crop takes the
   rest of the first screen; from 900px they are two columns and the crop is on
   the right. min-height rather than height, so a long headline grows the hero
   instead of clipping it. */
.hero{display:grid;grid-template-rows:auto minmax(200px,1fr);min-height:100svh;
  background:var(--surface)}
.herosay{display:flex;flex-direction:column;justify-content:center;
  padding:calc(var(--tpl-h,0px) + clamp(74px,11vh,96px)) var(--pad) clamp(26px,4vh,40px)}
.herokick{margin:0 0 clamp(16px,2.4vw,26px);font-family:var(--font-body);font-size:10.5px;
  font-weight:700;letter-spacing:.3em;text-transform:uppercase;color:var(--accent)}
.hero h1{font-size:clamp(40px,7.4vw,96px);line-height:1.0;letter-spacing:-.014em;
  margin:0;max-width:13ch}
/* THE DRAWN MOMENT — THE UNDERCUT. The payload phrase drops to its own line at
   .62em and takes the violet: it is the SMALLEST thing in the sentence and the
   only coloured one. Emphasis by getting quieter, not louder. No stroke, no bar,
   no highlight, no mark in the margin — this template draws nothing. */
.h1b{display:block;font-style:normal;font-size:.62em;letter-spacing:0;color:var(--accent)}
.herosub{max-width:40ch;margin:clamp(18px,3vw,30px) 0 0;font-size:16.5px;line-height:1.66;
  color:var(--ink-2)}
.hero .btnrow{margin-top:clamp(22px,3.4vw,34px)}
.herocrop{position:relative;overflow:hidden;background:var(--band)}
/* THE ONE object-fit in this file, and it is the hero — art-directed by us and
   exempt by name. Nothing below the hero crops an asset to a shape. */
/* THE PICTURE IS OUT OF FLOW, AND THAT IS THE FIX, NOT A TIDY-UP. Measured
   2026-08-15: at 1440x900 the hero was 1,651px — 858px of crop column times the
   1539/800 ratio the <img>'s own width/height attributes declare. A picture in
   flow gives the crop column an INTRINSIC HEIGHT, and an intrinsic height beats
   height:100% whenever the grid row is content-sized: before the wide source has
   decoded, while it is loading, if it is blocked, or if it never arrives, the
   column sizes itself from the placeholder ratio and the hero grows to whatever
   that says. Both hero CTAs landed 290px under the fold.
   position:absolute takes the picture out of intrinsic sizing entirely, so the
   crop column can contribute NO height in ANY of those states and the hero is the
   row height and nothing else. .herocrop is already position:relative. */
.herocrop picture{position:absolute;inset:0}
.herocrop picture,.herocrop img{display:block;width:100%;height:100%;object-fit:cover;
  object-position:56% 42%}

@media(min-width:900px){
  /* ONE row, minmax(0,1fr) — NEVER `none`. An implicit auto row is content-sized,
     which is the door the crop column walked through; a 1fr row resolves against
     the container, so the hero is max(600px, 100svh) and grows only if the CREAM
     column's own type outgrows it, which is the behaviour the min-height is for. */
  .hero{grid-template-rows:minmax(0,1fr);grid-template-columns:.66fr 1fr;
    min-height:max(600px,100svh);align-items:stretch}
  /* THE WHOLE COLUMN IS CAPPED BY THE VIEWPORT'S HEIGHT, not only its width — that
     is what keeps the buttons above the fold on a 720-tall laptop, where the
     uncapped 96px setting put them 11px under it. Every vertical figure below is
     min(its clamp, a vh), and this padding is the last of them: 68px at 720 tall,
     124px at 1440. The demo template bar costs another 34px that a live build
     does not pay. */
  .herosay{padding:calc(var(--tpl-h,0px) + max(52px,min(9.5vh,124px))) var(--pad) clamp(26px,4vh,52px)}
  .herocrop{order:2;height:100%}
  /* THE HERO TYPE IS CAPPED BY THE VIEWPORT'S HEIGHT, not only its width. On a
     1440x720 laptop the uncapped 96px setting pushed the buttons 11px under the
     fold — so every vertical figure in this column is min(its clamp, a vh). */
  .hero h1{font-size:min(clamp(40px,7.4vw,96px),12vh)}
  .herokick{margin-bottom:min(clamp(16px,2.4vw,26px),3vh)}
  .herosub{margin-top:min(clamp(18px,3vw,30px),3.4vh)}
  .hero .btnrow{margin-top:min(clamp(22px,3.4vw,34px),4.2vh)}
}

/* ------------------------------------------------------------- the buttons --
   Pill-shaped, and that is part of this template's voice. --radius stays at 2px
   because it is what the form fields and the mosaic tiles read; only the buttons,
   the nav CTA and the map link take the pill. */
.btn{border-radius:999px;padding:15px 26px;font-family:var(--font-body);font-weight:700;
  font-size:11.5px;letter-spacing:.2em;text-transform:uppercase}
.btn-primary{background:var(--accent);color:var(--accent-ink);border-color:var(--accent)}
.btn-primary:hover{background:var(--accent-2);border-color:var(--accent-2)}
.btn-ghost{border-color:var(--line);color:var(--ink);background:transparent}
.btn-ghost:hover{border-color:var(--ink)}

/* ---------------------------------------------------------------- THE CROPS --
   THE CROP IS A WINDOW, NOT A SHAPE. orientcheck.py forbids aspect-ratio,
   object-fit and a fixed height on any photographic frame below the hero, and it
   is right to: a frame that declares its own proportion is a hole, and whatever
   the client sends gets cut to fit it.

   So the height lives on .cut — a flex WINDOW with overflow:hidden, not a frame —
   and the picture inside keeps width and height:auto, which means it is never
   distorted and never squeezed into a box it does not fit:
     · asset taller than the window  -> hard crop, centred (or .cut-t / .cut-b)
     · asset shorter than the window -> the window closes down onto it and prints
                                        it WHOLE, full bleed, uncropped
   A client with only landscape photographs therefore gets shorter, complete
   pictures rather than a broken layout or a decapitated subject.

   --cut-z IS NOW 1 AT EVERY WIDTH and the token stays only because the phone is one
   contract change away from wanting it back. It was 1.5 below 620px to force a 4:5
   plate (469px tall at 375 wide) to fill a 574px window. The phone window is 287px
   now, every plate overflows it unaided, and the zoom's cost was a picture 563px
   wide inside a 375px window — 94px of bleed clipped off each side of the client's
   photograph at the one width where they are most likely to look at it. */
.cutblock{margin:0}
.cut{display:flex;align-items:center;overflow:hidden;max-height:var(--cut-max);
  background:var(--band)}
.cut img{flex:none;width:calc(100% * var(--cut-z));max-width:none;height:auto;
  margin-left:calc((100% - 100% * var(--cut-z)) / 2)}
.cut-t{align-items:flex-start}
.cut-b{align-items:flex-end}

/* THE CAPTION under every crop, and under the wall — the connective tissue of
   the whole page. An accent label and one sentence in the display face. Both are
   CONTRACT DATA, never a caption typed into this skeleton. */
.bigcap{margin:0;padding:16px var(--pad) 0;display:flex;gap:8px 26px;align-items:baseline;
  flex-wrap:wrap}
.bigcap b{font-family:var(--font-body);font-size:10px;font-weight:700;letter-spacing:.24em;
  text-transform:uppercase;color:var(--accent);white-space:nowrap}
.bigcap span{font-family:var(--font-display);font-size:clamp(19px,2.6vw,32px);
  line-height:1.24;letter-spacing:-.012em;max-width:46ch}

/* ----------------------------------------------------------- THE FIGURE ---- 
   The Google rating, high on the page, given a screen of cream to itself. It is
   never a stat in a card, and in this template that means the card is TAKEN AWAY:
   the ground, the shadow and the rim are all off and the figure is simply the
   largest thing on the page after the headline. The review slots under it are
   hairline rules on cream, not tiles. Skinned entirely through the five shared
   tokens plus scoped rules — shared/rating.css is never copied into this file. */
.score{
  --rating-bg:transparent;
  --rating-shadow:none;
  --rating-rim:transparent;
  --rq-bg:transparent;
  --rq-ink:var(--ink);
}
.scorehold{max-width:1120px}
.score .rating{margin-top:clamp(22px,3.4vw,44px)}
.score .rating-fig{padding:0;border-radius:0;overflow:visible}
.score .rating-fig::after{display:none}
.score .rnum{letter-spacing:-.05em;color:var(--ink)}
.score .rsay{color:var(--ink-2)}
.score .rquotes{gap:0;margin-top:clamp(26px,3.6vw,46px);max-width:820px}
.score .rq{padding:22px 0;border-radius:0;border-top:1px solid var(--line)}
.score .rq blockquote{font-size:clamp(17px,1.9vw,22px);line-height:1.34}
.score .rq figcaption{color:var(--ink-3)}
.score .rq-empty{border:0;border-top:1px dashed var(--line)}
/* THE ATTRIBUTION PILL. Google's asset is shipped unmodified and it is the WHITE
   one, so on a cream page the pill is given a ground the white mark can live on:
   the same near-black the crops sit behind while they load. It is the only dark
   object on the page that is not a photograph, and that is deliberate — it reads
   as Google's, not as ours. */
.score .gmpill{background:var(--band);border-color:var(--band);padding:9px 16px 9px 14px}
.score .gmpill:hover{background:var(--ink)}
.score .gmpill .gmgo{font-family:var(--font-body);font-weight:700;font-size:11px;
  letter-spacing:.14em;text-transform:uppercase;color:var(--band-ink)}
.score .gmpill:hover .gmgo{color:var(--accent-on-band)}
@media(min-width:900px){
  .score .rating-fig{column-gap:clamp(40px,6vw,90px)}
}

/* -------------------------------------------------------------- THE LIST ----
   The shared services board, and the only contained object on the page.
   THE TRAY DECLARES ITS OWN SECONDARY AND TERTIARY. build.py derives --board from
   --surface by an HSL lightness step plus a 5% accent tint and lands on #ded4ca;
   the page's --ink-2 is only 3.88:1 there. Darkening --ink-2 globally would have
   changed the cream page's body colour, which is carried from the approved
   mockup, so the two colours that are only ever used INSIDE the tray are declared
   on the tray. Both are real contrast_pairs in template.json.
   Every rule here is scoped to .prices — base.css owns these class names and a
   bare rule would redefine the services section for every build in the library. */
.prices .board{
  --bd-sec:#4E4739;
  --bd-ter:#5C5446;
  --bd-gap:0px;
  --bd-pad:0px;
  --bd-cell-pad:clamp(18px,2.4vw,26px) 0;
  --bd-price:clamp(23px,2.9vw,34px);
  --bd-radius:0px;
  --bd-icon:0px;
  max-width:1120px;
  background:transparent;
  border:0;
  border-top:1px solid var(--line);
  box-shadow:none}
/* The rows are RULED, not tiled: one column of hairlines, the way a priced list
   is set on paper. The tray keeps the shared markup and the shared grid — only
   its surfaces are taken away. */
.prices .bgrid{--bd-cols:1;gap:0}
@media(min-width:820px){.prices .board{--bd-cols:1}}
.prices .bitem{grid-template-columns:minmax(0,1fr) auto;column-gap:clamp(18px,3vw,40px);
  align-items:baseline;background:transparent;border-radius:0;
  border-bottom:1px solid var(--line);overflow:visible}
.prices .bitem::before{display:none}
/* THE GLYPH IS OFF. One icon per row is the house default and it is right on a
   panelled board; on a page whose entire argument is that nothing is boxed, a
   column of 46px plates would be the only repeated graphic in the file. The
   partial still emits it — it is hidden here, not forked out. */
.prices .bicon{display:none}
.prices .bmain{grid-column:1}
.prices .bprice{grid-column:2}
.prices .bname{font-family:var(--font-display);font-weight:400;
  font-size:clamp(23px,2.9vw,34px);line-height:1.14;letter-spacing:-.012em;color:var(--ink)}
.prices .bdesc{margin:6px 0 0;font-size:15px;line-height:1.6;color:var(--bd-ter);max-width:56ch}
.prices .btag{margin:11px 0 0;padding:0;border:0;border-radius:0;background:none;
  font-size:9.5px;letter-spacing:.24em;color:var(--bd-sec)}
.prices .bprice{font-family:var(--font-display);font-weight:400;letter-spacing:-.02em;
  color:var(--accent)}
.prices .bgh{margin:clamp(26px,3.4vw,40px) 0 12px;color:var(--accent);letter-spacing:.24em;
  font-size:10.5px}
.prices .bhook{margin:0 0 clamp(20px,2.6vw,30px);padding:0 0 clamp(18px,2.4vw,26px);
  border:0;border-bottom:1px solid var(--line);border-radius:0}
.prices .bhook-l{color:var(--accent);letter-spacing:.24em;font-size:10px}
.prices .bhook-t{max-width:58ch;color:var(--bd-sec);font-size:15px}
.boardhold{margin-top:clamp(32px,5vw,64px)}
.pfoot{margin:clamp(26px,3.4vw,44px) 0 0;font-family:var(--font-display);
  font-size:clamp(18px,2.2vw,25px);line-height:1.38;letter-spacing:-.01em;
  max-width:38ch;color:var(--ink-2)}
/* the crop that interrupts this section sits between the heading and the board */
.prices .cutblock{margin:clamp(34px,5vw,72px) 0 0}

/* -------------------------------------------------------------- THE SHOP ----
   Prose, then the wall bled to both edges, then the three facts as typographic
   RUNS and the review slot as a pull-quote. */
.ourplates{margin:clamp(22px,3vw,34px) 0 0;font-size:15px;line-height:1.62;
  color:var(--ink-3);max-width:60ch}
.wall{margin-top:clamp(30px,4.6vw,64px)}
/* THE WALL IS THE TEMPLATE'S TO FRAME, NOT TO LAY OUT. It bleeds to both edges of
   the viewport here, and paying for the row height is this template's own job —
   the shared clamp is built for a CONTAINED wall and would read as a strip of
   stamps at full bleed. Two tokens and two row figures, nothing else. */
.wall .mos{--mos-gap:5px;--mos-gap-sm:5px;--mos-sm-ar:1/1;--mos-text-bg:var(--surface-2);
  grid-auto-rows:clamp(150px,19vw,290px);
  grid-template-rows:repeat(var(--mos-rows,3),clamp(150px,19vw,290px))}
.wall .mos-t{border-radius:0;background:var(--band)}
.shop .bigcap{margin-top:0}

.runs{margin:clamp(40px,6vw,86px) 0 0;border-top:1px solid var(--line)}
.run{display:grid;gap:6px 40px;padding:clamp(20px,2.6vw,30px) 0;
  border-bottom:1px solid var(--line)}
.run h3{font-family:var(--font-display);font-weight:400;font-size:clamp(20px,2.3vw,27px);
  line-height:1.16;letter-spacing:-.012em}
.run p{margin:0;font-size:15.5px;line-height:1.62;color:var(--ink-2);max-width:56ch}
@media(min-width:820px){
  .run{grid-template-columns:minmax(0,22ch) minmax(0,1fr);align-items:baseline}
}
.pull{margin:clamp(40px,6vw,80px) 0 0;max-width:44ch}
.pull blockquote{margin:0;font-family:var(--font-display);font-weight:400;
  font-size:clamp(21px,3vw,38px);line-height:1.22;letter-spacing:-.014em;color:var(--ink)}
.pull figcaption{margin-top:16px;font-family:var(--font-body);font-size:10.5px;
  font-weight:700;letter-spacing:.24em;text-transform:uppercase;color:var(--accent)}

/* ------------------------------------------------------ WHAT PEOPLE ASK ---- */
.askcall{margin:clamp(14px,2vw,20px) 0 0;font-size:15px;color:var(--ink-3)}
.askcall a{color:var(--accent);text-decoration:none;border-bottom:1px solid var(--line)}
.asklist{margin:clamp(28px,4vw,52px) 0 0;max-width:940px;border-top:1px solid var(--line)}
.ask{border-bottom:1px solid var(--line)}
.ask summary{display:flex;align-items:baseline;justify-content:space-between;gap:24px;
  padding:clamp(17px,2.2vw,24px) 0;cursor:pointer;list-style:none;min-height:48px;
  font-family:var(--font-display);font-size:clamp(18px,2.1vw,25px);line-height:1.24;
  letter-spacing:-.01em}
.ask summary::-webkit-details-marker{display:none}
.ask summary i{flex:none;position:relative;width:13px;height:13px;margin-top:8px}
.ask summary i::before,.ask summary i::after{content:"";position:absolute;
  background:var(--accent);transition:transform .26s ease}
.ask summary i::before{top:6px;left:0;width:13px;height:1px}
.ask summary i::after{top:0;left:6px;width:1px;height:13px}
.ask[open] summary i::after{transform:scaleY(0)}
.ask p{margin:0 0 clamp(18px,2.2vw,24px);max-width:64ch;font-size:15.5px;line-height:1.66;
  color:var(--ink-2)}

/* -------------------------------------------------------------- OPEN -------
   The week and the address in the display face, on hairlines. Not a card. */
.week{margin:clamp(26px,3.6vw,46px) 0 0;padding:0;list-style:none;
  border-top:1px solid var(--line);max-width:820px}
.wrow{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px 28px;
  align-items:baseline;margin:0;padding:clamp(14px,1.9vw,22px) 0;
  border-bottom:1px solid var(--line)}
.wd,.wt{font-family:var(--font-display);font-weight:400;font-size:clamp(20px,2.6vw,33px);
  line-height:1.16;letter-spacing:-.012em}
.wt{color:var(--accent)}
.wrow-line{grid-template-columns:minmax(0,1fr)}
.weekask{margin:clamp(14px,2vw,20px) 0 0;max-width:52ch;font-size:15px;line-height:1.6;
  color:var(--ink-3)}
.findit{margin:clamp(34px,5vw,64px) 0 0}
.bigline{font-family:var(--font-display);font-weight:400;font-style:normal;
  font-size:clamp(22px,3vw,40px);line-height:1.2;letter-spacing:-.014em;max-width:22ch}
.crow{display:flex;flex-wrap:wrap;gap:6px 34px;margin:clamp(16px,2.2vw,24px) 0 0}
.cbig{font-family:var(--font-display);font-size:clamp(18px,2.1vw,26px);line-height:1.3;
  text-decoration:none;color:var(--accent);border-bottom:1px solid var(--line)}
.cbig:hover{color:var(--accent-2);border-bottom-color:var(--accent-2)}
.maplink{display:inline-flex;align-items:center;min-height:48px;margin-top:clamp(20px,2.6vw,30px);
  padding:14px 24px;border:1px solid var(--line);border-radius:999px;
  font-family:var(--font-body);font-size:11px;font-weight:700;letter-spacing:.2em;
  text-transform:uppercase;text-decoration:none;color:var(--ink)}
.maplink:hover{border-color:var(--accent);color:var(--accent)}
.area{margin:clamp(30px,4vw,52px) 0 0;padding-top:clamp(22px,3vw,32px);
  border-top:1px solid var(--line);max-width:60ch}
.area h3{font-family:var(--font-display);font-weight:400;font-size:clamp(19px,2.2vw,26px);
  line-height:1.18}
.area p{margin:10px 0 0;font-size:15.5px;line-height:1.62;color:var(--ink-2)}
.towns{display:flex;flex-wrap:wrap;gap:6px 20px;margin-top:14px}
.towns span{font-family:var(--font-body);font-size:10.5px;font-weight:700;
  letter-spacing:.2em;text-transform:uppercase;color:var(--ink-3)}
@media(min-width:900px){
  .whenwhere{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);
    column-gap:clamp(48px,7vw,110px);align-items:start}
  .findit{margin-top:clamp(26px,3.6vw,46px)}
}

/* ------------------------------------------------------------- THE CLOSE --- */
.close{padding:0 0 clamp(52px,8vw,124px);background:var(--surface)}
.closein{padding-top:clamp(40px,6vw,90px)}
.formhold{margin:clamp(30px,4.4vw,58px) 0 0;max-width:760px}
.form{gap:18px}
.field label{font-family:var(--font-body);font-size:10.5px;letter-spacing:.22em;
  color:var(--ink-3)}
.field input,.field select,.field textarea{background:var(--surface);
  border-color:var(--line);font-family:var(--font-body)}
.fineprint{margin:4px 0 0;font-size:13.5px;line-height:1.6;color:var(--ink-3)}
.thanks{background:var(--surface-2);border-color:var(--line);border-radius:var(--radius)}
.thanks h3{font-family:var(--font-display);font-weight:400;font-size:clamp(23px,2.6vw,32px)}

/* ------------------------------------------------------------------ footer - */
footer{padding:clamp(46px,6vw,86px) 0 clamp(34px,4vw,52px);border-top:1px solid var(--line)}
.fgrid{display:grid;gap:clamp(28px,4vw,44px)}
@media(min-width:820px){
  .fgrid{grid-template-columns:minmax(0,1.4fr) minmax(0,1fr) minmax(0,1fr);
    gap:clamp(30px,4vw,60px)}
}
.fname{margin:0;font-family:var(--font-display);font-size:clamp(24px,2.8vw,34px);
  line-height:1.14;letter-spacing:-.014em}
.fabout{margin:12px 0 0;max-width:40ch;font-size:15px;line-height:1.62;color:var(--ink-2)}
.fh{margin:0 0 14px;font-family:var(--font-body);font-size:10px;font-weight:700;
  letter-spacing:.26em;text-transform:uppercase;color:var(--accent)}
.fcol a,.fcol address,.fplain{display:block;font-size:15px;line-height:1.66;
  font-style:normal;color:var(--ink-2);text-decoration:none}
.fcol a:hover{color:var(--accent)}
.fnav{display:flex;flex-wrap:wrap;gap:10px 28px;margin:clamp(34px,4.4vw,56px) 0 0;
  padding-top:clamp(20px,2.6vw,28px);border-top:1px solid var(--line)}
.fnav a{font-family:var(--font-body);font-size:10.5px;font-weight:700;letter-spacing:.2em;
  text-transform:uppercase;text-decoration:none;color:var(--ink-2)}
.fnav a:hover{color:var(--accent)}
.fbase{display:flex;flex-wrap:wrap;gap:8px 24px;margin-top:clamp(20px,2.6vw,28px);
  font-size:13px;color:var(--ink-3)}
.fbase a{color:var(--ink-2)}

/* -------------------------------------------------------------- the 404 ---- */
.nf .herokick{color:var(--ink-3)}

/* ------------------------------------------------ THE PHONE'S OWN RHYTHM ----
   MEASURED, NOT ESTIMATED. 2026-08-15, real browser, same-origin iframe at exactly
   390x844: the page was 9,340px against the contract's 7,500px ceiling at 390.
   1,840px over, and none of it was copy — it was air and two crops.

   This block is the phone's vertical rhythm and NOTHING ELSE. Every figure in it is
   a value this sheet already sets; what changes is only how much of it a 390 screen
   pays. It is scoped to max-width:619px on purpose, so 620px up — and every desktop
   figure in this file — is byte-for-byte what it was. No type size that carries copy
   is touched: .lead, .run p, .ask p, .fabout and the form all keep their sizes. The
   two that do move are the crop caption's display line and the board's description,
   and both are secondary to a heading that keeps its scale.

   Where the 1,840px came from, measured section by section:
     hero      1,258 -> 844   the crop no longer sizes itself (see .herocrop picture)
     crops     574 -> 287     each, twice
     the rest  ~470           section padding and fifteen vertical margins
   Landing figure: 7,478px. The headroom over the ceiling is 22px, which is thin —
   a client with a longer services board or a fourth review will cross it, and the
   next lever after this one is content, not CSS. */
@media(max-width:619px){
  .sec{padding:28px 0}
  .elab{margin-bottom:11px}
  .shead .lead{margin-top:12px}
  .score .rating{margin-top:16px}
  .score .rquotes{margin-top:18px}
  .score .rq{padding:13px 0}
  .prices .cutblock{margin-top:24px}
  .boardhold{margin-top:22px}
  .prices .board{--bd-cell-pad:12px 0}
  .prices .bdesc{font-size:14.5px;line-height:1.55}
  .prices .btag{margin-top:8px}
  .prices .bhook{margin-bottom:15px;padding-bottom:13px}
  .prices .bgh{margin-top:19px}
  .pfoot{margin-top:18px}
  .wall{margin-top:22px}
  .wall .mos{grid-auto-rows:128px;grid-template-rows:repeat(var(--mos-rows,3),128px)}
  .runs{margin-top:26px}
  .run{padding:12px 0}
  .pull{margin-top:22px}
  .asklist{margin-top:20px}
  .ask summary{padding:11px 0}
  .ask p{margin-bottom:12px}
  .week{margin-top:18px}
  .wrow{padding:9px 0}
  .findit{margin-top:22px}
  .area{margin-top:20px;padding-top:16px}
  .closein{padding-top:24px}
  .formhold{margin-top:20px}
  .close{padding-bottom:32px}
  .form{gap:11px}
  .bigcap{padding-top:12px;gap:4px 26px}
  .bigcap span{font-size:17.5px}
  footer{padding:30px 0 24px}
  .fgrid{gap:18px}
  .fnav{margin-top:22px;padding-top:14px}
  .fbase{margin-top:14px}
}

/* ================================================== THE FOCUS PULL ==========
   THE SIGNATURE, and it belongs to this skeleton alone. A crop enters very
   slightly oversized and out of focus and resolves, ONCE, to sharp at true size.
   Nothing else on this page moves like that: every other section uses the house
   .reveal and nothing more.

   The blur lives on the <img>, never on the .cut window, for two reasons. The
   window is the element template.js observes, and an observer on something
   clipped or scaled to zero area can never fire; and blurring the window would
   soften its edges against the cream, which would turn a hard crop into a vignette.
   scale(1.07) is a little larger than the blur radius spreads, so the softened
   edge of the picture is always outside the window while the pull is running.

   The whole block is gated on html.js, which the inline head script sets ONLY
   when reduced motion is not requested — so no-JS, blocked JS and reduced motion
   all render every photograph sharp, immediately, with no transition to miss. */
html.js .cut img,html.js .herocrop img{transform:scale(1.07);filter:blur(10px);
  will-change:transform,filter}
html.js .cut.sharp img,html.js .herocrop.sharp img{transform:none;filter:none;
  transition:transform 1050ms cubic-bezier(.16,.8,.24,1),
             filter 900ms cubic-bezier(.16,.8,.24,1)}
@media(prefers-reduced-motion:reduce){
  html.js .cut img,html.js .herocrop img,
  html.js .cut.sharp img,html.js .herocrop.sharp img{
    transform:none!important;filter:none!important;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}
}
