/* ============================================================================
   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:#FCF5E9;
  --surface-2:#F4E6CE;
  --band:#58141F;
  --band-ink:#FBF1E4;
  --band-ink-2:#E6BEB4;
  --accent-on-band:#EFC469;
  --ink:#2A1810;
  --ink-2:#5A4231;
  --ink-3:#6D5340;
  --accent:#97202E;
  --accent-2:#73141F;
  --accent-ink:#FFF4E6;
  --line:#E2CFAE;
  --maxw:1240px;
  --gutter:22px;
  --radius:14px;
  --accent-a12:rgba(151,32,46,.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:#f2dabc;
  --board-cell:#f8ebd9;
  --board-line:rgba(151,32,46,.30);
  --board-accent:#97202E;
  --board-glow:rgba(151,32,46,.13);
  --board-shadow-1:rgba(42,24,16,.08);
  --board-shadow-2:rgba(42,24,16,.26);
  --font-display:'Petrona',Georgia,'Iowan Old Style','Times New Roman',serif;
  --font-body:'Hanken Grotesk',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:700;letter-spacing:-.012em}
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(151,32,46,.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:#97202E;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:72px}
.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)}


/* =============================================================================
   05 — THE ROOM.  MOBILE FIRST. Every query below is min-width; there is not one
   max-width query in this file, and there must never be.

   THE IDEA, in one line: you are standing in the room before you have read a word,
   and then the page DROPS onto warm paper and stays there.

   THE FIVE HOUSE RULES, and where each one is:
     1. PHOTOGRAPHS KEEP THEIR COLOUR — one --grade, and it is a whisper that moves
        saturation UP. No grayscale, no sepia, no hue-rotate, no duotone. grep it.
     2. THE GROUND IS A SATURATED BRAND COLOUR — --band claret carries the masthead,
        the review plate and the footer; --accent claret-red carries every button.
     3. FOUR COLOURS DOING REAL WORK — claret = the room, gold = the light and the
        money, pine = time, cream/sand = near paper and far paper. Delete any one and
        a category of information loses its signal.
     4. SERIF DISPLAY OVER A SANS BODY — Petrona over Hanken Grotesk.
     5. hero.h1b IS GOLD, ITALIC AND UNDERLINED — the one underline in the file.

   THE LIGHT LOGIC: the only lamp is inside the photograph. There is not one
   box-shadow, not one glow and not one gradient-as-surface below the hero. Depth is
   made ONLY by changing paper (cream -> sand -> claret) and by 1px hairlines. The
   two gradients in this file are the hero scrims and they exist to hit a measured
   contrast ratio, not to make a mood.
   ========================================================================== */

:root{
  /* THE FOURTH AND FIFTH COLOURS. base.css owns the thirteen shared tokens; these
     three are this skeleton's own and each one has a job.
       --gold      the money and the trust keys ON PAPER. Deliberately NOT --gold-lit:
                   #EFC469 on cream is 1.5:1, and shipping one gold for both surfaces
                   is the exact defect CLAUDE.md 1 was written about.
       --gold-lit  the light IN the room — gold on the photograph and on claret.
       --pine      time. Every duration chip, the week table, the open state. */
  --gold:#7C570C;
  --gold-lit:#EFC469;
  --pine:#245542;
  --pine-ring:rgba(36,85,66,.34);
  --gold-ring:rgba(124,87,12,.30);
  --hair-band:rgba(251,241,228,.16);
  /* the shade the two hero scrims are made of, as an rgb triple so the alpha can
     change per stop without a second colour ever entering the file */
  --shade:18,6,8;
  /* the two inks that only ever sit ON the photograph. They are not in
     contrast_pairs because a token pair cannot describe type over a picture: both
     were measured against the COMPOSITED pixels instead — see template.json
     contrast_note. */
  --on-photo:#FBF1E4;
  --on-photo-2:#F0DFCE;
  --grade:saturate(1.03) contrast(1.03) brightness(.97);
  --hair:1px solid var(--line);
  --navh:72px;
}

