/* ══════════════════════════════════════════════════════════════════════════════
   PAGE.CSS — the inner pages, the navigation dropdowns, and the accessibility
   primitives that main.css predates.
   ══════════════════════════════════════════════════════════════════════════════
   Loaded by every page including index.html. main.css still owns the chrome, the
   colour tokens and the home page's own sections; this file adds the page shapes
   the multi-page build introduced and the focus/scroll behaviour the conformance
   statement claims. Nothing here overrides main.css by force — where the two meet,
   this file is later in the cascade and that is sufficient.
   ══════════════════════════════════════════════════════════════════════════════ */

/* ── SC 2.4.11 FOCUS NOT OBSCURED, AND THE ANCHOR BUG IT ALSO FIXES ──────────
   The header is position:fixed and 62px tall plus its 12px offset. Without this,
   two things break and only one of them is obvious.

   The obvious one: every in-page anchor lands its target 74px behind the header.
   Measured before this rule — all four anchors on the home page put the heading
   they jumped to underneath the bar.

   The one that matters legally: a keyboard user tabbing to a control near the top
   of the viewport, or following a skip link, can have the FOCUSED element hidden
   behind that same bar. WCAG 2.2 added SC 2.4.11 for exactly this, and it is a
   failure a scanner will not report because the element is focused, visible to the
   DOM, and simply covered.

   scroll-padding-top on the scroll container solves both at once: the browser
   reserves that band when it scrolls anything into view, whether that scroll came
   from an anchor, a skip link, or focus moving. 90px is the bar plus breathing
   room, so the target lands clear rather than flush against it. */
:root { scroll-padding-top: 90px; }

/* ── SC 2.4.7 FOCUS VISIBLE, IN TWO TONES ───────────────────────────────────
   One focus colour cannot serve this site. The chrome is cream and the home page
   hero is a photograph of a bright sky, and a dark ring that reads well on cream
   disappears against pale cloud. So the indicator is a dark ring with a light halo
   outside it: whichever the backdrop, one of the two has contrast against it.

   :focus-visible rather than :focus, so a mouse user clicking a button does not
   get a ring they did not ask for, while every keyboard user does. The :focus
   fallback below it covers engines without :focus-visible, and is removed again
   for those that have it — without that pair, older engines show nothing at all. */
