/* ============================================================================
   Fortune Design & Permits — design system
   Minimal, modelled on buildoly.com. Photography leads; one accent; no ornament.
   Spec: docs/design-system.md   Identity: the restored Fortune house mark
   Restyles the class contract emitted by build.py so every page builder — the 72
   city pages, blog, legal and category pages — inherits it without markup changes.
   NOTE: the first pass claimed the contract was fully preserved; it was not.
   .bph/.bb, .rows/.r and a `figure` margin reset were missing and are restored
   below. If you add a class in build.py, add it here in the same commit.
   ========================================================================== */

:root{
  --bg:#F6F4F1; --cream:#F7F5F1; --card:#FFFFFF; --hover:#F5F1E8;
  --ink:#1A1A1A; --black:#0A0A0A; --mut:#6B6B6B; --line:#E6E3DA;
  /* Brand colour, 2026-08-05. The electric blue #2B2FE8 was invented during the
     rebuild; the real Fortune brand is the orange in the archived logo. Owner
     picked the "Ink & Ember" colourway, which makes orange the site's single
     accent and retires blue from the interface entirely.

     Ember shade set to the Claude logo orange on owner request (2026-08-05),
     brighter and warmer than the archived logo's #EF562C.

     THREE tokens, because one orange cannot do all three jobs at AA:
       --ember   #D97757  the bright ember. Button FILLS and the logo mark.
                          Only 2.84:1 against cream, so it can never be text —
                          and white on top of it is 3.12:1, which is why the
                          primary button carries an INK label, not a white one.
                          That flip is what buys the brightness; darkening the
                          fill until white worked would have undone the request.
       --accent  #AF4928  the same hue at L=42%. Links, chips, focus rings,
                          small text: 5.03:1 on cream, 5.52:1 under white.
       --accent-h #D66B48 the button hover wipe. Deliberately a SMALL step down
                          in lightness — go darker than this and the ink label
                          drops under 4.5:1 (measured: #D25D37 gives 4.44).
     Buttons are 600/16px, which is NOT WCAG "large text" (that needs 18.66px
     bold), so the 4.5:1 floor applies to the label. Do not raise --ember's
     brightness further without re-checking the ink label against it. */
  --accent:#AF4928; --accent-h:#D66B48; --ember:#D97757;
  --font:'Outfit',ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;
  --r-sm:4px; --r-md:12px; --r-lg:20px;
  --wrap:1280px; --narrow:760px; --gut:24px; --sec:112px; --nav:56px;
  /* The ONE elevation in the system, added 2026-08-07 with the nav dropdowns.
     Everything else on this site is flat by design — the only other box-shadows
     in this file are focus rings and the .pcard flash. The nav panel earns one
     because it is the only surface that ever floats over page content, and on
     the homepage it floats over arbitrary hero PHOTOGRAPHY, where --line is a
     1.17:1 hairline against --bg and cannot separate a cream panel from a bright
     sky on its own. rgba is --black, not a new hue. Do not reuse this on cards. */
  --sh-pop:0 10px 30px -8px rgba(10,10,10,.18), 0 2px 6px -2px rgba(10,10,10,.08);
  --d-fast:.15s; --d-base:.25s; --d-slow:.4s;
  --ease:cubic-bezier(.4,0,.2,1);
  --ease-mag:cubic-bezier(.83,0,.17,1);
  --ease-out:cubic-bezier(.16,1,.3,1);
}
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}
/* overflow-x:CLIP, not hidden. `hidden` computes overflow-y to auto and makes the
   viewport a scroll container, which stays PROGRAMMATICALLY scrollable — so the
   browser's own scroll-focused-element-into-view can shift the page sideways when
   a keyboard user tabs to an off-screen .hs-panel, with no scrollbar to get back.
   `clip` keeps overflow-y:visible and creates no scroll container at all. */
body{margin:0;background:var(--bg);color:var(--ink);
  font:400 17px/1.625 var(--font);letter-spacing:-.01em;overflow-x:clip;
  -webkit-font-smoothing:antialiased}
/* the header is fixed, so anchor targets must reserve its height or they land
   underneath it (e.g. the homepage's own href="#process") */
[id]{scroll-margin-top:calc(var(--nav) + 16px)}
img{max-width:100%;display:block}
a{color:inherit}
.wrap{max-width:var(--wrap);margin-inline:auto;padding-inline:var(--gut)}
.wrap-narrow{max-width:var(--narrow);margin-inline:auto;padding-inline:var(--gut)}
section{padding-block:var(--sec)}
:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:var(--r-sm)}
.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}

/* ---------------------------------------------------------------- type */
h1,h2,h3,h4{margin:0;color:var(--black);font-weight:800;letter-spacing:-.025em}
h1{font-size:clamp(38px,6vw,68px);line-height:1.05;letter-spacing:-.03em}
h2{font-size:clamp(28px,3.6vw,42px);line-height:1.15}
h3{font-size:clamp(19px,1.9vw,23px);line-height:1.25;font-weight:600;letter-spacing:-.02em}
h4{font-size:15px;font-weight:600;letter-spacing:-.01em}
p{margin:0 0 16px}
.lead,.phero-sub{font-size:clamp(17px,1.6vw,19px);line-height:1.625;color:var(--mut);
  letter-spacing:-.01em;max-width:58ch}
.eyebrow,.kicker{display:block;font-size:14px;font-weight:500;color:var(--mut);
  margin:0 0 14px;letter-spacing:-.005em;text-transform:none}
.mono{font-variant-numeric:tabular-nums}
.center{text-align:center}
.sec-head{margin-bottom:56px;max-width:52ch}
.sec-head h2+.lead{margin-top:16px}
.big{font-size:clamp(30px,3.8vw,44px)}

/* ---------------------------------------------------------------- buttons */
.btn{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:8px;
  min-height:48px;padding:0 24px;border-radius:var(--r-md);overflow:hidden;isolation:isolate;
  font:600 16px/1 var(--font);letter-spacing:-.01em;text-decoration:none;cursor:pointer;
  border:1px solid transparent;white-space:nowrap;
  transition:color var(--d-base) var(--ease),border-color var(--d-base) var(--ease)}
.btn::before{content:"";position:absolute;inset:0;z-index:-1;transform:scaleY(0);
  transform-origin:bottom;transition:transform var(--d-base) var(--ease-out)}
.btn:hover::before{transform:scaleY(1)}
/* Ink label, not white. On the bright ember white is 3.12:1 and fails; ink is
   5.57:1. The alternative was dulling the fill until white passed, which is
   exactly the brightness the owner asked to get back. */
/* The border is not decoration — it is the button's BOUNDARY, and .btn sets
   `border:1px solid transparent` so the fill is otherwise the only edge. The
   bright ember is 2.84:1 against cream, under WCAG 1.4.11's 3:1 for UI
   component boundaries (the old ultramarine was 7.19:1, so this regressed when
   the fill got brighter). --accent is 5.03:1 on cream and reads against the
   ember at 1.77:1, which is enough to draw an edge. This keeps the brightness
   the owner asked for AND a boundary that resolves on the cream header, the
   CTA band and the contact form. */
.btn-primary{background:var(--ember);color:var(--ink);border-color:var(--accent)}
.btn-primary::before{background:var(--accent-h)}
/* .btn's base rule transitions `color`, and the hover wipe keeps an ember
   ground underneath, so the label must stay ink through the hover too. */