/* ---------------------------------------------------------- THE TEMPLATE BAR --
   Demo only. It is fixed, so #nav is pushed down by a matching padding-top
   published as --tpl-h. No `top` and no `position` is declared on #nav — the
   shared floor owns both and houserules.py enforces it. */
.tplbar{position:fixed;top:0;left:0;right:0;z-index:130;background:var(--accent-2);
  color:var(--band-ink);font-size:11.5px;line-height:1.42;letter-spacing:.015em;
  padding:9px var(--gutter);text-align:center}
:root:has(.tplbar){--tpl-h:64px}
@media(min-width:760px){:root:has(.tplbar){--tpl-h:32px}}
#nav{padding-top:var(--tpl-h,0px)}

/* -------------------------------------------------------------- THE MASTHEAD --
   Solid claret from the first pixel, the way a hotel's is. It was briefly
   transparent over the photograph and the top-right of the frame measured 3.2:1
   against cream links — a masthead that needs its own scrim is a second veil, and
   the brief allows exactly one. Nothing here is blurred or shadowed; scrolling
   draws a gold hairline underneath and changes nothing else. */
#nav{background:var(--band);border-bottom:1px solid transparent}
#nav.scrolled{border-bottom-color:var(--hair-band)}
.nav-inner{gap:12px;flex-wrap:wrap}
.brand{display:inline-flex;align-items:center;gap:11px;text-decoration:none;
  color:var(--band-ink);min-height:44px}
.mark{display:inline-flex;align-items:center;justify-content:center;
  width:38px;height:38px;border-radius:50%;background:var(--accent-on-band);
  color:var(--band);font-family:var(--font-display);font-weight:700;font-size:16px;
  letter-spacing:0;flex:0 0 auto}
.markimg{width:auto;height:34px;border-radius:0;background:none}
.brandtype{font-family:var(--font-display);font-weight:700;font-size:19px;
  letter-spacing:-.01em;color:var(--band-ink)}
.navtel{display:none}
.navtoggle{color:var(--band-ink)}
.navtoggle .bars{display:block;width:22px}
.navtoggle .bars i{display:block;height:1px;margin:6px 0;background:var(--band-ink)}
.navtoggle .bars i:nth-child(2){width:14px}
.nav-links{flex:1 0 100%;flex-direction:column;align-items:stretch;width:100%;
  padding:2px 0 14px;margin-top:4px;border-top:1px solid var(--hair-band)}
.nav-links a{color:var(--band-ink);text-decoration:none;font-size:16px;font-weight:500;
  padding:11px 0;min-height:44px;display:flex;align-items:center;
  border-bottom:1px solid var(--hair-band)}
.nav-links a:hover{color:var(--accent-on-band)}
.drawlab{color:var(--accent-on-band);margin:12px 0 2px;text-transform:uppercase;
  letter-spacing:.16em;font-weight:700;font-size:10.5px}
.drawhours{font-size:14px;color:var(--band-ink-2);margin:0 0 8px}
.drawtel{font-family:var(--font-display);font-size:22px;font-weight:700;
  letter-spacing:-.015em}
.navcta{background:var(--accent-on-band);color:var(--band);justify-content:center;
  border-radius:999px;margin-top:14px;font-family:var(--font-display);font-weight:700}
@media(min-width:940px){
  .nav-inner{flex-wrap:nowrap}
  .navtel{display:inline-block;font-family:var(--font-display);font-size:17px;
    font-weight:600;color:var(--band-ink-2);text-decoration:none;margin-left:auto}
  .navtel:hover{color:var(--accent-on-band)}
  .nav-links{flex:0 0 auto;width:auto;padding:0;margin:0;border-top:0}
  /* TWO CLASSES, NOT ONE. `.nav-links a` is (0,1,1) and beats a bare `.drawtel`
     (0,1,0) even inside this query — which is how a drawer's 22px telephone number
     ends up printed twice in a desktop masthead. It happened on 04. */
  .nav-links a{width:auto;padding:0;border-bottom:0;font-size:12px;font-weight:600;
    letter-spacing:.14em;text-transform:uppercase;color:var(--band-ink-2)}
  .nav-links .drawlab,.nav-links .drawhours,.nav-links .drawtel{display:none}
  .nav-links .navcta{height:40px;margin-top:0;padding:0 20px;font-size:14px;
    letter-spacing:.01em;text-transform:none;color:var(--band)}
}