:where(a, button, input, textarea, select, summary, [tabindex]):focus { outline: 3px solid #111; outline-offset: 2px; }
@supports selector(:focus-visible) {
  :where(a, button, input, textarea, select, summary, [tabindex]):focus { outline: none; }
  :where(a, button, input, textarea, select, summary, [tabindex]):focus-visible {
    outline: 3px solid #111;
    outline-offset: 2px;
    box-shadow: 0 0 0 6px rgba(255, 255, 255, .95);
    border-radius: 2px;
  }
}
/* <main> takes focus from the skip link but must not draw a ring around the whole
   page when it does — the ring would be the entire viewport and mean nothing. */
main:focus { outline: none; }
main:focus-visible { outline: none; }

/* ══════════════════════════════════════════════════════════════════════════════
   §THE NAVIGATION DROPDOWNS
   ══════════════════════════════════════════════════════════════════════════════
   CLOSED IS visibility:hidden, NOT opacity:0, AND THAT IS THE WHOLE POINT. An
   opacity-0 panel keeps its links in the tab order, so a keyboard user tabs into a
   menu they cannot see — an invisible-focus defect failing SC 2.4.3 and 2.4.7 at
   once, and one no automated scanner reports because every link is "visible" to
   the DOM. visibility:hidden removes them from the tab order entirely.

   THE TRANSITION IS THE TRICK THAT MAKES THAT ANIMATABLE. visibility does not
   interpolate, so it is given a 0s duration with a DELAY equal to the fade on the
   way out, and no delay on the way in. Opening: visible immediately, then fade in.
   Closing: fade out, then hidden at the end. The panel animates and is never
   focusable while invisible.

   HOVERABLE (1.4.13) IS STRUCTURAL: .subnav is a child of .navitem, so :hover
   still matches while the pointer is inside the panel. Move it out of the item to
   "clean up the markup" and the criterion breaks silently. */
.navitem { position: relative; display: flex; align-items: center; }
.navitem > a { padding: 8px; }

.navtoggle {
  width: 26px; height: 26px;          /* SC 2.5.8 target size: 24px is the floor */
  margin-left: -2px;
  border: 0; padding: 0;
  background: transparent; cursor: pointer;
  display: grid; place-items: center;
  border-radius: 3px;
}
.navtoggle::before {
  content: ""; width: 7px; height: 7px;
  border-right: 1.6px solid currentColor; border-bottom: 1.6px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform .2s ease;
}
.navitem[data-open] .navtoggle::before { transform: translateY(1px) rotate(-135deg); }

.subnav {
  position: absolute; top: calc(100% + 10px); left: -12px;
  min-width: 234px; max-width: 84vw;
  background: var(--cream, #fff7e9);
  border: 1px solid var(--line, rgba(17,17,17,.18));
  border-radius: 5px;
  box-shadow: 0 14px 40px rgba(0, 0, 0, .16);
  padding: 8px;
  display: flex; flex-direction: column;
  z-index: 30;

  visibility: hidden;
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity .18s ease, transform .18s ease, visibility 0s linear .18s;
}
.navitem[data-open] > .subnav {
  visibility: visible;
  opacity: 1;
  transform: translateY(0);
  transition: opacity .18s ease, transform .18s ease, visibility 0s;
}
/* The bridge. Without it the 10px gap between the item and the panel is a dead
   strip that closes the menu as the pointer crosses it — the classic hover-menu
   defect, and a 1.4.13 "hoverable" failure in practice even though the markup is
   nested correctly. */
.navitem[data-open] > .subnav::before {
  content: ""; position: absolute; left: 0; right: 0; top: -12px; height: 12px;
}
/* .header nav a IS TWO CLASSES AND AN ELEMENT (0,2,1); a bare `.subnav a` is one
   class and an element (0,1,1) and LOSES. The panel rendered in uppercase with the
   header's letter-spacing until this selector was made specific enough to win —
   visible in a screenshot, invisible in every automated check. Matching the header's
   own shape rather than reaching for !important. */
.header .subnav a {
  display: block; padding: 9px 12px; border-radius: 4px;
  text-decoration: none; line-height: 1.3;
  font: 500 12.5px/1.3 Aeonik, Arial, sans-serif;
  text-transform: none; letter-spacing: 0;
}
.header .subnav a span {
  display: block; margin-top: 1px;
  font: 400 11px/1.3 Aeonik, Arial, sans-serif;
  text-transform: none; letter-spacing: 0; opacity: .66;
}
.header .subnav a:hover, .header .subnav a:focus-visible { background: rgba(17, 17, 17, .06); }
.header .subnav a[aria-current="page"] { background: rgba(255, 91, 32, .12); font-weight: 500; }
.header nav a[aria-current="page"] { text-decoration: underline; text-underline-offset: 5px; }

/* ── PHONE: the panel becomes an inline accordion ───────────────────────────
   A floating panel inside an already-overlaid menu is two overlays deep and hard
   to dismiss on a touch screen. Below the breakpoint the submenu simply becomes
   part of the list. visibility still does the showing and hiding, so the tab-order
   guarantee above holds at every width. */
@media (max-width: 900px) {
  .navitem { display: grid; grid-template-columns: 1fr auto; width: 100%; }
  .navtoggle { width: 44px; height: 44px; }

  /* WAS display:none -> display:flex, WHICH CANNOT ANIMATE. `display` is not an
     interpolable property, so the phone accordion snapped open while the desktop
     panel — same element, same file — faded and slid. Measured before this change:
     the closed panel computed `display:none` with a transition declared against it
     that could never run.

     max-height IS the animatable stand-in, and its usual defect is the magic
     number: too small clips the panel, too large spends most of the duration
     easing through empty space. Neither happens here because js/site.js measures
     scrollHeight at open time and clears the cap once the transition ends, so the
     panel is never capped while idle and a client who adds a seventh area page
     does not get a clipped menu. See setSubnavHeight() there.

     THE visibility CONTRACT FROM THE DESKTOP RULE IS PRESERVED EXACTLY. A panel
     that is only max-height:0 still has focusable links inside it — the invisible-
     focus defect the block above exists to prevent — so visibility still does the
     showing and hiding, with the same 0s-duration-plus-delay trick. */
  .subnav {
    position: static; grid-column: 1 / -1;
    min-width: 0; max-width: none; box-shadow: none; border: 0;
    background: transparent; padding: 0 0 0 10px;
    display: flex;
    max-height: 0;
    overflow: hidden;
    transform: none;
    transition: max-height .26s ease, opacity .2s ease, padding-bottom .26s ease,
                visibility 0s linear .26s;
  }
  .navitem[data-open] > .subnav {
    visibility: visible; opacity: 1;
    padding-bottom: 6px;
    transform: none;
    transition: max-height .26s ease, opacity .2s ease, padding-bottom .26s ease,
                visibility 0s;
  }

  /* THE PANEL MUST BE ABLE TO SCROLL, AND MEASUREMENT IS WHY. `.header nav` is
     position:fixed with no height bound, so its content simply ran off the bottom
     of the screen. Measured at 390x500 with both submenus open: panel 783.8px
     against a 500px viewport, overflowing by 357.8px, with the last item (Contact)
     BELOW THE FOLD AND UNREACHABLE — nothing scrolled because the panel is fixed
     and the document behind it is a different scroller. At 390x844 it still
     overflowed by 13.8px.

     dvh, not vh: on a phone the URL bar makes vh the LARGER viewport, which is the
     one that overflows. overscroll-behavior:contain keeps a flick at the end of the
     list from scrolling the page behind it. */
  .header nav {
    max-height: calc(100dvh - 86px);
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  /* THE BUTTON SAYS "Menu" AND NOTHING ELSE, SO NOTHING SAID IT OPENED ANYTHING.
     The caret is the same 7px/1.6px chevron the submenu toggles already use, so
     the two disclosures in this header read as one idea rather than two.

     IT IS DRIVEN FROM aria-expanded, WHICH IS THE STATE THE MENU ALREADY KEEPS.
     A second class toggled alongside would be a second source of truth, free to
     drift out of step with the thing it describes; there is nothing to keep in
     sync when the attribute the assistive tech reads is also the attribute the
     arrow is drawn from. */
  .menu { display: inline-flex; align-items: center; gap: 8px; }
  .menu::after {
    content: ""; width: 7px; height: 7px;
    border-right: 1.6px solid currentColor; border-bottom: 1.6px solid currentColor;
    transform: translateY(-2px) rotate(45deg);
    transition: transform .22s ease;
  }
  .menu[aria-expanded="true"]::after { transform: translateY(1px) rotate(-135deg); }

  /* THE PAGE SCROLLS WHILE THE MENU IS OPEN. main.css locks body overflow on
     open, which is right for a full-screen overlay and wrong for this: the panel
     is a dropdown occupying the top of the viewport, not a sheet covering it, and
     the operator asked to keep scrolling with it down. Only the Y axis is
     released — body keeps overflow-x:hidden from main.css, which several full-
     bleed sections rely on. The panel does its own scrolling above, so releasing
     the page does not make the menu itself harder to use. */
  body.menu-open { overflow-y: auto; }
}

/* ══════════════════════════════════════════════════════════════════════════════
   §INNER PAGE LAYOUT
   ══════════════════════════════════════════════════════════════════════════════ */
.crumbs { padding: 96px var(--pad, 32px) 0; }
.crumbs ol { display: flex; flex-wrap: wrap; gap: 7px; list-style: none; margin: 0; padding: 0;
  font: 400 11px/1.4 Aeonik, Arial, sans-serif; text-transform: uppercase; letter-spacing: .06em; }
.crumbs li { opacity: .72; }
.crumbs li[aria-current] { opacity: 1; font-weight: 500; }
.crumbs .sep { opacity: .38; }
.crumbs a { text-decoration: none; }
.crumbs a:hover { text-decoration: underline; }

/* The 22px is the gap BELOW the breadcrumbs, not clearance for the fixed
   header — .crumbs supplies that with its 96px. 404.html has no breadcrumb
   trail, so .pagehead landed first and 22px was the only top offset against
   a header whose bottom edge is at 74px: 52px of the h1 sat underneath it.
   Tying the override to the sibling that justifies it means .section-pad's
   own clamp(110px,10.5vw,152px) applies when there are no crumbs. */
.crumbs + .pagehead { padding-top: 22px; }
.pagehead { max-width: 1100px; }
.pagehead h1 { font-family: FeatureDeck, Georgia, serif; font-weight: 400;
  font-size: clamp(2.6rem, 6vw, 5rem); line-height: 1.02; margin: 0 0 .38em; max-width: 16ch; }
.pagehead .lede { font-size: clamp(1rem, 1.5vw, 1.22rem); line-height: 1.55; max-width: 58ch; margin: 0; }

.prose { max-width: 1100px; }
.prose h2 { font-family: FeatureDeck, Georgia, serif; font-weight: 400;
  font-size: clamp(1.7rem, 3vw, 2.5rem); line-height: 1.1; margin: 0 0 .5em; max-width: 22ch; }
.prose h3 { font-size: 1.06rem; font-weight: 500; margin: 1.7em 0 .5em; }
.prose p { max-width: 68ch; line-height: 1.62; margin: 0 0 1.05em; }
/* Universal phrases (webbuilderchecklist.md section 9) are agency-owned, so they must
   never be client-editable regions — the importer makes every <p> inside a section
   editable, and section 9 forbids a block being both. They are therefore <div>s, and
   this rule is what keeps a <div> reading as body copy. Matches nothing on a page that
   has no universal block, which today is every page but /privacy/. */
.prose [data-universal] { max-width: 68ch; line-height: 1.62; margin: 0 0 1.05em; }
.prose a { text-underline-offset: 3px; }

.linklist { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 9px 12px; }
.linklist li { margin: 0; }
.linklist a { display: inline-block; padding: 8px 13px; border: 1px solid var(--line, rgba(17,17,17,.18));
  border-radius: 999px; text-decoration: none; font-size: .9rem; }
.linklist a:hover { background: rgba(17, 17, 17, .05); }

.cardgrid { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(272px, 1fr)); max-width: 1240px; }
.card { border: 1px solid var(--line, rgba(17,17,17,.18)); border-radius: 6px; padding: 24px 22px 20px; background: rgba(255,255,255,.42); }
.card h2 { font-family: FeatureDeck, Georgia, serif; font-weight: 400; font-size: 1.5rem; line-height: 1.12; margin: 0 0 .5em; }
.card h2 a { text-decoration: none; }
.card h2 a:hover { text-decoration: underline; }
.card p { line-height: 1.55; margin: 0 0 .85em; }
.card-more a { font: 400 11px/1 Aeonik, Arial, sans-serif; text-transform: uppercase; letter-spacing: .07em; }

.faq-block { max-width: 1000px; }
.faq-block h2 { font-family: FeatureDeck, Georgia, serif; font-weight: 400; font-size: clamp(1.7rem, 3vw, 2.5rem); margin: 0 0 .7em; }
.faq details { border-top: 1px solid var(--line, rgba(17,17,17,.18)); }
.faq details:last-child { border-bottom: 1px solid var(--line, rgba(17,17,17,.18)); }
.faq summary { cursor: pointer; padding: 18px 40px 18px 0; position: relative; font-weight: 500; list-style: none; }
.faq summary::-webkit-details-marker { display: none; }
.faq summary span { position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  font-size: 1.3rem; line-height: 1; transition: transform .2s ease; }
.faq details[open] summary span { transform: translateY(-50%) rotate(45deg); }
/* The + returns as the answer collapses, not 260ms later when [open] finally
   drops. Same specificity as the rule above and immediately after it, so order
   decides. It keeps translateY(-50%) because that is this file's centring, not
   decoration — dropping to `transform:none` would un-rotate the glyph and shove
   it half its height down the row at the same time. */
.faq details.is-closing summary span { transform: translateY(-50%); }
.faq details p { margin: 0 0 20px; max-width: 68ch; line-height: 1.6; }

/* ── ANY DISCLOSURE, NOT ONLY THE FAQ BLOCK ──────────────────────────────────
   js/site.js animates every <details> on the page, so the two states it relies
   on have to be styled for every <details> too. Scoping these to `.faq` was why
   a disclosure written anywhere else lost its marker behaviour while still
   animating its height. The `.faq` rules above keep their own centring and stay
   authoritative there; these only reach the ones nothing else claims. */
details > summary { cursor: pointer; }
details:not([open]) > summary > span,
details.is-closing > summary > span { transition: transform .2s ease; }
.faq details.is-closing summary span { transform: translateY(-50%); }

.cta-band { display: flex; flex-wrap: wrap; gap: 26px 40px; align-items: end; justify-content: space-between;
  border-top: 1px solid var(--line, rgba(17,17,17,.18)); max-width: 1240px; }
.cta-band h2 { font-family: FeatureDeck, Georgia, serif; font-weight: 400;
  font-size: clamp(1.6rem, 3vw, 2.4rem); line-height: 1.1; margin: 0; max-width: 20ch; }
.cta-actions { display: flex; flex-wrap: wrap; gap: 12px; margin: 0; }

.contact-grid { display: grid; gap: 44px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); max-width: 1160px; align-items: start; }

/* THE SHORT COLUMN LEAVES A VOID, AND THE VOID READS AS BROKEN PADDING.
   Measured at 1280x900: prose column 1219px, the aside 676px, so `align-items:
   start` strands 543px of nothing beside it before the next section — the single
   worst gap anywhere in the template, and the only one of its kind (every other
   page measured clean at both widths). The section's own padding is correct;
   what is wrong is that one column simply stops.

   A grid item's containing block is its GRID AREA, which spans the full row even
   when `align-self: start` shrinks the item — so the aside has travel to stick
   within, and following the reader is better than leaving a hole. Below the
   wrap point the grid is one column and there is no void, so this is gated. */
@media (min-width: 901px) {
  .contact-grid > :last-child:not(:only-child) {
    position: sticky;
    top: calc(var(--nav-h, 72px) + 24px);
  }
}
.contact-phone { font-family: FeatureDeck, Georgia, serif; font-size: 2rem; text-decoration: none; }
.contact-lines { line-height: 2; }
.hours { list-style: none; margin: 0; padding: 0; max-width: 32ch; }
.hours li { display: flex; justify-content: space-between; gap: 18px; padding: 7px 0; border-bottom: 1px solid var(--line, rgba(17,17,17,.18)); }

/* ── FORM ───────────────────────────────────────────────────────────────────
   Labels are persistently visible and are never replaced by a placeholder: a
   placeholder disappears the moment typing starts, which strands anyone who loses
   their place, and it fails contrast in every browser's default grey. SC 3.3.2.

   .field-err is empty until validation fills it, and is NOT hidden when empty —
   its aria-describedby association is set in the markup, and a container that is
   display:none when empty can be skipped by some screen readers at the moment the
   text appears. An empty <p> costs one line box and removes that risk. */
.request-form { max-width: 46rem; }
.request-form label { display: block; margin: 20px 0 6px; font-weight: 500; }
.request-form .opt { font-weight: 400; opacity: .62; }
.request-form input, .request-form textarea {
  width: 100%; padding: 12px 13px; border-radius: 4px;
  border: 1px solid rgba(17, 17, 17, .42);       /* 3:1 against cream — SC 1.4.11 */
  background: #fffdf8; color: var(--ink, #111);
}
.request-form textarea { resize: vertical; min-height: 7em; }
.request-form input[aria-invalid="true"], .request-form textarea[aria-invalid="true"] {
  border-color: #a3260a; border-width: 2px;
  /* Colour is never the only signal — .field-err carries words. SC 1.4.1. */
}
.hint { margin: 6px 0 0; font-size: .86rem; opacity: .72; }
.field-err { margin: 6px 0 0; font-size: .88rem; color: #8f2108; font-weight: 500; min-height: 0; }
.form-error { margin: 0 0 4px; color: #8f2108; font-weight: 500; }
.form-error:empty { margin: 0; }
.form-success { margin: 18px 0 0; padding: 13px 15px; border-radius: 4px;
  background: rgba(17, 17, 17, .05); border-left: 3px solid var(--orange, #ff5b20); }
.request-form button { margin-top: 26px; }

/* ── NOTICE ─────────────────────────────────────────────────────────────────
   The template warnings. Loud on purpose — every one of them marks something that
   must be changed before a real business publishes the page, and a quiet note is
   a note that ships. */
.notice { border-left: 4px solid var(--orange, #ff5b20); background: rgba(255, 91, 32, .07);
  border-radius: 0 5px 5px 0; max-width: 1100px; padding-top: 26px; padding-bottom: 26px; }
.notice h2 { font-size: 1rem; font-weight: 500; margin: 0 0 .6em; font-family: Aeonik, Arial, sans-serif;
  text-transform: uppercase; letter-spacing: .07em; }
.notice p { max-width: 74ch; line-height: 1.6; margin: 0 0 .8em; }
.notice p:last-child { margin-bottom: 0; }

/* ── THE CONFORMANCE TABLE ──────────────────────────────────────────────────
   55 rows of five columns will not fit a phone, and shrinking type until it does
   would fail 1.4.4 and 1.4.10 both. So the table keeps its natural width and its
   wrapper scrolls horizontally — SC 1.4.10 Reflow explicitly permits a data table
   to scroll in one axis, because tabular relationships are meaning and reflowing
   them destroys it.

   THE WRAPPER IS A FOCUSABLE role="region", which is not decoration: a scrollable
   box that cannot be reached by keyboard is content a keyboard user cannot read.
   tabindex="0" makes it scrollable by arrow key, and the label tells a screen
   reader what they have landed in. */
.tablewrap { overflow-x: auto; max-width: 100%; border: 1px solid var(--line, rgba(17,17,17,.18)); border-radius: 6px; }
table.wcag { border-collapse: collapse; width: 100%; min-width: 62rem; font-size: .9rem; }
table.wcag caption { text-align: left; padding: 14px 16px; font-weight: 500; border-bottom: 1px solid var(--line, rgba(17,17,17,.18)); }
table.wcag th, table.wcag td { text-align: left; vertical-align: top; padding: 11px 14px; border-bottom: 1px solid rgba(17, 17, 17, .1); }
table.wcag thead th { background: rgba(17, 17, 17, .045); font-weight: 500; white-space: nowrap; position: sticky; top: 0; }
table.wcag tbody th { white-space: nowrap; font-weight: 500; font-variant-numeric: tabular-nums; }
table.wcag td:nth-child(2) { min-width: 15rem; }
table.wcag td:last-child { min-width: 26rem; line-height: 1.5; }
.lvl { display: inline-block; min-width: 2.1em; padding: 2px 6px; border-radius: 3px;
  background: rgba(17, 17, 17, .08); font-size: .82rem; text-align: center; }
.tag { display: inline-block; margin-left: 5px; padding: 1px 5px; border-radius: 3px;
  background: rgba(255, 91, 32, .16); font-size: .68rem; font-weight: 400; text-transform: uppercase; letter-spacing: .05em; }
.status { display: inline-block; padding: 3px 8px; border-radius: 3px; white-space: nowrap; font-size: .84rem; }
/* Each status carries a word, so the colour is reinforcement rather than the
   signal — SC 1.4.1 Use of Color. */
.status--ok { background: rgba(28, 105, 47, .13); color: #17542a; }
.status--partial { background: rgba(168, 106, 0, .15); color: #7a4c00; }
.status--na { background: rgba(17, 17, 17, .07); color: #4a4a4a; }

/* ── FOOTER ADDITIONS ───────────────────────────────────────────────────────── */
#site-footer { display: grid; gap: 34px 40px; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); align-items: start; }
.foot-brand { grid-column: span 2; min-width: 240px; }
.foot-addr { line-height: 1.7; margin: 14px 0 0; font-size: .92rem; }

/* SINGLE COLUMN ON PHONES — AND WHY THIS RULE HAS TO EXIST.
   main.css already collapses the footer at 900px (`footer{grid-template-columns:1fr}`),
   but the rule above is an ID selector, so it outranked that media query at every
   width and the footer stayed multi-column down to 320px. There it could not fit:
   `.foot-brand` spans 2 tracks and each track is `minmax(180px, 1fr)`, so the grid
   demanded 180 + 40 + 180 = 400px of a 280px content box and pushed the document
   30px wider than the viewport — a real horizontal scroll at 320px, which is the
   one width SC 1.4.10 Reflow names. Measured before: 350px scrollWidth in a 320px
   viewport, with window.scrollX reaching 30. Fixed 2026-09-15. */
@media (max-width: 600px) {
  #site-footer { grid-template-columns: 1fr; }
  .foot-brand { grid-column: auto; min-width: 0; }
}
.foot-nav h3 { font: 400 10px/1 Aeonik, Arial, sans-serif; text-transform: uppercase; letter-spacing: .08em;
  margin: 0 0 14px; opacity: .66; }
.foot-nav ul { list-style: none; margin: 0; padding: 0; }
/* SC 2.5.8 Target Size. These links are under 24px tall on their own, and clear
   the criterion through its spacing exception: 10px of vertical padding puts more
   than 24px between the centres of adjacent targets. Reduce this padding and the
   exception stops applying — it is not cosmetic. */
.foot-nav li { margin: 0; }
.foot-nav a { display: inline-block; padding: 5px 0; font-size: .92rem; text-decoration: none; }
.foot-nav a:hover { text-decoration: underline; }
/* NO opacity HERE, AND THAT IS A MEASURED FIX RATHER THAN A STYLE PREFERENCE.
   main.css already sets `footer small{color:#aaa}`, which against the #111 footer
   is 8.13:1 and passes comfortably. Adding opacity:.66 on top did not dim it by a
   third of the way to the criterion — it composited #aaa toward the BACKGROUND,
   giving an effective #767676 and 4.15:1, under the 4.5:1 floor for 11px text.
   axe caught it on all 30 pages at once.

   The general trap: opacity is a compositing operation, not a colour. Any element
   that already has a deliberate colour against a known background will be moved
   toward that background by opacity, and a value that looks harmless on white can
   fail on dark. State the colour instead, where the contrast can be computed. */
.foot-legal { grid-column: 1 / -1; line-height: 1.6; max-width: 90ch; }

.foot-motion { grid-column: 1 / -1; }
.motion-toggle {
  font: 400 11px/1 Aeonik, Arial, sans-serif; text-transform: uppercase; letter-spacing: .07em;
  padding: 11px 16px; min-height: 24px;
  border: 1px solid var(--line, rgba(17,17,17,.18)); border-radius: 999px;
  background: transparent; color: inherit; cursor: pointer;
}
.motion-toggle[aria-pressed="true"] { background: rgba(17, 17, 17, .09); border-color: rgba(17, 17, 17, .42); }
.motion-toggle[aria-pressed="true"]::after { content: " · on"; opacity: .7; }

/* ── REDUCED MOTION ─────────────────────────────────────────────────────────
   The OS setting removes every transition this file introduces. The dropdown still
   opens and closes — visibility is not a transition — so the menu keeps working
   while the animation stops. */
@media (prefers-reduced-motion: reduce) {
  /* `.subnav` covers BOTH the desktop panel and the phone accordion's max-height:
     the declaration is !important, so it beats the media-query rule that sets the
     phone transition regardless of which is more specific. A phone-scoped repeat
     was tried here and removed — planting its removal left the test green, which
     is the correct answer and proved the line was dead weight. */
  .subnav, .navtoggle::before, .menu::after, .faq summary span { transition: none !important; }
  :root { scroll-behavior: auto; }
}

/* ── PRINT ──────────────────────────────────────────────────────────────────
   The conformance table is the page most likely to be printed or sent as a PDF —
   it is the artefact a client hands to a lawyer or a procurement officer. Make it
   survive the trip. */
@media print {
  .header, .cta-band, .crumbs, .motion-toggle, .skip { display: none !important; }
  .tablewrap { overflow: visible; border: 0; }
  table.wcag { min-width: 0; font-size: 9pt; }
  table.wcag thead th { position: static; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 8pt; word-break: break-all; }
}

/* ── THE DEMO CHIP ──────────────────────────────────────────────────────────
   Marks a block holding example content rather than this business's. Loud on
   purpose and only rendered while CREDENTIALS.placeholder is true, so filling the
   page in removes it along with the notice — one flag, not two things to remember.
   Not the orange of the notice panel: this is a label on otherwise-real-looking
   copy, and it has to read as an annotation rather than as an error. */
.demo-chip {
  display: inline-block; vertical-align: middle; margin-left: .5em;
  padding: 3px 8px; border-radius: 999px;
  font: 500 10px/1 Aeonik, Arial, sans-serif; text-transform: uppercase; letter-spacing: .08em;
  background: rgba(50, 31, 97, .12); color: #321f61;   /* --purple, 6.6:1 on cream */
  border: 1px solid rgba(50, 31, 97, .3);
}
[data-demo] { position: relative; }

/* ── OWNER-ONLY NOTES ───────────────────────────────────────────────────────
   A note with nothing in it for the client to replace has no business being on a
   published page: a visitor should never read the builder's instructions, and an
   owner hunting for something to fill in should not be sent to a block that turns
   out to be advice. So these do not render at all by default.

   THEY ARE STILL IN THE HTML, and that is the point rather than a compromise —
   whoever opens the file still finds the guidance where it applies. The chat widget
   sets html.wbc-owner when it has a valid owner token, which is the same gate that
   lets the banner and the filler outlines exist, so exactly one person sees them.

   display:none rather than a visually-hidden pattern: this is not content and it
   must not reach a screen reader on a live page either. */
[data-note="publish"] { display: none; }
html.wbc-owner [data-note="publish"] { display: block; }

.note-private {
  display: inline-block; vertical-align: middle; margin-left: .6em;
  padding: 3px 9px; border-radius: 999px;
  font: 500 10px/1 Aeonik, Arial, sans-serif; text-transform: uppercase; letter-spacing: .07em;
  background: rgba(17, 17, 17, .1); color: #3c3c3c;
  border: 1px solid rgba(17, 17, 17, .22);
}

/* ── THE CHAT DOCK ──────────────────────────────────────────────────────────
   Where a form used to be. The panel itself is not authored into these — one chat
   exists per page and js/chatdock.js moves it in — so the dock has to look
   deliberate while empty, which is the state every dock but one is in at any
   moment. A min-height rather than nothing: a dock that collapses to zero makes the
   page jump by the panel's height every time the chat arrives, and on the request
   section that jump lands under the reader's thumb. */
.chat-dock { min-height: 320px; max-width: 46rem; }
.chat-dock:empty { border: 1px dashed var(--line); border-radius: 8px; }
.dock-lead { max-width: 46rem; line-height: 1.6; margin: 0 0 18px; }
.dock-lead .contact-phone { font-family: inherit; font-size: inherit; font-weight: 500; }

/* The panel is the hero's own shape while it is up there and an ordinary block
   down here, so the dock supplies the frame the hero's .ap-chat was supplying. */
.chat-dock .wbchat {
  display: block; background: rgba(255, 255, 255, .5);
  border: 1px solid var(--line); border-radius: 8px; padding: 18px;
}
@media (max-width: 600px) { .chat-dock { min-height: 280px; } }

/* ── THE DOCK ON .request, WHICH IS AN ORANGE SECTION ───────────────────────
   The dock's default frame is a translucent white over the page's cream, which on
   this section's orange composited to pink — the panel read as a different product
   rather than as the same chat the hero shows. Solid cream and the ink text it was
   designed for; the section's own colour stays the frame around it. */
.request-dock { display: flex; flex-direction: column; }
.request .chat-dock .wbchat {
  background: var(--cream);
  border-color: rgba(17, 17, 17, .22);
  color: var(--ink);
}
.request .dock-lead { max-width: 42rem; }

/* ── THE SAFETY NET ─────────────────────────────────────────────────────────
   A block still marked data-demo holds invented content — a plumber who does not
   exist, a licence number that belongs to nobody, a press mention that never
   happened. If a client publishes without filling it in, that content is live under
   their name, and the failure is not cosmetic: an invented tradesperson on a
   plumbing site is the kind of thing that ends up in a complaint.

   So it does not render. Same gate as the owner-only notes: hidden by default,
   revealed under html.wbc-owner, which the chat widget sets only when it holds a
   valid owner token. The owner still sees, edits and is nagged about every block;
   a customer sees a shorter page.

   A SHORTER PAGE IS THE POINT, AND IT IS THE RIGHT TRADE. Hiding these can leave
   /reviews/ almost empty and take five blocks off /credentials/. That is worse
   looking and better: the alternative is a page that reads as finished and is
   fiction. Nothing that remains is untrue.

   html[data-show-demo] IS THE ESCAPE HATCH, set from ?demo=1 by js/site.js, so the
   template itself can still be reviewed as designed without the chat widget
   present. It is a URL parameter rather than a build flag on purpose: a build flag
   is a thing you can forget to turn off. */
[data-demo] { display: none; }
html.wbc-owner [data-demo],
html[data-show-demo] [data-demo] { display: block; }

/* ── PAGES UNLINKED BY THE FILLER SWEEP ─────────────────────────────────────
   build.mjs marks every link to a page that falls under the thin-content floor
   once its filler is hidden. A visitor never sees the link; the owner sees it
   faded, labelled, and still clickable.

   STILL CLICKABLE IS THE POINT. The owner is the only person who can fix the page,
   and a link they cannot follow sends them looking for another way in. Fading it
   says "not public"; removing it would say "gone", which is false — the page is
   whole, it simply is not advertised yet.

   display:revert rather than a hard-coded value: these links live in the header
   nav, the footer columns and body prose, and each has its own display. Restoring
   the one the cascade would otherwise have given is the only version that works in
   all three without listing them. */
/* ── A UNIVERSAL PHRASE THE CLIENT HAS OPTED OUT OF ─────────────────────────
   The agency owns the words in a [data-universal] block, so the client cannot
   edit them. Their escape hatch is to HIDE it and write their own version in a
   normal block underneath — which only works if hiding actually removes it from
   the page. Until this rule existed the attribute was written correctly and did
   nothing: the block kept its fallback text and a visitor still read it, so an
   opt-out silently did not opt out.

   The block is hidden, NOT emptied. The fallback text stays in the markup so the
   client can un-hide and get a working paragraph back, and so the next agency
   update has something to substitute into.

   THE OWNER STILL SEES IT, following [data-page-hidden] above: the only person
   who can undo the hide is the one who made it, and a block that vanishes from
   their own view too gives them nothing to click. Faded and badged says "not
   public"; absent would say "gone", which is false. */
[data-universal-hidden] { display: none !important; }

html.wbc-owner [data-universal-hidden] {
  display: revert !important;
  opacity: .55;
  position: relative;
  border-left: 2px solid rgba(217, 135, 0, .8);
  padding-left: .8em;
}
html.wbc-owner [data-universal-hidden]:hover,
html.wbc-owner [data-universal-hidden]:focus-within { opacity: 1; }

/* Block-level, so the badge sits ABOVE the prose rather than trailing it — after
   a paragraph it would read as part of the sentence. */
html.wbc-owner [data-universal-hidden]::before {
  content: "hidden by you";
  display: block;
  margin-bottom: .35em;
  padding: 1px 6px;
  border-radius: 999px;
  background: #fde68a;
  color: #713f12;
  border: 1px solid #f0c760;
  font: 600 9px/1.6 Aeonik, Arial, sans-serif;
  text-transform: uppercase;
  letter-spacing: .06em;
  width: fit-content;
}

[data-page-hidden] { display: none !important; }

html.wbc-owner [data-page-hidden] {
  display: revert !important;
  opacity: .55;
  position: relative;
  text-decoration: line-through;
  text-decoration-thickness: 1px;
  text-decoration-color: rgba(217, 135, 0, .8);
}
html.wbc-owner [data-page-hidden]:hover,
html.wbc-owner [data-page-hidden]:focus-visible { opacity: 1; }

/* The label rides on the link so it travels wherever the link is, and it carries
   the word rather than an icon — an icon here would need explaining, and the whole
   point is that the owner should not have to ask. */
html.wbc-owner [data-page-hidden]::after {
  content: "hidden";
  display: inline-block;
  margin-left: .45em;
  padding: 1px 6px;
  border-radius: 999px;
  background: #fde68a;
  color: #713f12;
  border: 1px solid #f0c760;
  font: 600 9px/1.6 Aeonik, Arial, sans-serif;
  text-transform: uppercase;
  letter-spacing: .06em;
  text-decoration: none;
  vertical-align: middle;
}
/* In the dropdown the link is a block with its own second line, so the badge sits
   beside the name rather than after the description. */
html.wbc-owner .subnav [data-page-hidden] { display: block !important; }