.btn-primary:hover,.btn-primary:focus-visible{color:var(--ink)}
.btn-dark{background:var(--black);color:#fff}
.btn-dark::before{background:var(--ink)}
.btn-ghost{background:transparent;color:var(--ink);border-color:var(--line)}
.btn-ghost::before{background:var(--hover)}
.btn-ghost:hover{border-color:var(--mut)}
.btn-ghost-d{background:transparent;color:#fff;border-color:rgba(255,255,255,.45)}
.btn-ghost-d::before{background:rgba(255,255,255,.16)}
.hero-cta{display:flex;flex-wrap:wrap;gap:12px;margin-top:32px}

/* arrow link */
.lk,.go{display:inline-flex;align-items:center;gap:7px;font-size:16px;font-weight:600;
  color:var(--accent);text-decoration:none;letter-spacing:-.01em}
.lk svg,.go svg{width:16px;height:16px;stroke-width:1.5;fill:none;stroke:currentColor;
  stroke-linecap:round;stroke-linejoin:round;
  transition:transform var(--d-base) var(--ease-out)}
.lk:hover svg,.go:hover svg,.card:hover .lk svg,.card:hover .go svg{transform:translateX(5px)}

/* ---------------------------------------------------------------- header */
header.site{position:fixed;inset:0 0 auto;z-index:80;border-bottom:1px solid transparent;
  transition:background var(--d-base) var(--ease),border-color var(--d-base) var(--ease)}
header.site.solid{background:rgba(246,244,241,.88);backdrop-filter:blur(12px);
  border-bottom-color:var(--line)}
/* pages without a photo hero start solid — nav text must never sit on bare cream */
body.chrome-solid header.site{background:rgba(246,244,241,.9);backdrop-filter:blur(12px);
  border-bottom-color:var(--line)}
.nav{height:var(--nav);display:flex;align-items:center;gap:30px}
.brand{display:flex;align-items:center;gap:9px;flex:none;text-decoration:none;
  font-size:17px;font-weight:700;letter-spacing:-.03em;color:#fff;
  transition:color var(--d-base) var(--ease)}
header.site.solid .brand,body.chrome-solid .brand{color:var(--black)}
/* Restored Fortune house mark — currentColor so one markup serves both grounds */
/* 30px, not the 22px the old square mark used. Three nested outlines genuinely
   stop resolving below ~26px — rendered at 22px the two ink strokes merge and
   the ember house disappears into them. 30px holds on a 1x display and still
   sits comfortably in the 56px nav. If this ever needs to go smaller than 26px,
   use the inner house on its own (see assets/brand/favicon.svg), not this. */
.mark{width:30px;height:30px;flex:none}
.mark path{fill:none;stroke-width:7;stroke-linecap:round;stroke-linejoin:round}
/* currentColor, so the two ink strokes invert on the dark footer with no
   override rule — which is what the old .setback needed three rules to do. */
.mark .m-out,.mark .m-mid{stroke:currentColor}
/* The ember house keeps its colour on every ground. At 2.84:1 against cream it
   would fail as text, but it is a 7-unit stroke in a 30px mark — artwork, not
   text — and the two ink strokes beside it carry the shape. The 4.5:1 floor
   binds --accent, not this; see the token block for why they differ. */
.mark .m-in{stroke:var(--ember)}

/* ---- logo draw-on (option B, owner-picked 2026-08-05) --------------------
   Three passes back to front: outer sweep, then the ink peak, then the ember
   house. Dash lengths are the MEASURED path lengths (m-out 184.14, m-mid
   205.49, m-in 177.42), rounded up so the stroke is fully hidden at offset.

   READ THIS BEFORE EDITING. A draw-on hides the stroke first, which is the
   exact shape of the bug this site has shipped four times: content only JS can
   reveal. Three things keep it safe, and all three are required:

     1. The hidden state is scoped to `.mkdraw`, set by the inline <head> script
        (build.py MOTION_FLAG) BEFORE first paint. No class, no hidden state —
        so if that script is blocked, the mark is simply drawn and static.
     2. It is ALSO scoped to `.motion`, which main.js strips in its catch and
        the script tag's onerror handler. Either failure path un-hides it.
     3. `animation-fill-mode: forwards` holds the end state, and the animation
        has no `infinite` — so even a mid-animation JS error leaves it complete.

   The mark is in the header, above the fold on every one of 106 pages. It is
   the single worst element on the site to leave invisible. Do not move this
   hidden state out from under both flags. */
@keyframes markDraw{to{stroke-dashoffset:0}}
:root.motion.mkdraw .mark .m-out{stroke-dasharray:185;stroke-dashoffset:185;
  animation:markDraw .45s var(--ease) forwards}
:root.motion.mkdraw .mark .m-mid{stroke-dasharray:206;stroke-dashoffset:206;
  animation:markDraw .50s var(--ease) .25s forwards}
:root.motion.mkdraw .mark .m-in{stroke-dasharray:178;stroke-dashoffset:178;
  animation:markDraw .48s var(--ease) .50s forwards}
/* The footer carries a second .mark. Both would draw in step, but the footer's
   is far below the fold, so by the time it is seen the animation is long over —
   it just renders complete. No extra rule needed; noted so it isn't "fixed". */
.brand span{font-weight:400;opacity:.65}
/* The flex lives on .nav-list (the <ul>), NOT on .navlinks, since 2026-08-07 —
   .navlinks is now the mobile panel SURFACE (background, fixed position, border)
   and the list inside it is what lays the items out. The ≤960 rules that used to
   set flex-direction/gap on .navlinks moved to .nav-list with it. */
/* ⚠️ THE FLEX CHAIN HAS TO BE UNBROKEN, ALL THREE LEVELS. .navlinks must itself
   be a flex container: it is a <nav>, so with display:block its <ul> child would
   be auto-height (~32px of line box) sitting at the TOP of the stretched 56px
   box. Two things break at once when that happens — the labels sit ~12px above
   the brand and the CTA, which .nav{align-items:center} centres; and .dd's
   top:100% then resolves ~24px ABOVE the header's bottom border, so the panel
   opens inside the bar and clips the parent's underline. align-self alone on
   .navlinks is not enough; the <ul> has to stretch too. */
/* FOUR levels have to be stretched, not three: .nav > .navlinks > .nav-list >
   .nav-item. `align-self:stretch` is the top attach and is the easiest one to
   drop by accident — .nav sets align-items:center, so without it .navlinks
   resolves to its content height (15px x 1.625 + 8 padding = 32.375px), centred,
   and .dd{top:100%} opens 11.8px INSIDE the bar: it covers the header hairline,
   shows two stray vertical borders mid-bar, and hides the underline of the very
   item you just opened. `flex:1` is main-axis only and does not help here. */
.navlinks{display:flex;align-items:stretch;align-self:stretch;flex:1;min-width:0}
.nav-list{display:flex;align-items:stretch;flex:1;min-width:0;
  gap:24px;list-style:none;margin:0;padding:0}
/* full nav height, so the panel at top:100% is flush with the header's own
   bottom border — no dead gap for the pointer to fall through mid-hover */
.nav-item{position:relative;display:flex;align-items:center}
.navlinks a{position:relative;font-size:15px;font-weight:500;text-decoration:none;
  color:rgba(255,255,255,.92);letter-spacing:-.01em;white-space:nowrap;padding-block:4px;
  transition:color var(--d-base) var(--ease)}
/* ⚠️ `.nav-list>li>a`, NOT `.navlinks a` — TOP-LEVEL anchors only. This is the
   transparent-over-photo → solid-cream chrome flip, and it must not reach into
   the dropdown panel, whose rows are coloured by `.navlinks .dd a` further down.
   Left broad, `header.site.solid .navlinks a` is (0,3,2) and beats the panel's
   own `.navlinks .dd a:hover` at (0,3,1) — and main.js toggles `.solid` past
   40px of scroll on EVERY page, not just the photo-hero homepage, so the panel's
   hover and current-page colours would work only in the top 40px of each page.
   Scoping here rather than out-specifying there is what stops the arms race. */
header.site.solid .nav-list>li>a,body.chrome-solid .nav-list>li>a{color:var(--ink)}
.navlinks a::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;
  background:currentColor;transform:scaleX(0);transform-origin:right;
  transition:transform var(--d-base) var(--ease-out)}
/* .is-section is the underline for "a CHILD of this item is the current page".
   It is a CLASS and never aria-current — see header()'s docstring: asserting
   aria-current="page" on /adu from /adu/detached is a false statement to a
   screen reader, and that is exactly what this replaced. */
/* Scoped to `.nav-list>li>a` — TOP-LEVEL anchors only, not `.navlinks a`.
   Panel rows are styled with a background and colour instead, and they must not
   grow the sweeping underline. Scoping here rather than overriding there is what
   keeps the two out of a specificity fight: `.navlinks a[aria-current="page"]::after`
   is (0,2,2) and so is `.navlinks .dd a::after`, so an override would TIE and be
   decided by source order — the collision class this file documents around the
   .hs-panel/.card rules. Scoping means the panel never matches at all. */
.nav-list>li>a:hover::after,.nav-list>li>a[aria-current="page"]::after,
.nav-list>li>a.is-section::after{transform:scaleX(1);transform-origin:left}
/* belt-and-braces: the base .navlinks a::after above still CREATES the
   pseudo-element on panel rows (at scaleX(0), so invisible). Remove it. */
.navlinks .dd a::after{display:none}

/* ---- nav dropdowns (2026-08-07) -------------------------------------------
   OPENED BY CSS :hover / :focus-within, NOT by JavaScript, and the closed state
   is deliberately NOT gated on html.motion. That looks inconsistent with every
   other hidden state in this file, so read this before "fixing" it:

   .motion exists because .navlinks.open is only ever added by a JS click
   handler — no JS, no class, content unreachable. A :hover/:focus-within panel
   has no such dependency: the rules below are parsed before first paint and
   need nothing to run. Gating them on .motion would make the menus VANISH for
   a no-JS visitor rather than degrade, i.e. it would CREATE the bug the flag
   exists to prevent. The mobile accordion in the ≤960 block is a different
   story — that one IS JS-driven and IS gated.

   The parent stays a real <a href> to its hub. :focus-within is on the <li>
   and the trigger is a SIBLING of the panel, not a descendant: a
   visibility:hidden descendant cannot take focus, so a trigger inside the
   panel could never open it with the keyboard. Tab order is parent → its
   children → next parent. */
/* `not all and (max-width:960px)` — the EXACT complement of the mobile block,
   not `min-width:961px` and not `min-width:960.02px`. This is the file's only
   complementary pair, and any numeric gap is a real hole: layout viewports are
   fractional (1/64px in Blink, 1/60px in Gecko, plus browser zoom), so at e.g.
   960.015625 NEITHER block would match. There is no base .dd rule, so in that
   band the panels fall back to UA <ul> defaults — four bulleted lists inline
   inside the 56px bar, no hamburger (its display:flex lives in the ≤960 block),
   and the blow-out swallowed silently by body{overflow-x:clip}. A gapless
   construction costs nothing, so use one. */
@media not all and (max-width:960px){
  .dd{position:absolute;top:100%;left:-20px;z-index:1;
    min-width:252px;max-width:320px;margin:0;padding:8px;list-style:none;
    /* OPAQUE, and --card (white) rather than --bg: it must not inherit the
       header's translucency — on the homepage this panel hangs over
       photography — and white also separates it from the cream page behind. */
    background:var(--card);border:1px solid var(--line);border-top:0;
    border-radius:0 0 var(--r-md) var(--r-md);box-shadow:var(--sh-pop);
    /* The panel hangs inside a position:FIXED header, so anything past the
       viewport bottom has nothing to scroll it and is simply cut off with no
       scrollbar. Regulations is ~385px tall, fine at 720p — but a short window
       (a netbook, or a browser with several toolbars) would silently lose its
       last rows. Cap and scroll rather than clip. */
    max-height:calc(100vh - var(--nav) - 16px);
    max-height:calc(100dvh - var(--nav) - 16px);
    overflow-y:auto;overscroll-behavior:contain;
    opacity:0;visibility:hidden;transform:translateY(-4px);
    /* 120ms close delay forgives a diagonal mouse trip to the first row;
       opening has none, so the menu still feels instant. transition-delay,
       not a JS timer. */
    transition:opacity var(--d-fast) var(--ease) .12s,
               transform var(--d-fast) var(--ease-out) .12s,
               visibility 0s linear calc(.12s + var(--d-fast))}
  .nav-item:hover>.dd,.nav-item:focus-within>.dd{
    opacity:1;visibility:visible;transform:none;
    transition:opacity var(--d-fast) var(--ease),
               transform var(--d-fast) var(--ease-out),visibility 0s}
  /* Escape has to beat :hover, and the pointer is still over the item when it
     fires. (0,3,0) outranks the (0,2,0) open rules above. main.js clears it on
     the next pointerleave. */
  .nav-item.dd-mute:hover>.dd,.nav-item.dd-mute:focus-within>.dd{
    opacity:0;visibility:hidden;transform:translateY(-4px)}
  .nav-item:hover>a .dd-c,.nav-item:focus-within>a .dd-c{transform:rotate(180deg)}
  /* No parent's panel comes near the right edge today (the widest case, About Us
     at 961px, clears it by 123px). Here so it exists before someone adds an
     eighth item — body{overflow-x:clip} would swallow the overhang silently. */
  .dd.dd-right{left:auto;right:-20px}
  /* ---- the wide (multi-column) panel: Regulations, 20 rows -----------------
     20 rows in one column would be ~880px tall — taller than a laptop viewport,
     under a FIXED header, with nothing to scroll it. Three columns bring it to
     ~350px. 414px wide is measured, not guessed: the widest label is "Imperial
     Beach" at 98.2px, + 24px row padding + slack = a 130px column.
     Right edge lands at 934px inside a 1232px box at 1280, and 778px inside 913
     at 961 — clears by 298px and 135px, so it needs no .dd-right. */
  .dd-wide{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
    min-width:414px;max-width:none}
  .dd-wide>li{min-width:0}
  .dd-wide>li.dd-span{grid-column:1/-1}
}
/* ⚠️ EVERY COLOUR RULE IN THIS PANEL IS PREFIXED `.navlinks`, AND IT IS LOAD-BEARING.
   `body.chrome-solid .navlinks a{color:var(--ink)}` above is (0,2,2) and applies
   to panel rows too. A bare `.dd a:hover{color:var(--accent)}` is (0,2,1) and
   LOSES to it — on 106 of 107 pages (every page but the unscrolled homepage).
   That left `background:var(--hover)` #F5F1E8 on `--bg` #F6F4F1 as the only
   hover feedback: 1.02:1, i.e. no perceptible change at all, and the current
   row distinguishable only by font-weight. `.navlinks .dd a:hover` is (0,3,1)
   and wins. Do not shorten these selectors. */
.dd a{display:block;padding:12px;border-radius:var(--r-sm);
  font-size:15px;font-weight:500;line-height:1.35;
  text-decoration:none;letter-spacing:-.01em;
  transition:background var(--d-fast) var(--ease),color var(--d-fast) var(--ease)}
.navlinks .dd a{color:var(--ink)}
.navlinks .dd a:hover{background:var(--hover);color:var(--accent)}
.navlinks .dd a[aria-current="page"]{background:var(--hover);color:var(--accent);font-weight:600}
.dd a:focus-visible{outline-offset:-2px}   /* ring draws inside the 8px panel padding */
.navlinks .dd .dd-hub{font-weight:600;color:var(--black)}
.dd .dd-hub{border-bottom:1px solid var(--line);
  border-radius:var(--r-sm) var(--r-sm) 0 0;margin-bottom:4px;padding-bottom:14px}
/* the closing row of a wide panel — mirrors the hub rule at the other end, so
   the 18 city rows read as a block bracketed by the two navigational rows */
.dd .dd-all{border-top:1px solid var(--line);border-radius:0 0 var(--r-sm) var(--r-sm);
  margin-top:4px;padding-top:14px;font-weight:500;color:var(--accent)}
/* --accent #AF4928 measures 4.95:1 on --hover and 5.52:1 on --card, both over
   the 4.5:1 floor. The colour change is the real affordance here — the two
   cream tokens are 1.02:1 apart and can only ever be a supporting hint. */
/* Rows are 12 + 12 + ~20 line-box = 44px, meeting the 44x44 target on the
   touch-capable widths that still get the desktop panel (tablets at 1024). */
.nav-item.has-dd>a{display:inline-flex;align-items:center;gap:3px}
.dd-c{width:9px;height:9px;flex:none;opacity:.62;fill:none;stroke:currentColor;
  stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;
  transition:transform var(--d-fast) var(--ease),opacity var(--d-fast) var(--ease)}
.nav-item.has-dd>a:hover .dd-c{opacity:1}
/* the mobile disclosure — display:none above 960, which is WHY its aria-expanded
   can never contradict a panel that CSS :hover opened */
.dd-tog{display:none;width:48px;height:48px;margin-right:-10px;padding:0;
  background:none;border:0;cursor:pointer;color:var(--ink);
  align-items:center;justify-content:center}
.dd-tog .dd-c{width:14px;height:14px;opacity:.8}
.dd-tog[aria-expanded="true"] .dd-c{transform:rotate(180deg)}

/* Skip link. The nav went from 7 tab stops to 25 on 2026-08-07, and focusing a
   parent opens its panel, so a keyboard user tabs through every child before
   reaching content on all 107 pages. Visible only when focused; page() and both
   blog builders wrap their content in <main id="main">. */
/* position:FIXED, not absolute — header.site is fixed, and an absolutely
   positioned skip link is pinned to the top of the DOCUMENT, so after any
   scroll it stays focusable while sitting off-screen. */
.skip{position:fixed;left:var(--gut);top:-100px;z-index:90;
  padding:10px 16px;border-radius:var(--r-sm);background:var(--black);color:#fff;
  font-size:15px;font-weight:600;text-decoration:none;transition:top var(--d-fast) var(--ease)}
/* :focus, not :focus-visible — the link is off-screen until focused, so it can
   only ever be reached by keyboard or by a screen reader's own focus move, and
   the latter does not always satisfy :focus-visible. */
.skip:focus{top:8px}
/* <main> carries tabindex="-1" so the skip link actually lands somewhere in
   Safari. That makes it match :focus-visible after a keyboard activation, and
   the global ring would then outline the whole page — with outline-offset:3px
   the vertical edges fall outside the viewport and get clipped, leaving two
   stray accent lines across the page. The scroll jump is the affordance here;
   a non-interactive landing target does not need a ring. */
#main:focus,#main:focus-visible{outline:none}
/* The phone needs the SAME dark-ground flip as .brand and .navlinks a. It was the
   one header element left on `color:inherit`, which resolves to body's #1A1A1A —
   near-black over the homepage hero scrim (62% black), so the primary contact
   fact was invisible above the fold on the only page with a photo hero. */
.navcta{display:flex;align-items:center;gap:14px;flex:none;color:#fff;
  transition:color var(--d-base) var(--ease)}
/* The nav carries seven links from 2026-08-05 ("About Us" + "Contact" added by
   the owner), and the desktop row does not fit them at every width between the
   768px hamburger breakpoint and full desktop. Measured at 1024px: content box
   976px vs brand ~239 + 30 + links ~512 + 6x24 gaps + 30 + phone ~120 + 14 +
   button ~137 = well over. body{overflow-x:clip} hides the overflow rather than
   showing a scrollbar, so the symptom is the CTA silently sliding off the right
   edge — the same failure mode as the 360px header, at a different width.
   Two steps, cheapest first: drop the phone (it is in the footer, on /contact,
   and one tap away in the CTA), then tighten the gaps. */
/* ⚠️ RE-MEASURED 2026-08-07, when the dropdowns added four chevrons to this row.
   The estimates above are PESSIMISTIC — they were rounded up, and the row has
   far more slack than they imply. Measured properly this time, from the real
   Outfit advance widths (the variable font from google/fonts, instanced per
   weight with fontTools) rather than from an average character width:

     brand, full "Fortune Design & Permits" 17/700/-.03em   225.9px  (est. ~239)
     brand, short "Fortune"                                  97.8px
     the 7 nav labels, 15/500/-.01em, no gaps                427.4px  (est. ~512)
     phone "(619) 836-0025" 15/500                           101.8px  (est. ~120)
     "Free consultation" btn 14/600 + 2x16 padding           142.3px  (est. ~137)
     4 chevrons @ 9px + 3px gap                               48.0px  <- the cost

   Sanity check on the model: the PRE-FIX case this comment block describes
   (full brand + phone + 30/24 gaps at 1024px) comes out 139px over its 976px
   box, which is the "well over" recorded above. So the method agrees with the
   original measurement; it is just less conservative.

   Row width vs .wrap content box, WITH the chevrons and with every gap left
   exactly as it was. Note the box is NOT constant above 1280: the
   @media(min-width:1600px) block further down sets --wrap:1440 and --gut:40,
   so it steps up to 1360 there. (A first draft of this table missed that and
   listed 1232 for 1920 — it errs safe, but "measured" has to mean measured.)

     vw 1920 / 1600      box 1360   used 1163.4   slack +196.6
     vw 1599 / 1440 / 1280  box 1232   used 1163.4   slack  +68.6
     vw 1279             box 1231   used 1047.6   slack +183.4   (phone drops)
     vw 1201             box 1153   used 1047.6   slack +105.4
     vw 1200             box 1152   used  863.4   slack +288.6   (brand + gaps)
     vw 1024             box  976   used  863.4   slack +112.6
     vw  961             box  913   used  863.4   slack  +49.6   <- worst case

   ⚠️ The 1200/1024/961 rows DEPEND on `.nav-list{gap:18px}` in the
   @media(max-width:1200px) block below. When the dropdowns moved the flex
   container from .navlinks to the <ul>, that rule was briefly left on
   .navlinks, where `gap` is inert — which silently restored 24px gaps and cut
   the worst case from +49.6 to +13.6. If you ever see that rule back on
   .navlinks, this table is wrong.

   Panel right edges, widest case (About Us, the 6th item): clears the content
   box by +274px at 1280, +362 at 1200, +123 at 961. Nothing needs .dd-right.

   So the chevrons fit everywhere with room to spare and NOTHING had to be
   given up for them: no tighter gaps, no shortened "About Us", and the phone
   still drops at 1279 and not a pixel earlier. If you add an eighth nav item
   or a fifth dropdown, re-run this before you ship it — body{overflow-x:clip}
   means a failure here is silent, which is the whole reason this block exists. */
/* 1279, not 1200 — staggered on purpose. At exactly 1201 the brand goes back to
   its long form, the gaps widen AND the phone returns, all on the same pixel,
   which put the row ~34px over. Bringing the phone back one breakpoint later
   spreads the three restorations out so no single width is over. */
@media(max-width:1279px){ .navcta .phone{display:none} }
@media(max-width:1200px){
  /* ⚠️ .nav-list, NOT .navlinks. The flex container moved to the <ul> when the
     dropdowns landed; `gap` on .navlinks is inert now that it only ever has one
     flex child, so leaving it here silently reverted the measured 24→18px
     tightening this breakpoint exists for. */
  .nav{gap:20px} .nav-list{gap:18px}
  /* Still ~31px over at 1024 with the gaps alone. Dropping "Design & Permits"
     frees ~138px and takes it to ~869 in a 976 box — comfortable, and the same
     treatment the ≤768px block already applies. The full name stays in the
     footer, every <title>, and the JSON-LD. */
  .brand span{display:none}
}
header.site.solid .navcta,body.chrome-solid .navcta{color:var(--ink)}
.navcta .phone{font-size:15px;font-weight:500;text-decoration:none;color:inherit;
  white-space:nowrap}
.navcta .btn{min-height:38px;padding:0 16px;font-size:14px}
.navtoggle{display:none;background:none;border:0;cursor:pointer;padding:10px;
  flex-direction:column;gap:5px;color:#fff}
header.site.solid .navtoggle,body.chrome-solid .navtoggle{color:var(--ink)}
.navtoggle span{display:block;width:20px;height:1.5px;background:currentColor;
  transition:transform var(--d-base) var(--ease),opacity var(--d-base) var(--ease)}
.navtoggle[aria-expanded="true"] span:nth-child(1){transform:translateY(6.5px) rotate(45deg)}
.navtoggle[aria-expanded="true"] span:nth-child(2){opacity:0}
.navtoggle[aria-expanded="true"] span:nth-child(3){transform:translateY(-6.5px) rotate(-45deg)}

/* ---------------------------------------------------------------- photo frames */
@property --sc{syntax:'<number>';inherits:false;initial-value:1.12}
.ph{position:relative;overflow:hidden;background:var(--line);border-radius:var(--r-lg)}
.ph img{width:100%;height:100%;object-fit:cover;will-change:transform;--sc:1.12;
  transform:translate3d(var(--px,0px),var(--py,0px),0) scale(var(--sc));
  transition:--sc .7s var(--ease-out)}
.card:hover .ph img,.bcard:hover .ph img{--sc:1.19}

/* ---------------------------------------------------------------- home hero */
.hero{position:relative;min-height:88vh;min-height:88svh;display:flex;align-items:flex-end;
  padding-block:120px 72px;overflow:hidden;isolation:isolate}
.hero-media{position:absolute;inset:-60px 0 0;z-index:-2;will-change:transform}
.hero-media img{width:100%;height:calc(100% + 60px);object-fit:cover;
  object-position:center 70%;transform:scale(1.07);animation:heroIn 1.8s var(--ease-out) forwards}
@keyframes heroIn{to{transform:scale(1)}}
.hero::after{content:"";position:absolute;inset:0;z-index:-1;
  background:linear-gradient(180deg,rgba(10,10,10,.62) 0%,rgba(10,10,10,.16) 38%,
             rgba(10,10,10,.50) 68%,rgba(10,10,10,.72) 100%)}
.hero .wrap{width:100%;position:relative}
.hero h1{color:#fff;max-width:16ch}
.hero .lead{color:rgba(255,255,255,.88);max-width:48ch;margin-top:20px}
.hero-foot{display:flex;flex-wrap:wrap;gap:0;margin-top:48px;
  border-top:1px solid rgba(255,255,255,.26)}
.hero-foot div{flex:1;min-width:150px;padding:18px 24px 0 0;font-size:14px;
  color:rgba(255,255,255,.68)}
.hero-foot b{display:block;font-size:21px;font-weight:700;letter-spacing:-.025em;
  color:#fff;margin-top:4px}

/* ---------------------------------------------------------------- inner page hero */
.phero{position:relative;padding-block:calc(var(--nav) + 76px) 56px;background:var(--cream);
  border-bottom:1px solid var(--line)}
.phero .bp-grid{display:none}
.phero h1{max-width:20ch}
.phero-sub{margin-top:16px}
.crumbs{display:flex;flex-wrap:wrap;gap:8px;font-size:14px;color:var(--mut);margin-bottom:20px}
.crumbs a{color:var(--mut);text-decoration:none}
.crumbs a:hover{color:var(--accent)}

/* ---------------------------------------------------------------- horizontal flow */
/* padding-block:0 is load-bearing twice. (1) main.js sets .hs height to
   pinH+travel as a BORDER box, but `section{padding-block:var(--sec)}` still
   applies here — so 2x112px is stolen from the sticky range. The track starts
   translating 112px BEFORE the pin engages and the pin releases 112px before the
   travel ends: ~12% of the horizontal motion happens unpinned, at both ends.
   (2) In .hs-fallback the pin re-applies var(--sec) itself, so leaving it here
   double-pads the mobile / reduced-motion carousel. */
.hs{position:relative;padding-block:0}
/* The pin sits at top:0 UNDER the fixed header, so it must reserve --nav itself
   or `justify-content:center` pushes the heading up behind the nav bar. */
/* GRID, not flex. `flex:1 1 auto` kept resolving its basis to the image's
   intrinsic height, so the track never shrank and the panels overflowed the pin.
   `minmax(0,1fr)` has no auto minimum, so the track gets exactly the space the
   head leaves — never more. Every nested track needs minmax(0,...) too; a bare
   `1fr` is `minmax(auto,1fr)` and reintroduces the same floor. */
/* grid-template-COLUMNS is not optional here, for the same reason as the rows.
   Without it the single implicit column is `auto`, and .hs-track is a flex row of
   non-shrinkable panels whose min-content width is ~3700px — which sizes the
   column to ~3700px inside a ~1900px pin. overflow:clip hides that it happened.
   The damage lands on .hs-head, the OTHER item in that column: it carries .wrap
   (max-width:1280px; margin-inline:auto) so it centred itself inside 3700px
   instead of the viewport, putting the heading ~900px off to the right, clipped.
   minmax(0,1fr) has no auto minimum, so the column is exactly the pin's width,
   the head centres correctly, and only the track overflows — as intended. */
/* ⚠️ THE PIN IS THE ENHANCEMENT, NOT THE DEFAULT — gated on .motion, and NOT on
   .hs-fallback, so it applies only when JS actually ran.

   It used to be unconditional. That was a JS-off trap of the same family as the
   reveal-class regressions, just wearing a container instead of a class: with
   scripting disabled the inline <head> script never runs, so neither `motion`
   nor `hs-fallback` is ever set — and the base rule pinned the section to
   100svh with `overflow:clip`, which is not a scroll container, while
   `body{overflow-x:clip}` blocked viewport scroll too. The .hs-track flex row is
   ~3224px at 1440px viewport, so Residential was ~30% visible and Commercial
   entirely off-screen, with no way to reach either. The panels are <a>s, so a
   keyboard user could still tab into two links the browser could not scroll to.

   Inverted: the plain-flow scroll-snap carousel in the .hs-fallback block below
   is now also what JS-off gets, because neither class is present and none of the
   pin geometry applies. Do not remove the .motion scope to "simplify" this. */
:root.motion .hs-pin{position:sticky;top:0;height:100vh;height:100svh;overflow:clip;
  display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(0,1fr);
  gap:16px;padding-block:calc(var(--nav) + 10px) 24px}
.hs-head{display:flex;justify-content:space-between;align-items:flex-end;gap:32px;
  flex-wrap:wrap}
/* 15ch wrapped this 43-character heading onto three lines, and three lines of
   section heading is the "blank space at the top" — it pushes the photos down
   without adding information. Two lines at most. */
.hs-head h2{max-width:26ch;font-size:clamp(24px,2.5vw,33px)}
.hs-head .eyebrow{margin-bottom:6px}
.hs-count{display:flex;align-items:center;gap:14px;font-size:14px;color:var(--mut);
  font-variant-numeric:tabular-nums;flex:none}
.hs-bar{width:132px;height:2px;background:var(--line);position:relative;overflow:hidden}
.hs-bar::after{content:"";position:absolute;inset:0;background:var(--accent);
  transform:scaleX(var(--p,0));transform-origin:left}
/* The track is a grid item sized by minmax(0,1fr) above — it gets exactly the
   height the head leaves. It is still flex internally for the horizontal row. */
.hs-track{display:flex;gap:32px;will-change:transform;min-height:0;min-width:0;
  align-items:stretch}
/* --a is rewritten every frame — NO css transition here; the JS lerp is the smoothing */
/* Rows: caption sized to its content, photo takes ALL the rest. The reverse
   (photo 1fr, caption auto) is what buried the caption — the photo won the
   remaining space first and pushed the caption past the bottom of the pin. */
/* `.hs-panel.card`, not `.hs-panel` — the panel is <a class="hs-panel card wipe">
   and `.card{display:block}` further down matches at the SAME specificity, so
   source order gave it the win and this rule's `display:grid` never applied.
   With `display:block` the rows and the 16px gap are inert, and
   `.hs-panel.card .ph{height:100%}` resolves against the full stretched panel
   height — so caption + full-height photo exceeded the panel and the bottom of
   every photo was clipped by `.hs-pin{overflow:clip}`. The descendant rules were
   already fixed for this trap; the panel's own display was missed. */
/* transform-origin is a fixed PIXEL offset, not a percentage. Caption + gap is a
   constant ~157px regardless of viewport, so the photo's optical centre always
   sits ~78px below the panel's centre. The old `58%` was only correct at a
   ~1200px-tall viewport and drifted to 66% at 700px, pivoting the scale well
   above the photo. */
.hs-panel.card{flex:0 0 min(68vw,860px);text-decoration:none;color:inherit;
  display:grid;grid-template-rows:auto minmax(0,1fr);gap:16px;min-height:0;
  transform:scale(calc(.955 + .045*var(--a,1)));
  transform-origin:center calc(50% + 78px)}
/* The de-emphasis ramp lives on the PHOTO, never the panel. --mut is only 4.85:1
   on --bg at full strength, so any panel-level alpha drags the caption under AA:
   solving 107a + 246(1-a) <= 112 gives a >= 0.963, i.e. --mut tolerates ~3.7%
   transparency and no more. The old panel-level `.42` floor measured 1.76:1 for
   .desc/.tag/.num, 2.84:1 for the h3 and 2.84:1 for the focus ring — and an
   off-centre panel is a live, focusable link, so neither the "decorative" nor the
   "inactive component" exemption in SC 1.4.3 applies. Scale plus off-centre
   position carry the de-emphasis instead. */
.hs-panel.card .ph{opacity:calc(.55 + .45*var(--a,1))}
/* WRITTEN AS `.hs-panel.card` ON PURPOSE. The panel carries both classes, and the
   generic `.card .ph` / `.card h3` / `.card .tag` / `.card .desc` rules further
   down the file match at the SAME specificity (0,2,0) — so source order handed
   them the win and this block's margins were being silently discarded, adding
   ~44px of stray margin inside a pin with no spare height. Two classes = 0,3,0,
   which wins regardless of where the generic rules sit. */
.hs-panel.card .ph{min-height:0;height:100%;margin:0;aspect-ratio:auto}
.hs-panel .num{display:block;font-size:13px;color:var(--mut);
  font-variant-numeric:tabular-nums;margin-bottom:4px}
.hs-panel.card h3{display:inline-block;font-size:clamp(20px,2.2vw,27px);margin-bottom:0}
.hs-panel.card .tag{display:block;font-size:14px;color:var(--mut);margin:8px 0 6px}
/* Clamped to two lines so the body height is predictable — an unbounded
   paragraph is what pushes the panel past the pin and clips the number. */
.hs-panel.card .desc{color:var(--mut);font-size:16px;line-height:1.5;margin:0;max-width:46ch;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
/* No counter-translate. The parent's scale about `center calc(50% + 78px)`
   already moves the caption ~18px DOWN and closes it toward the photo; a -10px
   lift on top of that nets to ~+8px down anyway while widening the caption/photo
   gap on inactive panels — two gestures fighting, which reads as a wobble rather
   than a settle. (The removed comment also had the sign backwards: positive
   translateY moves DOWN, away from the section heading, not toward it.) */
.hs-body{display:flex;justify-content:space-between;align-items:flex-end;gap:28px}
.hs-body>div{min-width:0}
/* The END spacer is TWO GAPS narrower than the start. travel = trackWidth - vw,
   which already contains all four 32px inter-item gaps, so an equal-width end
   spacer leaves panel 03 sitting 2x32px left of centre when p reaches 1 — its
   --a peaks at ~0.978 and it never fully resolves, while panel 01 is dead centre
   at p=0. Solve S_end = vw/2 - W/2 - 2G. */
.hs-start{flex:0 0 max(var(--gut),calc(50vw - min(34vw,430px)));pointer-events:none}
.hs-end{flex:0 0 max(var(--gut),calc(50vw - min(34vw,430px) - 64px));pointer-events:none}
/* mobile / reduced-motion / NO-JS: native snap carousel, no pin.
   Every selector here is doubled with :root:not(.motion). `.hs-fallback` is
   added by the same inline <head> script as `.motion` and removed by main.js
   only once the pin is actually wired — so with scripting off NEITHER class
   exists, and keying the carousel on `.hs-fallback` alone left the JS-off
   visitor with base `.hs-track{display:flex}` and 860px panels, overflowing
   into body{overflow-x:clip} with no scroll container: Residential partly
   visible, Commercial unreachable. Gating the PIN on .motion (above) was only
   half the fix; the fallback has to answer for the same case. */
.hs-fallback .hs,:root:not(.motion) .hs{height:auto!important}
.hs-fallback .hs-pin,:root:not(.motion) .hs-pin{position:static;height:auto;overflow:visible;display:block;
  padding-block:var(--sec)}
.hs-fallback .hs-head,:root:not(.motion) .hs-head{margin-bottom:40px}
/* scroll-padding pairs with scroll-snap-align:start below. With `center` snapping
   plus a leading spacer, panel 01's snap point sat at a negative scrollLeft and
   was therefore unreachable — it always rested ~18px left of centre while 02 and
   03 centred properly. Left-aligned snapping is the conventional mobile pattern
   and every panel is reachable. */
.hs-fallback .hs-track,:root:not(.motion) .hs-track{transform:none!important;overflow-x:auto;overflow-y:hidden;
  scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;padding-bottom:8px;
  scroll-padding-inline:var(--gut)}
/* no pin, so the panel goes back to normal flow height — drop the grid rows.
   `.hs-panel.card` in these two, not `.hs-panel` — otherwise they merely TIE with
   the pinned rules and win on source order alone, so any future reshuffle of this
   file silently breaks the mobile carousel. The height tie is the dangerous one:
   if the pinned rule won, `height:100%` against an auto-height block parent
   resolves to auto and the img renders at its full 1400x933 intrinsic ratio. */
.hs-fallback .hs-panel.card,:root:not(.motion) .hs-panel.card{flex:0 0 min(82vw,520px);scroll-snap-align:start;
  display:block;transform:none!important;opacity:1!important}
/* margin-TOP now — the photo follows the caption in the DOM. opacity:1 is needed
   here because the de-emphasis ramp moved onto .ph, so the panel-level
   `opacity:1!important` above no longer reaches it. */
.hs-fallback .hs-panel.card .ph,:root:not(.motion) .hs-panel.card .ph{height:clamp(220px,42vh,380px);margin-top:16px;opacity:1}
.hs-fallback .hs-start,.hs-fallback .hs-end,:root:not(.motion) .hs-start,:root:not(.motion) .hs-end{flex:0 0 var(--gut)}
.hs-fallback .hs-count{display:none}

/* ---------------------------------------------------------------- city marquee */
.strip{border-block:1px solid var(--line);padding-block:26px;overflow:hidden;
  background:var(--cream)}
.strip .flow{display:flex;width:max-content;animation:marq 52s linear infinite}
.strip:hover .flow,.strip:focus-within .flow{animation-play-state:paused}
@keyframes marq{to{transform:translateX(-50%)}}
.strip .g{display:flex;align-items:center;flex:none}
.strip a{display:flex;align-items:center;gap:26px;padding-inline:13px;text-decoration:none;
  font-size:19px;font-weight:500;letter-spacing:-.02em;color:var(--ink);
  transition:color var(--d-fast) var(--ease)}
.strip a:hover{color:var(--accent)}
.strip a::after{content:"";width:4px;height:4px;border-radius:50%;background:var(--line);flex:none}

/* ---------------------------------------------------------------- cards / grids */
.cards,.citygrid,.areagrid,.bloggrid,.gal,.two,.pgal,.quotes{display:grid;gap:32px}
.cards,.bloggrid,.gal,.quotes{grid-template-columns:repeat(3,1fr)}
.pgal{grid-template-columns:repeat(3,1fr);gap:20px}
.areagrid{grid-template-columns:repeat(4,1fr);gap:0 24px}
.two{grid-template-columns:1fr 1fr}
.card,.bcard,.areacard,.svc{display:block;text-decoration:none;color:inherit}
.card .ph,.bcard .ph,.svc .ph{aspect-ratio:4/3;margin-bottom:20px}
.card h3,.bcard h3,.svc h3{margin-bottom:8px;display:inline-block}
.card .tag,.svc .tag{display:block;font-size:14px;color:var(--mut);margin:10px 0}
.card .desc,.bcard p,.svc .desc{color:var(--mut);font-size:16px;margin-bottom:16px}
.bcard .post-date{font-size:14px;color:var(--mut);margin:10px 0 6px}
.areacard{padding:18px 0;border-top:1px solid var(--line);font-weight:500;
  transition:color var(--d-fast) var(--ease),padding-left var(--d-base) var(--ease-out)}
.areacard:hover{color:var(--accent);padding-left:6px}
.areacard .no{font-size:13px;color:var(--mut);display:block;margin-bottom:3px;
  font-variant-numeric:tabular-nums}
.cbox{background:var(--cream);border:1px solid var(--line);border-radius:var(--r-lg);
  padding:32px}
.corner{display:none}
/* heading underline on card hover */
.draw{position:relative}
.draw::after{content:"";position:absolute;left:0;right:0;bottom:-8px;height:1px;
  background:var(--ink);transform:scaleX(0);transform-origin:left;
  transition:transform .55s var(--ease-out)}
.card:hover .draw::after,.bcard:hover .draw::after,.svc:hover .draw::after{transform:scaleX(1)}

/* ---------------------------------------------------------------- split */
.split{display:grid;grid-template-columns:1fr 1fr;gap:80px;align-items:center}
.split .img{position:relative;border-radius:var(--r-lg);aspect-ratio:3/4}
.split .img>.ph{height:100%;border-radius:var(--r-lg)}
.split .img>img{width:100%;height:100%;object-fit:cover;border-radius:var(--r-lg)}
.split .img img{width:100%;height:100%;object-fit:cover}
.split .ovl{display:none}
.split .txt .lead{margin-top:16px}
.ptlist{margin-top:36px}
.pt{padding:20px 0;border-top:1px solid var(--line);display:flex;gap:18px;font-size:16px;
  align-items:flex-start;transition:padding-left var(--d-base) var(--ease-out)}
.pt:hover{padding-left:10px}
.pt b{font-weight:600;color:var(--black)}
.pt span{color:var(--mut)}
.ck{flex:none;width:20px;height:20px;margin-top:2px}
.ck svg{width:20px;height:20px;fill:none;stroke:var(--accent);stroke-width:1.5;
  stroke-linecap:round;stroke-linejoin:round}

/* ---------------------------------------------------------------- dark band / steps */
.band{background:var(--black);color:#fff;overflow:hidden}
.band h2,.band h3{color:#fff}
.band .lead,.band .eyebrow{color:rgba(255,255,255,.66)}
.steps{display:grid;grid-template-columns:repeat(4,1fr);gap:32px}
.step{position:relative;padding-top:22px}
.step::before{content:"";position:absolute;top:0;left:0;right:0;height:1px;background:var(--line)}
.step::after{content:"";position:absolute;top:0;left:0;right:0;height:1px;background:var(--ink);
  transform:scaleX(0);transform-origin:left;
  transition:transform .9s var(--ease-out);transition-delay:calc(var(--i,0)*130ms)}
.step.in::after{transform:scaleX(1)}
.step .num{font-size:14px;color:var(--mut);font-variant-numeric:tabular-nums}
.step h3{margin:10px 0 8px}
.step p{font-size:15px;color:var(--mut);line-height:1.6;margin:0}
.band .step::before{background:rgba(255,255,255,.2)}
.band .step::after{background:#fff}
.band .step .num{color:rgba(255,255,255,.5)}
.band .step p{color:rgba(255,255,255,.68)}

/* ---------------------------------------------------------------- spec rows */
.specrow,.ticks{list-style:none;margin:0;padding:0}
.specrow li,.ticks li{display:flex;justify-content:space-between;gap:16px;padding:11px 0;
  border-top:1px solid var(--line);font-size:15px}
.specrow .s,.ticks .s{color:var(--mut)}
.specrow .b,.ticks .b{font-weight:500;text-align:right}
.dim{display:block;font-size:12px;color:var(--mut);font-weight:400;margin-top:2px}

/* ---------------------------------------------------------------- CTA band */
.cta-band{background:var(--cream);border-top:1px solid var(--line);text-align:center;
  padding-block:96px}
.cta-band .wrap{max-width:720px}
.cta-band h2{margin-bottom:16px}
.cta-band p{color:var(--mut);font-size:clamp(17px,1.6vw,19px);margin:0 auto 32px;max-width:52ch}
.cta-band .wrap>div{display:flex;gap:12px;flex-wrap:wrap;justify-content:center}

/* ---------------------------------------------------------------- city pages */
.citygrid{grid-template-columns:minmax(0,2fr) minmax(0,1fr);gap:64px;align-items:start}
.citymain h2{margin-top:48px;margin-bottom:16px}
.citymain>h2:first-child{margin-top:0}
.citymain p{color:var(--mut)}
.citybody{max-width:72ch}
.cityside{position:sticky;top:calc(var(--nav) + 32px);background:var(--cream);
  border:1px solid var(--line);border-radius:var(--r-lg);padding:28px}
.cityside h4{margin-bottom:16px}
.sidelinks{list-style:none;margin:0 0 20px;padding:0}
.sidelinks li{border-top:1px solid var(--line)}
.sidelinks a{display:block;padding:11px 0;text-decoration:none;font-size:15px;color:var(--ink);
  transition:color var(--d-fast) var(--ease),transform var(--d-fast) var(--ease-out)}
.sidelinks a:hover{color:var(--accent);transform:translateX(3px)}

/* ---------------------------------------------------------------- blog / prose */
.post{padding-block:calc(var(--nav) + 56px) 20px}
/* .post-hero is an <img> in the generator, not a section — style it as one */
.post-hero{width:100%;aspect-ratio:16/8;object-fit:cover;border-radius:var(--r-lg);
  margin:0 0 28px}
.post-title{max-width:22ch}
.post-date{font-size:14px;color:var(--mut);margin-bottom:14px}
.post-body,.prose{max-width:72ch}
.prose h2{font-size:clamp(24px,2.6vw,32px);margin:48px 0 16px}
.prose h3{margin:36px 0 12px}
.prose p,.prose li{color:var(--mut);font-size:17px;line-height:1.7}
.prose ul,.prose ol{padding-left:22px;margin:0 0 20px}
.prose li{margin-bottom:8px}
.prose a{color:var(--accent);text-underline-offset:3px}
.prose img{border-radius:var(--r-lg);margin:32px 0}
.prose blockquote{margin:32px 0;padding-left:24px;border-left:2px solid var(--accent);
  color:var(--ink);font-size:19px}
/* The blog's imported WordPress cost tables had NO styling anywhere in the design
   system — they rendered as raw UA tables (no borders, no padding) in the middle
   of the most-read block of the cost article. Wrapper scroll so a wide table can
   never force the page to scroll sideways. */
.prose table{width:100%;border-collapse:collapse;margin:0 0 24px;font-size:16px;
  display:block;overflow-x:auto}
.prose th,.prose td{text-align:left;padding:12px 14px;border-bottom:1px solid var(--line);
  vertical-align:top}
.prose th{font-weight:600;color:var(--black);white-space:nowrap}
.prose td{color:var(--mut)}
.prose tr:last-child td{border-bottom:0}
.post-cta{margin-top:56px;padding-top:32px;border-top:1px solid var(--line)}

/* ---------------------------------------------------------------- contact / forms */
.contact-grid{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,1fr);gap:64px;
  align-items:start}
.contact-info .ci-row{display:flex;gap:14px;padding:18px 0;border-top:1px solid var(--line);
  align-items:flex-start}
/* The label and the value are two inline elements inside one <div>, so without
   this they rendered as "Phone(619) 836-0025" / "Service AreaSan Diego County".
   Pre-existing — it affected every row, and only became obvious once the Email
   row made it three. Column stack, small gap, label sized down as a label. */
.contact-info .ci-row>div{display:flex;flex-direction:column;gap:3px;min-width:0}
.contact-info .ci-row .mono{font-size:12px;letter-spacing:.06em;text-transform:uppercase;
  color:var(--mut)}
.contact-info .ci-row a{color:inherit;text-decoration:none;word-break:break-word}
.contact-info .ci-row a:hover{color:var(--accent)}
.ci-ic{flex:none;width:20px;height:20px;color:var(--accent)}
.ci-ic svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.5;
  stroke-linecap:round;stroke-linejoin:round}
.contact-form{background:var(--cream);border:1px solid var(--line);border-radius:var(--r-lg);
  padding:32px}
.field{margin-bottom:20px}
.field label{display:block;font-size:14px;font-weight:500;margin-bottom:7px;color:var(--ink)}
.field input,.field select,.field textarea{width:100%;min-height:48px;padding:12px 14px;
  border:1px solid var(--line);border-radius:var(--r-md);background:#fff;color:var(--ink);
  font:400 16px/1.4 var(--font);letter-spacing:-.01em;
  transition:border-color var(--d-fast) var(--ease),box-shadow var(--d-fast) var(--ease)}
.field textarea{min-height:120px;resize:vertical}
.field input:focus,.field select:focus,.field textarea:focus{outline:none;
  /* Derived from the token, not hand-typed. This carried rgba(186,67,34) for one
     build — #BA4322, an intermediate accent that never became a token and so
     would have survived the next palette change untouched. If color-mix is
     unsupported the declaration is simply dropped and border-color still marks
     focus, so the fallback is a missing glow, not a missing focus state. */
  border-color:var(--accent);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent)}
.formnote{font-size:13px;color:var(--mut);margin-top:12px}

/* ---- classes the generator emits that were missed in the first pass ---- */
figure{margin:0}                       /* UA default is margin:1em 40px */
.bph{display:block;aspect-ratio:16/10;overflow:hidden;border-radius:var(--r-lg);
  background:var(--line);margin-bottom:18px}
.bph img{width:100%;height:100%;object-fit:cover;
  transition:transform .7s var(--ease-out)}
.bcard:hover .bph img{transform:scale(1.04)}
.bb{padding:0}
.rows{display:grid;gap:0}
.r{display:grid;grid-template-columns:64px 1fr auto;gap:20px;align-items:baseline;
  padding:26px 0;border-top:1px solid var(--line);text-decoration:none;color:inherit;
  transition:padding-left var(--d-base) var(--ease-out)}
.r:hover{padding-left:8px}
.r .no{font-size:13px;color:var(--mut);font-variant-numeric:tabular-nums}
.r p{color:var(--mut);font-size:16px;margin:6px 0 0}
.r .go{color:var(--accent)}
.pgal figure .ph{aspect-ratio:4/3}

/* ---------------------------------------------------------------- footer */
footer.site{background:var(--black);color:rgba(255,255,255,.6);padding-block:72px 32px}
footer.site .cols{display:grid;grid-template-columns:1.8fr 1fr 1fr 1fr 1fr;gap:36px}
footer.site h4{color:#fff;margin:0 0 16px}
footer.site ul{list-style:none;margin:0;padding:0}
footer.site a{color:rgba(255,255,255,.6);text-decoration:none;display:block;padding:6px 0;
  font-size:15px;width:fit-content;
  transition:color var(--d-fast) var(--ease),transform var(--d-fast) var(--ease-out)}
footer.site a:hover{color:#fff;transform:translateX(3px)}
footer.site .brand{color:#fff;margin-bottom:14px}
footer.site .desc{font-size:15px;max-width:34ch;color:rgba(255,255,255,.6)}
footer.site .contact div{padding:5px 0;font-size:15px}
/* `color:#fff` deleted 2026-08-05. footer() emits no .btn inside .contact, so
   the rule matched nothing — but at (0,3,1) it outranked both .btn-primary
   (0,1,0) and its :hover/:focus-visible guard (0,2,0). Restore a primary button
   to the footer contact block and it would have silently taken a white label on
   the ember fill: 3.12:1, the exact failure the .btn-primary comment prevents,
   and unoverridable from that block. Spacing kept; the colour was the trap. */
footer.site .contact .btn{margin-top:12px}
footer.site .bottom{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap;
  border-top:1px solid rgba(255,255,255,.14);padding-top:22px;margin-top:32px;
  font-size:13px;color:rgba(255,255,255,.55)}
footer.site .bottom a{display:inline;padding:0}

/* ---------------------------------------------------------------- motion primitives */
/* THREE STATES BELOW HIDE CONTENT UNTIL JS ADDS .in. Every one is scoped to
   html.motion, set by an inline <head> script and removed if main.js 404s or
   throws (see build.py MOTION_FLAG). Without that gate a single JS failure
   renders the whole page blank — which is exactly what happened. Any new
   hidden-until-revealed state MUST be scoped the same way. */
.mask{display:inline-block;overflow:hidden;vertical-align:bottom;
  padding-bottom:.09em;margin-bottom:-.09em}
.motion .mask>i{display:inline-block;font-style:inherit;transform:translateY(110%);
  transition:transform .85s var(--ease-out);transition-delay:calc(var(--i,0)*55ms)}
.motion .in>.mask>i{transform:none}
.motion .rv{opacity:0;transform:translateY(20px);
  transition:opacity .7s var(--ease-out),transform .7s var(--ease-out);
  transition-delay:calc(var(--i,0)*80ms)}
.motion .rv.in{opacity:1;transform:none}
.motion .wipe .ph{clip-path:inset(0 0 100% 0 round var(--r-lg));
  transition:clip-path 1.05s var(--ease-out);transition-delay:calc(var(--i,0)*90ms)}
.motion .wipe.in .ph{clip-path:inset(0 0 0 0 round var(--r-lg))}

/* ---------------------------------------------------------------- responsive */
/* Large screens. The container is 1280px by default, which keeps line length
   readable — full-bleed media (hero, the pinned strip, the dark bands) already
   widens to the viewport, so a 1920 screen is not "broken", it is a photo-width
   page with a measured text column.
   At 1600+ it opens to 1440, which is the "wide" value docs/design-system.md
   specified from the start and that nothing ever implemented (a review caught
   that no 1440 token existed). --gut widens with it so the extra width becomes
   air, not longer lines. Above ~1440 of content the eye starts losing the start
   of the next line, which is why this stops at one step and does not track the
   viewport. */
@media(min-width:1600px){
  :root{--wrap:1440px;--gut:40px}
}
@media(max-width:1024px){
  :root{--sec:88px}
  .cards,.bloggrid,.gal,.quotes,.pgal{grid-template-columns:1fr 1fr}
  .areagrid{grid-template-columns:repeat(3,1fr)}
  .steps{grid-template-columns:1fr 1fr}
  .split,.citygrid,.contact-grid{gap:48px}
  footer.site .cols{grid-template-columns:1fr 1fr}
}
@media(max-width:860px){
  .citygrid,.contact-grid{grid-template-columns:1fr}
  .cityside{position:static}
}
/* ---- nav collapses to the hamburger HERE, not at 768 -------------------
   The desktop row carries seven links from 2026-08-05 plus the CTA, and stops
   fitting at roughly 960px — measured, not guessed: at 800px the content box is
   752px and the row needs ~857px even with the brand shortened. It used to
   collapse at 768 together with the page's grid breakpoints, which left a
   769-960px band where the links overflowed into body{overflow-x:clip} and the
   CTA slid off-screen with no scrollbar to reveal it.
   These rules were lifted verbatim out of the 768px block; the LAYOUT rules
   (grid columns, --sec, --gut, --nav) deliberately stay at 768 because they are
   about page width, not about this row. */
@media(max-width:960px){
  /* The CLOSED state is gated on html.motion for the same reason the reveal
     states are: `.open` is only ever added by the JS click handler, so if main.js
     404s or throws, an ungated `visibility:hidden` would leave the hamburger inert
     and ALL SIX primary nav links unreachable on every page — and the phone link
     is display:none at this width, so a phone visitor would be left with the logo
     and one button. Without .motion the links simply render stacked and open. */
  .motion .navlinks{position:fixed;inset:var(--nav) 0 auto;background:var(--bg);
    padding:8px var(--gut) 24px;
    border-bottom:1px solid var(--line);
    /* ⚠️ ADDED 2026-08-07 WITH THE DROPDOWNS, and it is not cosmetic. This panel
       held 7 rows (~350px) and never needed to scroll. With the submenus it can
       reach ~1,000px inside a position:fixed box — the bottom of the menu would
       sit below the viewport with NO WAY TO SCROLL TO IT. Same class of defect
       as the 360px hamburger push-off documented below, and it does not
       reproduce by resizing a desktop window.
       Scoped to .motion on purpose: the :root:not(.motion) fallback is in
       normal flow and the PAGE scrolls it, so capping it there would build an
       inner scroller a no-JS visitor may never find. */
    max-height:calc(100vh - var(--nav));
    max-height:calc(100dvh - var(--nav));
    overflow-y:auto;overscroll-behavior:contain;
    transform:translateY(-8px);opacity:0;pointer-events:none;visibility:hidden;
    transition:opacity var(--d-base) var(--ease),transform var(--d-base) var(--ease-out)}
  .motion .navlinks.open{opacity:1;transform:none;pointer-events:auto;visibility:visible}
  /* flex-direction/gap moved off .navlinks onto the <ul> — see the .nav-list
     note in the header block. .navlinks is the panel; .nav-list is the layout. */
  .nav-list{flex-direction:column;align-items:stretch;gap:0}
  /* ⚠️ NO-JS MOBILE: the header un-fixes itself. header.site is position:fixed at
     56px, and without .motion the whole menu renders stacked INSIDE it — so it
     has always overflowed its own header and laid over the top of the page.
     That was ~350px of overlap with 7 links; the dropdowns take it to ~600px,
     so this stops being a blemish and starts eating the hero. Static header +
     auto height makes the menu PUSH the page down instead of covering it, which
     is the correct no-JS layout anyway.
     Scoped to :root:not(.motion) and to ≤960 only: with JS the panel is fixed
     and overlaid on purpose, and that path is untouched. The cost is that
     [id]{scroll-margin-top} and .phero's top padding now over-reserve by 56px
     on this one fallback path — cosmetic slack, not a broken layout. */
  :root:not(.motion) header.site{position:static}
  :root:not(.motion) .nav{height:auto;flex-wrap:wrap;padding-block:8px}
  /* ⚠️ `flex:1 0 100%`, and the basis is the whole point. .navlinks is `flex:1`
     (= 1 1 0%) with min-width:0, so its hypothetical main size is ZERO — it can
     never overflow a flex line and `flex-wrap:wrap` on .nav is inert for it.
     At 375px the line is brand ~98 + CTA ~129 + toggle 40 + gaps = ~303 inside a
     343px box, so nothing wraps and .navlinks is handed the ~40px left over —
     25 white-space:nowrap links in a 40px column, spilling over the CTA and
     clipped silently by body{overflow-x:clip}. A 100% basis forces its own line;
     order:3 puts it under the brand row. This is the no-JS path, so it is
     reached by all three failure modes at once (JS off / 404 / throw). */
  :root:not(.motion) .navlinks{flex:1 0 100%;order:3;padding-bottom:12px}
  /* No JS means the menu is already expanded, so the hamburger would be a
     control that does nothing sitting above an open list. Gate it like every
     other JS-dependent affordance. */
  :root:not(.motion) .navtoggle{display:none}
  /* ⚠️ RE-SCOPED FROM `.navlinks a` TO `.nav-list>li>a` ON 2026-08-07 — the
     highest-risk line in the dropdown change. The !important beats any
     specificity, so left broad it painted every CHILD link 17px with its own
     bottom border, which is neither the type scale nor the grouping the panel
     wants. The !important itself stays: it has to beat
     `header.site.solid .navlinks a` at (0,3,2). Colour is set separately
     below, because children need the ink flip too. */
  /* scoped to top-level too, for the same reason as the chrome flip above: left
     as `.navlinks a`, this !important discarded the panel's --accent hover and
     current-page colour, leaving the current child row at --ink on --hover
     (1.02:1 against the panel) — distinguishable only by font-weight. */
  .nav-list>li>a{color:var(--ink)!important;flex:1;padding:14px 0;font-size:17px}
  .navlinks a::after{display:none}
  /* ...and because that ::after kill removes the ONLY current-page cue the
     top-level rows have, they need a replacement here. Without it /regulations,
     /projects and /contact — the three destinations with no dropdown — showed
     nothing at all in the mobile menu. !important and a higher-specificity
     selector, so it beats the colour rule directly above. */
  .nav-list>li>a[aria-current="page"],.nav-list>li>a.is-section{
    color:var(--accent)!important;font-weight:600}
  .navtoggle{display:flex}
  /* the row rule moves from the anchor to the <li> so the hairline runs the
     full width, including under the 48px toggle button */
  .nav-item{flex-wrap:wrap;justify-content:space-between;position:static;
    border-bottom:1px solid var(--line)}
  .nav-list>li>a .dd-c{display:none}      /* the button carries the chevron here */
  .motion .dd-tog{display:flex}
  /* Children are indented under their parent behind a single hairline rule —
     the site's existing vocabulary, no new surface and no new colour. They
     carry no bottom borders of their own, so the group does not read as 25
     equal peers. */
  .dd{flex-basis:100%;order:3;position:static;margin:0 0 8px;padding:0 0 0 16px;
    list-style:none;background:none;border:0;border-left:1px solid var(--line);
    border-radius:0;box-shadow:none;opacity:1;visibility:visible;transform:none}
  /* THIS one IS .motion-gated, unlike the desktop panel: it is opened by a JS
     click handler, so without JS it must render open. No height animation —
     an animated max-height over a variable-length list is a JS/perf trap, and
     the panel's own open transition already carries the motion. */
  .motion .dd{display:none}
  .motion .nav-item.is-open>.dd{display:block}
  .dd a{padding:12px 0;min-height:48px;font-size:16px;display:flex;
    align-items:center;border-radius:0}
  /* `.navlinks .dd`, matching the base rule's (0,3,1) — as a bare `.dd a:hover`
     at (0,2,1) this lost to the base panel rule and never applied. There is no
     floating panel here to tint, so the row keeps the flat list ground and the
     --accent colour alone carries the state. */
  .navlinks .dd a:hover{background:none;color:var(--accent)}
  .dd .dd-hub{border-bottom:0;margin-bottom:0;padding-bottom:12px}
  .dd .dd-all{border-top:0;margin-top:0;padding-top:12px}
  /* Regulations carries 20 rows. Stacked at 48px each that is ~960px of
     accordion inside a panel that is already max-height-capped and scrolling —
     workable but a long thumb drag. Two columns halves it; the hub and the
     closing row still span. City labels are short enough that 2 columns hold
     "Imperial Beach" without wrapping at 360px. */
  /* ⚠️ `display:grid` MUST ride on the .motion .is-open selector, not on
     .dd-wide alone. `.dd-wide` is (0,1,0) and every rule that sets display on
     this element at ≤960 outranks it: `.motion .dd{display:none}` (0,2,0),
     `.motion .nav-item.is-open>.dd{display:block}` (0,4,0) and
     `:root:not(.motion) .dd{display:flex}` (0,3,0). There is no fourth state
     here — the :hover/:focus-within rules are desktop-only — so a bare
     `.dd-wide{display:grid}` can never win in ANY state and is dead code. It
     shipped that way for one build: the 20-row panel rendered as a single
     ~960px column, exactly what the two columns exist to prevent.
     (0,5,0) beats the block rule, and it cannot touch the no-JS path because
     .motion and :root:not(.motion) are mutually exclusive — so the wrapped-chip
     fallback below is untouched. Matching the winner's shape, not !important. */
  .dd-wide{grid-template-columns:1fr 1fr;column-gap:12px}
  .motion .nav-item.is-open>.dd.dd-wide{display:grid}
  .dd-wide>li.dd-span{grid-column:1/-1}
  .dd-wide>li>a{font-size:15px}
  /* No-JS mobile: all 18 children are visible (that is the contract), but as
     full-width 48px rows they would push ~1,150px of navigation above the
     content on every page. Wrapped and compact, the same 18 links cost ~250px.
     Reachability is unchanged; only the density is. */
  :root:not(.motion) .dd{display:flex;flex-wrap:wrap;gap:0 18px;padding:2px 0 10px 14px}
  :root:not(.motion) .dd a{padding:8px 0;min-height:0;font-size:15px}
}
@media(max-width:768px){
  :root{--sec:64px;--gut:16px;--nav:50px}
  .navcta .phone{display:none}
  /* ⚠️ THE NAV OVERFLOWS AT 360px WITHOUT THESE THREE. Measured, not guessed:
     at 360px --gut is 16, so .nav's content box is 328px. Full brand (30px mark
     + 9 gap + "Fortune Design & Permits" at 17px ≈ 230px) + 30 gap + the 14px
     "Free consultation" button ≈ 137px + 30 gap + 40px toggle ≈ 477px. And
     because body{overflow-x:clip} swallows the excess instead of producing a
     scrollbar, the symptom was not a visible overflow — it was the CTA clipped
     and THE HAMBURGER PUSHED ENTIRELY OFF-SCREEN, i.e. no navigation at all on
     a small phone, on all 106 pages. It does not reproduce by resizing a desktop
     window past the breakpoint fast enough to notice.
     Now: 30 + 9 + ~85 ("Fortune") + 12 + ~129 + 12 + 40 = ~317px, inside 328. */
  .brand span{display:none}       /* "Design & Permits" — full name stays in the footer, <title> and JSON-LD */
  .nav{gap:12px}
  .navcta .btn{padding:0 14px}
  .split,.two{grid-template-columns:1fr;gap:40px}
  .split .img{aspect-ratio:4/3}
  .cards,.bloggrid,.gal,.quotes,.pgal{grid-template-columns:1fr}
  .areagrid{grid-template-columns:1fr 1fr}
  .steps{grid-template-columns:1fr}
  .hero{min-height:82svh;padding-block:100px 48px}
  .hero-foot div{min-width:50%;padding-right:12px}
  /* .post, NOT .post-hero — .post-hero is the <img>, and padding on an element
     with aspect-ratio:16/8 under border-box collapsed the blog hero photo to a
     ~33px strip on phones. .post is the section that wants the padding. */
  .phero,.post{padding-block:calc(var(--nav) + 48px) 40px}
  footer.site .cols{grid-template-columns:1fr}
  .cta-band{padding-block:64px}
}

/* ---------------------------------------------------------------- reduced motion */
@media(prefers-reduced-motion:reduce){
  *,*::before,*::after{animation:none!important;transition:none!important;
    scroll-behavior:auto!important}
  /* must carry the same .motion prefix as the rules they undo, or they lose on
     specificity regardless of source order */
  .motion .rv{opacity:1;transform:none}
  .motion .mask>i{transform:none}
  .motion .wipe .ph{clip-path:none}
  .hero-media{transform:none!important}
  .hero-media img{transform:none}
  .ph img{transform:none}
  .step::after{transform:scaleX(1)}
  .strip .flow{animation:none;width:auto;flex-wrap:wrap}
  .strip .g{flex-wrap:wrap}
  .strip .g:nth-child(2){display:none}
}

/* ---------------------------------------------------------------- print */
@media print{
  header.site,footer.site,.cta-band,.strip,.hs{display:none}
  body{background:#fff;color:#000}
}

/* ---------------------------------------------------------------- plan explorer */
/* The catalog's own teal/yellow/red size coding is deliberately NOT reproduced:
   it fights the single-accent palette, and colour alone must never be the only
   carrier of meaning. The band is stated in words and figures instead.
   (Said "clashes with the ultramarine accent" until 2026-08-05; the accent is
   now the ember family, and the reasoning holds regardless of which it is.) */
.bands{display:grid;grid-template-columns:repeat(3,1fr);gap:0;margin-top:8px;
  border-top:1px solid var(--line)}
.bandrow{padding:24px 28px 26px 0;border-bottom:1px solid var(--line)}
.bandrow b{display:block;font-size:19px;font-weight:700;letter-spacing:-.02em;color:var(--black)}
.bandrow .bandsf{display:block;margin:2px 0 10px;font-size:14px;color:var(--accent);
  font-variant-numeric:tabular-nums}
.bandrow p{margin:0;font-size:15px;line-height:1.6;color:var(--mut);max-width:34ch}

.planbar{display:flex;flex-wrap:wrap;gap:12px 28px;align-items:center;
  padding-bottom:22px;border-bottom:1px solid var(--line)}
.chips{display:flex;flex-wrap:wrap;gap:8px}
.chip{font:inherit;font-size:14px;font-weight:500;color:var(--mut);cursor:pointer;
  background:none;border:1px solid var(--line);border-radius:999px;padding:8px 15px;
  min-height:38px;display:inline-flex;align-items:center;gap:7px;
  transition:border-color var(--d-fast) var(--ease),color var(--d-fast) var(--ease),
             background var(--d-fast) var(--ease)}
.chip span{color:var(--mut);opacity:.72;font-variant-numeric:tabular-nums}
.chip:hover{border-color:var(--ink);color:var(--ink)}
/* aria-pressed is the source of truth for screen readers; .is-on only paints it */
.chip.is-on{background:var(--accent);border-color:var(--accent);color:#fff}
.chip.is-on span{color:#fff;opacity:.8}
.plancount{margin:20px 0 0;font-size:14px;color:var(--mut);font-variant-numeric:tabular-nums}
.plancount b{color:var(--ink);font-weight:600}
.planempty{margin:40px 0 0;font-size:16px;color:var(--mut)}
.linkish{font:inherit;font-size:inherit;color:var(--accent);background:none;border:0;
  padding:0;cursor:pointer;text-decoration:underline;text-underline-offset:3px}

.plangrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:28px;margin-top:24px}
.plan{border:1px solid var(--line);border-radius:var(--r-lg);background:var(--card);
  overflow:hidden;display:flex;flex-direction:column;
  transition:border-color var(--d-base) var(--ease),transform var(--d-base) var(--ease)}
.plan:hover{border-color:var(--ink);transform:translateY(-3px)}
/* the drawings are line art on white — contain, never cover, or rooms get cropped */
.plan-img{background:#fff;padding:18px;border-bottom:1px solid var(--line)}
.plan-img img{width:100%;height:210px;object-fit:contain}
.plan-meta{padding:20px 22px 22px}
.plan-sf{font-size:27px;font-weight:700;letter-spacing:-.03em;color:var(--black);
  font-variant-numeric:tabular-nums;line-height:1}
.plan-sf span{font-size:15px;font-weight:500;color:var(--mut);letter-spacing:-.01em}
/* "From $X" — one literal workbook cell per size band (build.py plan_price()).
   Accent because it is the number the visitor came for; AAA on the white card. */
.plan-price{margin-top:10px;font-size:19px;font-weight:700;letter-spacing:-.02em;
  color:var(--accent);font-variant-numeric:tabular-nums;line-height:1.2}
.plan-price span{font-size:13px;font-weight:500;color:var(--mut);letter-spacing:-.01em}
/* the 208 sq ft plan prices below the lowest workbook band: no dollar, no guess */
.plan-price.is-ask{color:var(--ink);font-size:16px}
.plan-note{margin-top:4px;font-size:12.5px;line-height:1.5;color:var(--mut)}
.plan-note a{color:inherit}
/* the plans page opens on the grid itself now (router deleted) — its h1 sits in
   a sec-head, not a photo hero, so it runs a step below the hero h1 scale */
.plans-h1{font-size:clamp(32px,4.4vw,52px)}
.plans-note{max-width:62ch}
.plan-cfg{margin-top:9px;font-size:15px;font-weight:600;color:var(--ink)}
.tag-2s{display:inline-block;margin-left:6px;font-size:12px;font-weight:600;color:var(--accent);
  border:1px solid var(--accent);border-radius:999px;padding:1px 8px;vertical-align:1px}
.plan-dim,.plan-rooms{margin-top:5px;font-size:14px;color:var(--mut);line-height:1.5}
.plan-rooms{margin-top:9px;padding-top:9px;border-top:1px solid var(--line)}
/* (No `.plan[hidden]` rule: the configurator filters with .is-off + aria-hidden and
   never sets the hidden attribute, so such a rule would match nothing.) */

@media(max-width:1024px){
  .bands{grid-template-columns:1fr 1fr}
  .plangrid{grid-template-columns:1fr 1fr;gap:20px}
}
@media(max-width:768px){
  .bands{grid-template-columns:1fr}
  .bandrow{padding-right:0}
  .plangrid{grid-template-columns:1fr}
  .plan-img img{height:240px}
  .planbar{gap:10px 16px}
}

/* ---------------------------------------------------------------- configurator */
.cfg{border:1px solid var(--line);border-radius:var(--r-lg);background:var(--card);
  padding:clamp(20px,2.4vw,28px);margin-bottom:32px}
.cfg-head{display:flex;justify-content:space-between;align-items:flex-start;gap:24px;
  flex-wrap:wrap}
.cfg-head .eyebrow{margin-bottom:6px}
.cfg-count{margin:0;text-align:right;font-size:15px;color:var(--mut)}
.cfg-count b{display:block;font-size:clamp(24px,2.6vw,32px);font-weight:700;color:var(--accent);
  letter-spacing:-.03em;font-variant-numeric:tabular-nums;line-height:1;
  transition:transform .18s var(--ease-out)}
.cfg-count.is-move b{transform:scale(1.12)}

.cfg-chips{margin-top:24px;padding-top:20px;border-top:1px solid var(--line)}
.cfg-chips .eyebrow{margin-bottom:10px}
/* the static readout that replaced the deleted two-thumb slider's live <output> */
.cfg-static{margin:18px 0 0;font-size:14px;color:var(--mut);
  font-variant-numeric:tabular-nums}

/* THE load-bearing rule: filtered-out plans stay ON SCREEN. They grey out where
   they stand so every drag produces continuous, whole-grid change. `display:none`
   removes the evidence that your input did anything, which is exactly what made
   the previous build feel inert. Safe for contrast because these are aria-hidden,
   pointer-events:none and out of the tab order — SC 1.4.3 exempts inactive
   components — and the live count is the accessible carrier of the change. */
.plan{transition:opacity .28s var(--ease),transform .28s var(--ease-out),
      filter .28s var(--ease),border-color var(--d-base) var(--ease)}
.plan.is-off{opacity:.16;transform:scale(.94);filter:grayscale(1);pointer-events:none}

@media(max-width:900px){
  .cfg-head{flex-direction:column;gap:12px}
  .cfg-count{text-align:left}
}
@media(prefers-reduced-motion:reduce){
  .plan,.cfg-count b{transition:none}
  /* Logo draw-on: cancel the animation AND clear the hidden state. Killing the
     animation alone would strand the mark at stroke-dashoffset — an invisible
     logo for exactly the users who asked for less motion. The !important is
     needed because the armed rules above carry the same (0,3,0) specificity
     and win on source order otherwise. */
  :root.motion.mkdraw .mark .m-out,
  :root.motion.mkdraw .mark .m-mid,
  :root.motion.mkdraw .mark .m-in{animation:none!important;stroke-dashoffset:0!important}
}

/* ---------------------------------------------------------------- pricing */
/* Calm by construction, not by hiding things. Three cards, ordered smallest and
   most certain first; the one that is not our money says so in the card itself.
   No division-level breakdown on the page — that is a bill, and nobody has agreed
   to anything yet. */
.pricing{background:var(--cream);border-block:1px solid var(--line)}
.psize{margin-bottom:32px}
.psize .eyebrow{margin-bottom:10px}
.pcards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px}
/* Two cards since the City-fees card was removed (owner, 2026-08-05). Without
   this the pair would sit in a fixed 3-column grid and leave a bare third cell —
   the same empty-cell defect the .sibs grid shipped with two of three subtypes.
   (0,1,0) ties with .pcards and wins on source order, which is why it is here
   and not next to it. */
.pcards-2{grid-template-columns:repeat(2,minmax(0,1fr))}
.pcard{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);
  padding:28px 26px 26px}
/* our fee is the anchor: it is the smallest number, the most certain, and the only
   one Fortune controls — so it gets the accent and the others stay quiet */
.pcard.is-ours{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}
.pk{display:block;font-size:14px;font-weight:500;color:var(--mut);margin-bottom:10px}
.pcard.is-ours .pk{color:var(--accent)}
.pv{margin:0 0 12px;font-size:clamp(27px,3vw,36px);font-weight:700;letter-spacing:-.035em;
  color:var(--black);font-variant-numeric:tabular-nums;line-height:1.05}
.pd{margin:0;font-size:15px;line-height:1.6;color:var(--mut)}
.pd b{color:var(--ink);font-weight:600}
.pnote{margin:28px 0 0;font-size:15px;line-height:1.65;color:var(--mut);max-width:62ch}
@media(max-width:900px){ .pcards{grid-template-columns:1fr} }

/* the configurator announces itself when a subtype page's pre-filtered link
   (/adu/plans?size=…#plans) lands here, so the mid-page jump reads as arrival
   rather than an unexplained scroll — main.js adds .is-arrive on a match */
.plans-sec{scroll-margin-top:calc(var(--nav) + 16px)}
@keyframes pFlash{0%{box-shadow:0 0 0 0 color-mix(in srgb, var(--accent) 42%, transparent)}
                  100%{box-shadow:0 0 0 16px transparent}}
.cfg.is-arrive{animation:pFlash .9s var(--ease-out) 1}
@media(prefers-reduced-motion:reduce){ .cfg.is-arrive{animation:none} }

/* ---------------------------------------------------------------- the choice block */
/* Sits directly under the hero: it is the decision the visitor came to make, so it
   goes above the supporting material rather than after it. Numbers were removed on
   purpose — these are mutually exclusive alternatives and 01/02/03 read as stages
   of a sequence. The photograph does the differentiating faster than the caption. */
.sibs-sec{padding-block:0 var(--sec)}
.sibs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px}
.sib{display:flex;flex-direction:column;text-decoration:none;color:inherit;
  background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);
  overflow:hidden;
  transition:border-color var(--d-base) var(--ease),transform var(--d-base) var(--ease-out)}
.sib:hover{border-color:var(--ink);transform:translateY(-4px)}
.sib .ph{display:block;position:relative;aspect-ratio:4/3;margin:0;overflow:hidden}
.sib .ph img{width:100%;height:100%;object-fit:cover;
  transition:transform .6s var(--ease-out)}
.sib:hover .ph img{transform:scale(1.06)}
.sib-t{display:flex;flex-direction:column;gap:6px;padding:22px 24px 24px}
.sib b{display:inline-block;font-size:19px;font-weight:700;letter-spacing:-.02em;
  color:var(--black);align-self:flex-start}
.sib i{font-style:normal;font-size:15px;line-height:1.55;color:var(--mut)}
.sib .lk{margin-top:10px;font-size:15px}
/* the current subtype renders in the same 3-up grid as a non-link "you're here"
   card, instead of being filtered out and leaving a bare cell — see build.py
   category_page()/_sib_card(). White-on-black is 19.8:1 (design-system.md §2). */
.sib-here{position:absolute;top:12px;left:12px;background:var(--black);color:#fff;
  font-size:12px;font-weight:600;letter-spacing:-.005em;padding:5px 11px;
  border-radius:999px}
.sib.is-current{cursor:default;border-color:var(--ink);background:var(--hover)}
.sib.is-current:hover{transform:none}
/* hub comparison cards (.sib.is-rich): the sib card grown to carry the SAME
   sourced spec rows as the homepage type cards, so the hub never shows less
   than the teaser that sent you there. NO rv/wipe/draw-reveal anywhere in this
   block — it sits directly under the hero, and reveal-gating the choice block
   is the regression that has shipped four times (see build.py category_page). */
.sib.is-rich .sib-t{padding-bottom:12px}
.sib-specs{margin:0 24px;border-top:1px solid var(--line)}
.sib-specs li{font-size:14.5px}
.sib-foot{display:block;padding:14px 24px 22px;margin-top:auto}
.sib-foot .lk{font-size:15px}
/* under the hub grid: the plain arrow-link to the full plan library, and the
   one honest cost sentence (size-driven, not type-driven) */
.hub-lk{margin:36px 0 0;text-align:center}
.hub-note{text-align:center}

/* ---------------------------------------------------------------- disclosure */
/* Collapsed secondary content is a NATIVE <details> — zero JS, keyboard-operable
   out of the box, and with JS off it opens on click like anywhere else. Reveal
   classes are banned inside: a .rv in a closed fold has no box for the observer
   and would open invisible if anything went wrong. */
.fold{margin:0;border-top:1px solid var(--line)}
.fold:last-of-type{border-bottom:1px solid var(--line)}
.fold summary{cursor:pointer;list-style:none;display:flex;justify-content:space-between;
  align-items:center;gap:16px;padding:18px 0;font-size:16px;font-weight:600;
  color:var(--ink);transition:color var(--d-fast) var(--ease)}
.fold summary::-webkit-details-marker{display:none}
.fold summary::after{content:"+";flex:none;width:26px;height:26px;display:inline-flex;
  align-items:center;justify-content:center;border:1px solid var(--line);
  border-radius:999px;font-size:17px;font-weight:400;color:var(--mut);
  transition:transform var(--d-base) var(--ease)}
.fold[open] summary::after{transform:rotate(45deg)}
.fold summary:hover{color:var(--accent)}
.fold summary:hover::after{border-color:var(--accent);color:var(--accent)}
.fold-body{padding:6px 0 26px}
.fold-body .split{margin-top:8px}
/* subtype pages: the plans sample and the folded detail stack directly under the
   you're-here row (which already ends with --sec padding) — no doubled gaps.
   .plansroute is the Branch-B contact block (build.py no_plans_route_block) and
   shares this rule because it occupies the SAME slot: one declaration, so the
   two can never drift into different rhythms on ADU vs Residential/Commercial.
   Collisions checked for `section.plansroute`: only `section{padding-block:var(--sec)}`
   above also matches, and a class beats a type selector, so this wins on
   specificity, not source order — exactly as .plansfit already does.
   The one child rule: .plangrid carries margin-top:24px (above) and .pgal does
   not, so the ADU twin put 80px between lead and grid where the Branch-B twin
   put 56px — the section padding matched while the INTERNAL rhythm drifted,
   which is exactly what the paragraph above claims cannot happen. Scoped to
   .plansroute so /projects' own .pgal is untouched; (0,2,0) beats .pgal's
   (0,1,0) on specificity, so it survives a reorder. */
.plansfit,.plansroute{padding-block:0 var(--sec)}
.plansroute .pgal{margin-top:24px}
/* Two-up for the front/rear ADU pair. .pgal is a 3-column grid, so without this
   the pair would sit in two of three cells and leave one bare — the same empty-
   cell defect .sibs and .pcards both shipped with. (0,1,0) ties with .pgal and
   wins on source order. */
.pgal-2{grid-template-columns:repeat(2,minmax(0,1fr));margin-top:24px}
.detail-sec{padding-top:0}

/* ---------------------------------------------------------------- video block */
/* A NATIVE <video controls poster preload="none">. There is no JS in this
   component and there must never be: click-to-play is a browser behaviour, so
   there is nothing here for a reveal gate to hide and nothing for main.js to
   fail at. The poster IS the no-JS state, and it is also the JS state.
   NEVER add .rv / .wipe / .draw to .vid-sec, .vidfig, .vid or .vid-cap.
   Content only JS can reveal has shipped four times on this site; a media block
   is the worst candidate for it, because hiding a <video> also hides the
   controls that are the only way to start it.
   Nothing here appears in the prefers-reduced-motion block, deliberately:
   nothing animates, autoplays or loops. There is no motion to reduce. */

/* .is-tight is the ADU SUBTYPE rhythm (112px) — those pages run .sibs-sec,
   .plansfit and .detail-sec each dropping one side, so a video at full padding
   would be the only 224px gap on the page. (0,2,0) beats `section` (0,0,1) on
   SPECIFICITY, not source order. */
.vid-sec.is-tight{padding-block:0 var(--sec)}

/* CLS defence, doubled on purpose: aspect-ratio here AND width/height
   attributes on the element. Never set a width on .vid without the ratio.
   --line as the ground matches .ph, so the frame reads as a photo frame for
   the moment before the poster decodes. */
.vid{display:block;width:100%;height:auto;aspect-ratio:16/9;
  background:var(--line);border-radius:var(--r-lg)}

/* The global :focus-visible rule sets `border-radius:var(--r-sm)` as a REAL
   property, so without this a focused video snaps its corners from 20px to 4px
   while the ring is up. The ring stays --accent, never --ember: this is a focus
   indicator, so SC 1.4.11's 3:1 applies and --ember is 2.84:1. outline-offset
   puts the ring OUTSIDE the frame on --bg, where --accent measures 5.03:1, so
   the indicator never has to survive whatever is in the poster art. */
/* border-radius ONLY. The global :focus-visible already sets the outline and
   offset; re-declaring them here byte-identically would mean this element
   silently keeps the OLD ring the day someone changes the global one. */
.vid:focus-visible{border-radius:var(--r-lg)}

/* Runtime + one honest line about what the footage shows. The runtime is not
   decoration: preload="none" means the browser holds no metadata, so the
   native scrubber shows no duration until playback starts. --mut is 4.85:1 on
   --bg; 15px clears the body floor. flex-wrap, not nowrap — the two past
   header-overflow incidents were both fixed-width content in a flex row. */
.vid-cap{display:flex;flex-wrap:wrap;gap:4px 12px;align-items:baseline;
  margin:14px 0 0;font-size:15px;line-height:1.6;color:var(--mut);max-width:72ch}
.vid-cap b{color:var(--ink);font-weight:500;flex:none}

@media(min-width:1600px){
  /* --wrap opens to 1440 here, which would put a 16:9 frame at 765px tall —
     close to hero scale for a block that is not the page's subject. Capped one
     step and left-aligned so the extra width becomes air on the right, exactly
     as --gut does at this tier. */
  .vidfig{max-width:1160px}
}

@media(max-width:900px){
  .sibs{grid-template-columns:1fr}
}

/* ================================================================ pricing pages
   /adu/pricing, /residential/pricing, /commercial/pricing (owner 2026-08-07).
   ONE class set, TWO column counts — ADU has 3 size bands, RSF and Commercial
   have 7 — driven entirely by --pt-n, which build.py writes inline on the
   <table>. No @media per category and no second markup path.

   THERE IS NO JS IN THIS BLOCK AND THERE MUST NEVER BE. Every band renders at
   load. Never put .rv/.wipe/.mask/data-split on .ptab-wrap, .ptab or any row:
   the tables ARE the page, so a reveal class there means a JS failure blanks the
   content — the defect this site has shipped four times. audit_no_reveal() in
   build.py halts the build on it, but the rule belongs here too. */

/* -- the three summary cards. NO reveal classes: this sits under the hero. -- */
.pcards-3{grid-template-columns:repeat(3,minmax(0,1fr))}
.psum{padding-block:0 var(--sec)}
.psum .pcard{display:flex;flex-direction:column;
  transition:border-color var(--d-base) var(--ease),
             transform var(--d-base) var(--ease-out)}
/* The permits card has no figure, and an empty slot in a row of numbers reads as
   redaction. It gets a WORD in the same slot instead. This size sits between
   .pv's 27-36px clamp and a heading, so it is clearly subordinate to a real
   number without looking like something failed to load. */
.pv.is-word{font-size:clamp(19px,2vw,23px);color:var(--ink);letter-spacing:-.02em;
  line-height:1.2;font-weight:600}
.psum .pcard .lk{margin-top:auto;padding-top:18px;font-size:15px}
/* .pcard.is-ours (0,2,0) sets an accent frame; a bare hover rule would strip it.
   Both are declared so neither cancels the other. */
.psum .pcard:hover{transform:translateY(-3px)}
.psum .pcard.is-ours:hover{border-color:var(--accent)}

/* a ruled band, not a tinted one: --cream and --bg are 1.005:1 apart, so a
   background swap is invisible on this palette. The hairline is the site's
   existing device for separating a section. */
section.alt{border-block:1px solid var(--line)}

/* ---------------------------------------------------------------- the table -- */
.ptab-wrap{margin:0}
.ptab-scroll{overflow-x:auto;overscroll-behavior-x:contain;padding-bottom:8px}
/* the global :focus-visible offset is +3px, which on a full-bleed scroller draws
   outside the .wrap gutter. Offset only — re-declaring the colour here would
   silently keep the OLD ring the day the global one changes. */
.ptab-scroll:focus-visible{outline-offset:-2px;border-radius:var(--r-sm)}
.ptab{--pt-lab:210px;--pt-col:104px;--pt-max:190px;--pt-bg:var(--bg);
  width:100%;table-layout:fixed;
  /* SEPARATE, not collapse: in collapse mode borders belong to the table, and a
     position:sticky cell loses them as it scrolls. Every border below is per-cell. */
  border-collapse:separate;border-spacing:0;
  /* min-width is what makes the overflow REAL. Without it table-layout:fixed
     squeezes 7 columns to unreadable and .ptab-scroll never scrolls — and page
     overflow is SILENT here because body{overflow-x:clip}. */
  min-width:calc(var(--pt-lab) + var(--pt-n,3) * var(--pt-col));
  /* and max-width stops the 3-band ADU table sprawling to the full 1,232px with
     300px of dead space between label and figure. Capped and left-aligned — the
     same move .vidfig makes at 1600px: extra width becomes air, not stretch. */
  max-width:calc(var(--pt-lab) + var(--pt-n,3) * var(--pt-max));
  font-variant-numeric:tabular-nums;text-align:left}
.ptab col.ptab-lab{width:var(--pt-lab)}
.ptab thead th{padding:0 16px 14px;vertical-align:bottom;text-align:right;
  font-size:15px;font-weight:600;color:var(--black);letter-spacing:-.01em;
  border-bottom:1px solid var(--ink)}
.ptab thead th:first-child{text-align:left;padding-left:0;font-size:14px;
  font-weight:500;color:var(--mut)}
.ptab thead th:last-child{padding-right:0}
.ptab-bu{display:block;margin-top:2px;font-size:13px;font-weight:400;color:var(--mut);
  letter-spacing:-.005em}
.ptab tbody th,.ptab tbody td{padding:12px 16px;font-size:15px;line-height:1.45;
  vertical-align:top;border-bottom:1px solid var(--line)}
.ptab tbody th{text-align:left;padding-left:0;font-weight:400;color:var(--ink)}
.ptab tbody td{text-align:right;color:var(--ink)}
.ptab tbody td:last-child{padding-right:0}
.dim2{display:block;margin-top:2px;font-size:13px;line-height:1.45;color:var(--mut)}
/* group headers take the .eyebrow voice, so the three blocks read as labels
   rather than as data rows that happen to have no numbers */
.ptab .grp th{padding:26px 0 8px;border-bottom:1px solid var(--line);
  font-size:14px;font-weight:500;color:var(--mut);letter-spacing:-.005em}
.ptab tbody:first-of-type .grp th{padding-top:14px}
/* SUBTOTAL takes no extra rule: the row above already carries a bottom hairline,
   and in accounting the rule sits ABOVE the subtotal — so that hairline already
   IS it. A second border would double the line. */
.ptab .is-sub th,.ptab .is-sub td{padding-top:14px;padding-bottom:22px;
  font-weight:600;color:var(--black);border-bottom:0}
/* TOTAL — the heaviest line on the page. --accent on the figure matches
   .plan-price, the site's existing "this is the number you came for". 5.03:1. */
.ptab tfoot th,.ptab tfoot td{padding:20px 16px 0;border-top:2px solid var(--ink);
  vertical-align:baseline}
.ptab tfoot th{text-align:left;padding-left:0;font-size:17px;font-weight:700;
  color:var(--black);letter-spacing:-.02em}
.ptab tfoot td{text-align:right;font-size:19px;font-weight:700;color:var(--accent);
  letter-spacing:-.02em}
.ptab tfoot td:last-child{padding-right:0}
/* ⚠️ THE NARROW TABLE NEEDS ITS OWN TRACK WIDTH. --pt-col:104px is tuned for a
   SINGLE figure ($12,000); every cell here holds TWO plus a dash, and
   Residential band 6 emits "$1,305,000–$1,935,000". At 900px the column content
   box is 104 − 32 = 72px against ~88px of first-line text, so the figures bleed
   LEFT out of their cell (they are right-aligned) into the neighbouring band's
   number — and table-layout:fixed does not clip, so nothing reveals it. Two body
   rows means it can afford the width. */
.ptab.is-narrow{--pt-col:168px;--pt-max:210px}
.ptab.is-narrow tbody .is-total th,.ptab.is-narrow tbody .is-total td{
  font-weight:700;color:var(--black);border-bottom:0;padding-top:14px}
.ptab.is-narrow tbody .is-total td{color:var(--accent);font-size:17px}
/* the hover accent has to be restated here: .ptab.is-narrow tbody .is-total th is
   (0,3,2) and beats .ptab tbody tr:hover th at (0,2,3) on class count, so the
   total row was the one row that silently lost the hover it was written for. */
.ptab.is-narrow tbody tr:hover .is-total th,
.ptab.is-narrow tbody tr:hover.is-total th{color:var(--accent)}
/* Row hover is a scanning aid across a wide row, not a control. The wash is
   1.02:1 and carries nothing on its own — the LABEL turning --accent is the part
   you actually see. (0,2,3) so it beats the sticky background rule at (0,1,2). */
.ptab tbody tr:hover th,.ptab tbody tr:hover td{background:var(--hover)}
.ptab tbody tr:hover th{color:var(--accent)}
.ptab .grp:hover th{background:none;color:var(--mut)}
/* STICKY ROW LABEL — works because .ptab-scroll is the horizontal scroll
   container, so the line item never scrolls away from its own number.
   --pt-bg is a variable, not a literal: a hardcoded --bg would show a seam if
   this table is ever placed on the cream band. */
.ptab tbody th,.ptab thead th:first-child,.ptab tfoot th{
  position:sticky;left:0;z-index:1;background:var(--pt-bg)}
/* the fade must track the row hover, or it cuts an 18px seam of the wrong colour
   through the wash — the one place the two backgrounds are guaranteed to differ */
.ptab tbody tr:hover th{--pt-bg:var(--hover)}
/* an 18px fade into the scrolling content, so the sticky edge reads as "there is
   more under here". A gradient, and the design system bans gradients as ORNAMENT
   — this is functional, the same exemption the hero scrim takes. It lands almost
   entirely inside the next cell's 16px padding. */
.ptab tbody th::after,.ptab thead th:first-child::after,.ptab tfoot th::after{
  content:"";position:absolute;inset-block:0;right:0;width:18px;pointer-events:none;
  transform:translateX(100%);
  background:linear-gradient(90deg,var(--pt-bg),transparent)}
/* ⚠️ EXCEPT on the group-header row. `.ptab tbody th` also matches
   <th colspan="n+1">, whose right edge IS the table's right edge — so the 18px
   translated box lands beyond it, and a transformed box contributes to scrollable
   overflow. That gave every full-width table an 18px phantom scrollbar under a
   table that fits, and a keyboard user in the role="region" scroller could press
   → and scroll 18px to reveal nothing. */
.ptab .grp th::after{content:none}
.ptab-cap{display:flex;flex-wrap:wrap;gap:4px 12px;margin:16px 0 0;
  font-size:14px;line-height:1.6;color:var(--mut);max-width:72ch}
.ptab-cap b{flex:none;color:var(--ink);font-weight:500}

/* ------------------------------------------------------------ share of cost -- */
/* --w is emitted inline by build.py and sizes every bar STATICALLY. Only the
   reveal is motion-gated. A width encoded in a transform would leave twelve empty
   tracks if main.js 404s — .step::after has exactly that shape and gets away with
   it only because it is decorative. Here the width IS the data. */
.tspine{display:flex;gap:3px;height:14px;margin:0;border-radius:999px;overflow:hidden}
.tspine i{flex:var(--w) 1 0;background:var(--ember)}
.tspine-cap{margin:10px 0 0;font-size:14px;color:var(--mut)}
.tshare{list-style:none;margin:36px 0 0;padding:0}
.tsr{display:grid;grid-template-columns:minmax(0,250px) minmax(0,1fr) 52px;gap:24px;
  align-items:center;padding:13px 0;border-top:1px solid var(--line)}
.tsr:last-child{border-bottom:1px solid var(--line)}
.tsl{font-size:15px;line-height:1.4;color:var(--ink)}
.tsb{height:10px;border-radius:999px;background:var(--line);overflow:hidden}
/* scaled to the LARGEST share, not to 100% — at 100% scale the 5% row is a 35px
   stub. The absolute value is carried as text in .tsv, so the bar only has to
   carry rank. --ember is a fill here, never text: exactly its permitted job. */
.tsb i{display:block;height:100%;border-radius:999px;background:var(--ember);
  width:calc(var(--w) / var(--w-max,15) * 100%)}
.tsv{font-size:15px;font-weight:600;color:var(--black);text-align:right;
  font-variant-numeric:tabular-nums}

/* -------------------------------------------------- permits, and no figures -- */
/* .specrow's own .dim is 12px, under the 13px fine-print floor — not reused here,
   because these rows carry a real sentence rather than a note. */
.pfees{border-block:1px solid var(--line)}
.pfee li{align-items:flex-start;padding:18px 0}
.pfee .s{flex:1 1 auto;min-width:0;color:var(--ink)}
.pfee .s b{display:block;margin-bottom:4px;font-size:16px;font-weight:600;
  color:var(--black);letter-spacing:-.01em}
.pfee .s i{display:block;font-style:normal;font-size:15px;line-height:1.6;
  color:var(--mut);max-width:62ch}
/* THE anti-evasion detail: the value slot is filled with an ANSWER — who levies
   the fee — in the same accent the site uses for real prices. Six different true
   answers, not six identical "varies", which is what evasion looks like. */
.pfee .b{flex:none;font-size:14px;font-weight:600;color:var(--accent);white-space:nowrap}

/* -------------------------------------------------------------- responsive -- */
@media(max-width:1024px){ .ptab{--pt-lab:190px} }
@media(max-width:900px){ .pcards-3{grid-template-columns:1fr} }
@media(max-width:768px){
  .ptab{--pt-lab:150px;--pt-col:100px}
  .ptab thead th{padding:0 12px 12px;font-size:14px}
  .ptab thead th:first-child,.ptab tbody th,.ptab tfoot th{padding-left:0}
  .ptab tbody th,.ptab tbody td{padding:11px 12px}   /* cells stay 15px — type floor */
  .ptab tfoot th,.ptab tfoot td{padding:18px 12px 0}
  .ptab tfoot th{font-size:16px} .ptab tfoot td{font-size:17px}
  /* the bar drops to its own row so the trade name gets the full width */
  .tsr{grid-template-columns:minmax(0,1fr) 48px;gap:8px 16px;padding:14px 0}
  .tsl{grid-column:1;grid-row:1} .tsv{grid-column:2;grid-row:1}
  .tsb{grid-column:1/-1;grid-row:2}
  .pfee li{flex-direction:column;gap:6px}
  .pfee .b{text-align:left}
}

/* The ONLY motion on these pages, and it composes existing primitives: the bar
   fill borrows .rv's observer, the spine borrows .wipe's clip-path language. */
.motion .rv .tsb i{transform:scaleX(0);transform-origin:left;
  transition:transform .8s var(--ease-out);transition-delay:calc(var(--i,0)*45ms)}
.motion .rv.in .tsb i{transform:none}
.motion .rv .tspine{clip-path:inset(0 100% 0 0 round 999px);
  transition:clip-path 1.05s var(--ease-out)}
.motion .rv.in .tspine{clip-path:inset(0 0 0 0 round 999px)}
/* THIS UNDO MUST LIVE HERE, AT THE END OF THE FILE — not in the earlier
   reduced-motion block. It matches the armed rules' specificity, so it wins on
   SOURCE ORDER; placed earlier it would lose, and every bar would be stranded at
   scaleX(0) with the spine fully clipped. That is exactly the .mkdraw
   logo-stranding failure this stylesheet already documents. */
@media(prefers-reduced-motion:reduce){
  .motion .rv .tsb i{transform:none}
  .motion .rv .tspine{clip-path:none}
}