/* -------------------------------------------------------------------- BUTTONS --
   Generous rounding, no shadow, no lift. A button changes COLOUR on hover because
   nothing on this page is allowed to cast. */
.btn{border-radius:999px;font-family:var(--font-display);font-weight:700;
  font-size:16px;letter-spacing:.005em;padding:15px 30px}
.btn-primary:hover{background:var(--accent-2);border-color:var(--accent-2)}
.btn-ghost{color:var(--on-photo);border-color:rgba(251,241,228,.55);background:transparent}
.btn-ghost:hover{border-color:var(--on-photo);background:rgba(251,241,228,.10)}

/* ==========================================================================
   THE ROOM — the hero. ONE photograph, 100svh, full colour strength.
   Two compositions, not one collapsed: below 900px the portrait cut with the type
   full width; from 900px the landscape cut with the type in the RIGHT column,
   because that is where the photograph's own dark side is at every desktop width.
   The type sits ON the picture (DESIGN-FLOOR A1 by construction) and the only
   thing between them is a feathered pool of shade fitted to the type block.
   THE HERO IS EXEMPT from the orientation rule by name, and from the reveal.
   ========================================================================== */
.hero{position:relative;isolation:isolate;overflow:hidden;min-height:100svh;
  display:flex;align-items:flex-start;background:var(--band)}
.heroshot{position:absolute;inset:0;display:block;z-index:0}
.heroimg{width:100%;height:100%;object-fit:cover;object-position:100% 50%;
  filter:var(--grade)}
/* THE SCRIM, PHONE. An ellipse with a five-stop feather, no straight edge in it,
   centred on the type block. Mean alpha over the first screen 0.659 — heavy, and
   honestly so: a full-width type block on a 390px frame has nowhere dark to sit,
   because the subject is dead centre in the portrait cut and no crop escapes it.
   Every stop was derived, not chosen: crop, grade and gradient were composited in
   numpy exactly as the browser will and the brightest pixel in the type block was
   measured at nine widths from 320 to 899. Worst 4.50:1, and 5.34:1 at 390. */
.heroveil{position:absolute;inset:0;z-index:1;pointer-events:none;
  background:radial-gradient(115% 86% at 40% 42%,
    rgba(var(--shade),.86) 0%, rgba(var(--shade),.76) 34%,
    rgba(var(--shade),.42) 66%, rgba(var(--shade),.12) 90%,
    rgba(var(--shade),0) 100%)}
.hero > .container{position:relative;z-index:2;width:100%}
.herotype{padding-top:calc(var(--tpl-h,0px) + var(--navh) + 28px);padding-bottom:44px}
.hkick{font-size:11.5px;font-weight:700;letter-spacing:.22em;text-transform:uppercase;
  color:var(--gold-lit);margin:0 0 16px}
.hero h1{font-size:46px;line-height:1.06;color:var(--on-photo);margin:0}  /* 46, not 44: DESIGN-FLOOR C2 wants the largest type at 2.5x the 17px body and
     44px is 2.59x — inside the rule with 0.09 of margin, which is not a margin. */
.h1a{color:var(--on-photo)}
/* THE DRAWN MOMENT. Gold, italic, underlined — a real text-decoration on ONE
   inline element, so a phrase that wraps carries its own line down with it and can
   never become two bars or a full-width slab under the last line (D1). It is the
   only underline in the file. */
.h1b{color:var(--gold-lit);font-style:italic;font-weight:600;
  text-decoration-line:underline;text-decoration-thickness:2px;
  text-underline-offset:.16em;text-decoration-skip-ink:auto;
  text-decoration-color:var(--gold-lit)}
.hsub{font-size:17px;line-height:1.58;color:var(--on-photo-2);margin:20px 0 26px;
  max-width:34em}
@media(min-width:640px){.hero h1{font-size:52px}}
@media(min-width:900px){
  .heroimg{object-position:50% 50%}
  /* THE SCRIM, DESKTOP. The same ellipse, fitted to the right-hand column and much
     lighter: mean alpha 0.341, so two thirds of the picture is untouched and every
     corner of the frame is clean. Worst case over ten desktop states 4.71:1. */
  .heroveil{background:radial-gradient(62% 68% at 78% 44%,
    rgba(var(--shade),.78) 0%, rgba(var(--shade),.68) 36%,
    rgba(var(--shade),.38) 66%, rgba(var(--shade),.10) 90%,
    rgba(var(--shade),0) 100%)}
  /* the type column is anchored to the VIEWPORT, not to the centred container:
     55vw is where the photograph goes dark, and a container-relative offset put the
     block back over the subject at 1024 and measured 2.4:1. */
  .hero > .container{max-width:none;padding-left:55vw;padding-right:var(--gutter)}
  .herotype{max-width:620px;
    padding-top:calc(var(--tpl-h,0px) + var(--navh) + 96px);padding-bottom:60px}
  .hero h1{font-size:clamp(48px,4.4vw,76px)}
  .hkick{margin-bottom:20px}
  .hsub{font-size:19px;margin:26px 0 32px}
}

/* ==========================================================================
   THE SEAM — THE DROP, made mechanical. The photograph ends and the very next
   element is the trust row, flush against its bottom edge with no gap above it:
   picture, then paper, in one step. Three proofs from story.rows[], key in gold.
   ========================================================================== */
.sec.seam{padding:26px 0 30px;background:var(--surface);border-bottom:var(--hair)}
.trust{display:grid;gap:16px}
.trust li{display:block}
.trust b{display:block;font-family:var(--font-display);font-size:19px;font-weight:700;
  color:var(--gold);letter-spacing:-.008em;margin-bottom:2px}
.trust span{display:block;font-size:14.5px;line-height:1.5;color:var(--ink-2)}
@media(min-width:760px){
  .sec.seam{padding:30px 0 34px}
  .trust{grid-template-columns:repeat(3,1fr);gap:34px}
}
@media(min-width:1180px){.trust{gap:56px}.trust b{font-size:21px}}

/* ---------------------------------------------------------- SECTION FURNITURE --
   One heading block shape, four paper changes. --surface is near paper, --surface-2
   is the same paper a step further into the room. */
.sec{padding:56px 0}
@media(min-width:760px){.sec{padding:84px 0}}
@media(min-width:1180px){.sec{padding:104px 0}}
.offer,.door{background:var(--surface-2)}
.elab{font-size:11px;font-weight:700;letter-spacing:.2em;text-transform:uppercase;
  color:var(--accent);margin:0 0 14px}
.h-lg{font-size:32px;line-height:1.1;color:var(--ink)}
.h-md{font-size:27px;line-height:1.12;color:var(--ink)}
.shead .lead{margin:16px 0 0}
.measure{max-width:56ch}
@media(min-width:760px){.h-lg{font-size:40px}.h-md{font-size:32px}}
@media(min-width:1180px){.h-lg{font-size:46px}.h-md{font-size:36px}}

/* ==========================================================================
   THE CARD — the sand paper, and the densest thing on the page. Six priced rows on
   hairlines. The figure is the display serif in paper-gold; the duration is a pine
   chip, because on this page pine means time. No imagery at all: the sections
   either side of it are photographic, so this is where the page holds still.
   ========================================================================== */
.card{margin-top:32px;border-top:2px solid var(--gold)}
.crow{display:grid;grid-template-columns:1fr auto;column-gap:20px;row-gap:6px;
  padding:20px 0;border-bottom:var(--hair);
  grid-template-areas:"name price" "tag price" "blurb blurb"}
.crow.notag{grid-template-areas:"name price" "blurb blurb"}
.cn{grid-area:name;font-size:21px;font-weight:600;color:var(--ink);letter-spacing:-.006em}
.ctag{grid-area:tag;justify-self:start;font-family:var(--font-body);font-size:11px;
  font-weight:600;letter-spacing:.13em;text-transform:uppercase;color:var(--pine);
  border:1px solid var(--pine-ring);border-radius:999px;padding:4px 11px;line-height:1.4}
.cp{grid-area:price;justify-self:end;align-self:start;font-family:var(--font-display);
  font-size:26px;font-weight:700;color:var(--gold);letter-spacing:-.01em;
  white-space:nowrap}
/* THE EMPTY PRICE. The column keeps its place and the figure is replaced by a rule
   the width of a price, sitting exactly where the number will sit — which is what
   an unpriced card in a room actually looks like. Never a blank cell, never a
   collapsed row, and never an invented figure. */
.cpblank{display:block;padding-top:12px}
.cpblank i{display:block;width:66px;height:2px;background:var(--gold);opacity:.85}
.cb{grid-area:blurb;margin:4px 0 0;font-size:15.5px;line-height:1.55;color:var(--ink-2);
  max-width:62ch}
/* THE NOTE ON THE CARD — the reply hook. Emitted inside the services loop on every
   unpriced row and deduped here with the general sibling combinator, so exactly one
   lands immediately above the first row with no figure. No :has(), no JS, no field. */
.cnote{display:block;background:var(--surface);border:var(--hair);
  border-radius:var(--radius);padding:20px 22px;margin:0 0 6px}
.cnote ~ .cnote{display:none}
.cnotep{font-size:15.5px;line-height:1.6;color:var(--ink-2);margin:0 0 12px;max-width:62ch}
.cnotep b{color:var(--ink);font-weight:700}
.cnotego{display:inline-block;font-family:var(--font-display);font-weight:700;
  font-size:15px;color:var(--accent);text-decoration:none;border-bottom:2px solid var(--gold);
  padding-bottom:2px;min-height:44px}
.cnotego:hover{color:var(--accent-2)}
.mgrp{margin-top:34px}
.mgh{font-size:20px;color:var(--ink);margin-bottom:10px}
.mrows{border-top:var(--hair)}
.mrow{display:grid;grid-template-columns:1fr auto;column-gap:18px;padding:13px 0;
  border-bottom:var(--hair);align-items:baseline}
.mn{font-weight:600;color:var(--ink)}
.mnote{grid-column:1;font-size:14px;color:var(--ink-3)}
.mp{grid-row:1;grid-column:2;font-family:var(--font-display);font-weight:700;
  font-size:19px;color:var(--gold)}
.cfoot{margin:26px 0 0;font-size:15px;color:var(--ink-3);max-width:62ch}
@media(min-width:900px){
  .crow{grid-template-columns:minmax(210px,1fr) minmax(0,1.9fr) auto auto;
    column-gap:28px;row-gap:0;align-items:baseline;padding:22px 0;
    grid-template-areas:"name blurb tag price"}
  .crow.notag{grid-template-columns:minmax(210px,1fr) minmax(0,1.9fr) auto;
    grid-template-areas:"name blurb price"}
  .cb{margin:0}
  .cn{font-size:23px}
  .cp{font-size:29px}
  .cpblank{padding-top:14px}
}

/* ==========================================================================
   THE ROOM ITSELF — cream, and the most open section on the page. The biggest
   pictures on the site in a two-column CSS COLUMN-FLOW.

   THE ORIENTATION RULE LIVES HERE. There is no aspect-ratio, no object-fit, no
   fixed height and no width/height attribute anywhere near a plate. A plate is
   given a WIDTH and its own proportions decide its height, so a portrait, a
   landscape and a square pack into the same run with nothing cropped and no row to
   break. A column-flow, never a fixed grid with equal rows — a grid with fixed rows
   passes every static check and still breaks the first time a portrait lands in it.
   ========================================================================== */
.run{margin-top:34px;columns:1;column-gap:22px}
.plate{break-inside:avoid;margin:0 0 22px;display:block}
.plate img{display:block;width:100%;height:auto;border-radius:var(--radius);
  filter:var(--grade)}
.plate figcaption{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 12px;
  padding-top:11px;margin-top:11px;position:relative}
/* the caption's gold rule — one element, drawn on reveal. See MOTION. */
/* border-top, never height: `.plate ...` carries a photographic token, and
   orientcheck.py rejects any fixed height on one of those — correctly, because that
   is how a frame stops taking the asset's own shape. A zero-box with a 2px top
   border draws the identical rule and is orientation-rule clean. */
.plate figcaption::before{content:"";position:absolute;top:0;left:0;right:0;
  border-top:2px solid var(--gold);transform-origin:left center}
.plate figcaption b{font-family:var(--font-display);font-size:18px;font-weight:700;
  color:var(--ink)}
.plate figcaption span{font-size:13px;letter-spacing:.03em;color:var(--ink-3)}
.ourplates{column-span:all;margin:0 0 22px;padding:18px 20px;background:var(--surface-2);
  border-radius:var(--radius);font-size:15.5px;line-height:1.6;color:var(--ink-2);
  max-width:66ch}
@media(min-width:700px){.run{columns:2;column-gap:26px}.plate{margin-bottom:26px}}
@media(min-width:1180px){.run{column-gap:32px}.plate{margin-bottom:32px}
  .plate figcaption b{font-size:20px}}

/* ==========================================================================
   WHY THEY COME BACK — cream, wide. Three ruled proofs across the measure, and the
   review under them in a CLARET PLATE: the one saturated object between the hero
   photograph and the footer, and the reason this is not a third cream block.
   ========================================================================== */
.reg{margin-top:30px;display:grid;gap:0}
.regrow{padding:20px 0 22px;border-top:2px solid var(--gold)}
.regrow h3{font-size:20px;color:var(--ink);margin-bottom:8px}
.regrow p{font-size:15.5px;line-height:1.58;color:var(--ink-2);margin:0;max-width:44ch}
.say{margin-top:30px;background:var(--band);color:var(--band-ink);
  border-radius:var(--radius);padding:30px 24px}
.say blockquote{margin:0 0 18px;font-family:var(--font-display);font-style:italic;
  font-weight:600;font-size:20px;line-height:1.42;color:var(--band-ink);max-width:52ch}
.say figcaption{font-size:11.5px;font-weight:700;letter-spacing:.18em;
  text-transform:uppercase;color:var(--accent-on-band)}
@media(min-width:760px){
  .reg{grid-template-columns:repeat(3,1fr);gap:0 32px}
  .say{margin-top:40px;padding:40px 38px}
  .say blockquote{font-size:24px}
}
@media(min-width:1180px){.say{padding:52px 56px}.say blockquote{font-size:27px}}

/* ==========================================================================
   WHAT PEOPLE ASK — cream, narrow. An accordion in one column beside its own
   heading from 900px; four short rows run across a 1240px measure is restraint that
   reads as unfinished, which is a deduction, not minimalism.
   ========================================================================== */
.askgrid{display:grid;gap:26px}
.askcall{margin:16px 0 0;font-size:15px;color:var(--ink-3)}
.askcall a{font-family:var(--font-display);font-size:20px;font-weight:700;
  color:var(--accent);text-decoration:none}
.askcall a:hover{color:var(--accent-2)}
.asklist{border-bottom:var(--hair)}
.ask{border-top:var(--hair)}
.ask summary{display:flex;justify-content:space-between;align-items:center;gap:18px;
  padding:17px 0;cursor:pointer;list-style:none;min-height:44px;
  font-family:var(--font-display);font-size:18px;font-weight:600;color:var(--ink)}
.ask summary::-webkit-details-marker{display:none}
.ask summary i{flex:0 0 auto;width:13px;height:13px;position:relative}
.ask summary i::before,.ask summary i::after{content:"";position:absolute;
  background:var(--accent);left:0;top:6px;width:13px;height:1.5px}
.ask summary i::after{transform:rotate(90deg)}
.ask[open] summary i::after{transform:rotate(0deg)}
.ask[open] summary{color:var(--accent)}
.ask p{margin:0 0 18px;font-size:15.5px;line-height:1.62;color:var(--ink-2);max-width:62ch}
@media(min-width:900px){
  .askgrid{grid-template-columns:minmax(0,.78fr) minmax(0,1.22fr);gap:52px;
    align-items:start}
  .ask summary{font-size:19px}
}

/* ==========================================================================
   THE DOOR — back onto the sand for the close. The week prints on hairlines beside
   the form, because the last question anybody has standing outside a room is
   whether it is open.
   ========================================================================== */
.dgrid{display:grid;gap:30px;margin-top:32px}
.week{border-top:2px solid var(--pine)}
.wrow{display:flex;justify-content:space-between;align-items:baseline;gap:18px;
  padding:13px 0;border-bottom:var(--hair)}
.wd{font-size:14px;font-weight:600;letter-spacing:.03em;color:var(--pine)}
.wt{font-family:var(--font-display);font-size:17px;font-weight:600;color:var(--ink)}
/* THE SIGN IN THE WINDOW — the designed no-hours state. Not seven invented rows,
   not an empty table: the one line they did publish, set like a sign, and the ask. */
.sign{background:var(--surface);border:var(--hair);border-radius:var(--radius);
  padding:22px 22px 20px;border-top:2px solid var(--pine)}
.signlab{font-size:11px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;
  color:var(--pine);margin:0 0 10px}
.signline{font-family:var(--font-display);font-size:22px;font-weight:700;
  color:var(--ink);margin:0 0 12px;padding-bottom:12px;border-bottom:2px solid var(--gold)}
.signask{font-size:14.5px;line-height:1.56;color:var(--ink-2);margin:0}
.drow{margin-top:26px}
.dbig{display:block;font-family:var(--font-display);font-size:22px;font-weight:700;
  color:var(--ink);text-decoration:none;margin-bottom:6px;letter-spacing:-.012em}
.dbig:hover{color:var(--accent)}
.drow address{font-style:normal;font-size:15.5px;color:var(--ink-2);margin-top:8px}
.darea{margin:14px 0 0;font-size:15px;color:var(--ink-2)}
.dlab{display:block;font-size:11px;font-weight:700;letter-spacing:.18em;
  text-transform:uppercase;color:var(--pine);margin-bottom:3px}
.area{margin-top:26px;padding-top:22px;border-top:var(--hair)}
.area h3{font-size:19px;color:var(--ink);margin-bottom:8px}
.area p{font-size:15.5px;line-height:1.58;color:var(--ink-2);margin:0 0 12px}
.towns{display:flex;flex-wrap:wrap;gap:8px}
.towns span{font-size:11.5px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;
  color:var(--pine);border:1px solid var(--pine-ring);border-radius:999px;padding:5px 12px}
.formpanel{background:var(--surface);border:var(--hair);border-radius:var(--radius);
  padding:24px 22px}
.formpanel .field label{color:var(--ink-3);letter-spacing:.11em}
.formpanel .btn{width:100%}
.fineprint{margin:14px 0 0;font-size:13.5px;line-height:1.55;color:var(--ink-3)}
.thanks{border-color:var(--gold-ring);background:var(--surface)}
.thanks h3{font-size:24px;color:var(--ink)}
.thanks p{margin:0;color:var(--ink-2)}
@media(min-width:960px){
  .dgrid{grid-template-columns:minmax(0,.85fr) minmax(0,1.15fr);gap:52px;align-items:start}
  .formpanel{padding:32px 30px}
}

/* ------------------------------------------------------------------- FOOTER --
   The room's colour closes the page. The drop was cream all the way down and this
   is the door shutting behind you — the only claret surface below the review plate. */
footer{background:var(--band);color:var(--band-ink-2);padding:52px 0 32px}
.fgrid{display:grid;gap:30px}
.fbrand .mark.big{width:52px;height:52px;font-size:21px}
.fname{font-family:var(--font-display);font-size:25px;font-weight:700;
  color:var(--band-ink);margin:14px 0 10px;letter-spacing:-.012em}
.fabout{font-size:15px;line-height:1.6;color:var(--band-ink-2);margin:0 0 8px;max-width:44ch}
.ftag{font-size:14px;line-height:1.55;color:var(--band-ink-2);margin:0;max-width:44ch}
.fh{font-family:var(--font-display);font-size:12px;font-weight:700;letter-spacing:.16em;
  text-transform:uppercase;color:var(--accent-on-band);margin:0 0 14px}
.fcol a,.fcol address,.fplain{display:block;color:var(--band-ink);font-style:normal;
  font-size:15.5px;margin-bottom:9px;text-decoration:none}
.fcol a:hover{color:var(--accent-on-band)}
.fplain.lab{font-family:var(--font-display);font-weight:600;font-size:17px}
.fnav{display:flex;flex-wrap:wrap;gap:10px 22px;margin-top:34px;padding-top:22px;
  border-top:1px solid var(--hair-band)}
.fnav a{font-size:12px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;
  color:var(--band-ink-2)}
.fnav a:hover{color:var(--accent-on-band)}
.fbase{display:flex;flex-wrap:wrap;gap:8px 24px;justify-content:space-between;
  margin-top:24px;padding-top:18px;border-top:1px solid var(--hair-band);
  font-size:13px;color:var(--band-ink-2)}
.fbase .credit a{color:var(--band-ink)}
@media(min-width:820px){
  .fgrid{grid-template-columns:1.35fr 1fr 1fr;gap:44px}
  footer{padding:66px 0 34px}
}

/* ==========================================================================
   MOTION — three moments, none of which moves a layout box.
   1. THE HOUSE REVEAL. base.css owns it, gated behind html.js which the inline head
      script sets only when prefers-reduced-motion is unset, so no-JS, blocked-JS and
      reduced-motion all render the page fully visible. THE HERO IS EXEMPT and must
      stay exempt: fading the first screen delays the headline and fights the fold.
   2. THE UNDERLINE DRAWS. hero.h1b's text-decoration-color comes up from
      transparent. It changes no box and withholds no content — the words are painted
      the instant the page arrives and only the stroke arrives late.
   3. THE CAPTION RULE DRAWS. Each plate's gold caption hairline scales from 0 as
      that plate reveals — the label being written under the picture. transform only,
      so it costs nothing in CLS and D2 measures exactly what it measured before.
   ========================================================================== */
html.js .h1b{animation:drawline .6s cubic-bezier(.22,.61,.36,1) .45s 1 both}
@keyframes drawline{from{text-decoration-color:rgba(var(--shade),0)}
  to{text-decoration-color:var(--gold-lit)}}
html.js .plate figcaption::before{transition:transform .6s cubic-bezier(.22,.61,.36,1) .12s}
html.js .reveal:not(.in) .plate figcaption::before{transform:scaleX(0)}
@media(prefers-reduced-motion:reduce){
  html.js .h1b{animation:none;text-decoration-color:var(--gold-lit)}
  html.js .plate figcaption::before{transition:none;transform:scaleX(1)}
}


/* =============================================================== THE BOARD ==
   The shared services section (base.css THE BOARD, partials/board.html) wearing
   the room's skin. Everything structural — the grid, the cell, the edge accent,
   the hook — is inherited; only colour, face, weight and the round radii are set
   here. Jon, 2026-08-14: one services section for every skeleton.

   Three columns only from 1900px. This room has the widest measure in the set, but
   at 1560 the third column squeezed every description to four lines on an ordinary
   laptop — measured on the render, not assumed. */
.card .board{--bd-gap:12px;--bd-cell-pad:18px;--bd-price:23px;--bd-radius:14px;
  --bd-edge:linear-gradient(90deg,var(--gold-rule,#C9A24A) 0%,var(--pine,#245542) 70%,transparent 100%)}
@media(min-width:900px){.card .board{--bd-icon:54px;--bd-glyph:27px;--bd-price:26px}}
@media(min-width:1900px){.card .board{--bd-cols:3}}
.card .bicon{border-radius:999px;color:var(--gold-tx,#7C570C)}
.card .bname{font-family:var(--font-display);font-weight:600;font-size:20px;
  letter-spacing:-.01em;color:var(--ink)}
.card .bprice{font-family:var(--font-display);font-weight:600;
  color:var(--gold-tx,#7C570C)}
.card .btag{color:var(--pine,#245542);border-color:var(--gold-rule,#C9A24A)}
.card .bdesc{color:var(--ink-2)}
.card .bgh{font-family:var(--font-display);letter-spacing:.1em;
  color:var(--gold-tx,#7C570C)}
.card .bhook{border-color:var(--gold-rule,#C9A24A)}
.card .bhook-l{color:var(--gold-tx,#7C570C)}


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