/* Cuberis micro-site platform — core stylesheet.
   All color/typography comes from CSS custom properties set per-site in the
   base layout from site.config.json. Components are scoped by class; the
   body carries nav-style-{top|side|slideout} so presentation variants can
   coexist in one sheet.

   THE HOUSE STYLE IS THE :root BLOCK BELOW (2026-08-05). Every design decision
   — colours, every text size, menu padding, hover states, corner radii — is a
   custom property declared once there, and the rules in this file just read it.
   That block is GENERATED from tools/design.py and must not be hand-edited:
   change a value there and run `python tools/design.py --sync`. Plain
   `python tools/design.py` checks the two agree and that nothing declares a
   token no rule reads (a control in the design admin that does nothing).

   A single site overrides any of it from its own settings — see
   http://localhost:8825/__design/ — which is emitted as a second :root block in
   each page's <head>, after this file. Nothing else about a site's look should
   need a stylesheet of its own.

   TEXT SIZES ARE ALWAYS IN rem (Eric 2026-08-05), never px — a visitor who has
   set a larger default text size in the browser gets it honoured by rem and
   ignored by px. `em` is fine where the intent really is "relative to the text
   around it" (h3/h4). px stays correct for hairlines, radii and breakpoints.
   Design Standards: `font-sizes-in-rem`. */

/* @house-style */
/* THE HOUSE STYLE. Generated from tools/design.py — run
   `python tools/design.py --sync` after changing a default there,
   and never hand-edit between these two markers.
   A site overrides any of these from its own settings; see
   http://localhost:8825/__design/. Text sizes are rem, always. */
:root {
  /* Palette */
  --color-primary:      #1f3a5f;
  --color-accent:       #b08d57;
  --card-bg:            transparent;
  --color-bg:           #ffffff;
  --color-text:         #1a1a1a;
  --heading-color:      var(--color-primary);
  --link-color:         var(--color-primary);
  --link-hover-color:   var(--color-accent);
  /* Typography */
  --font-heading:       Georgia, 'Times New Roman', serif;
  --font-body:          -apple-system, 'Segoe UI', Roboto, sans-serif;
  --text-base:          1.0625rem;
  --line-body:          1.6;
  --line-heading:       1.25;
  /* Headings */
  --h1:                 2rem;
  --h2:                 1.5rem;
  --h3:                 1.17em;
  --h4:                 1em;
  --hero-title-size:    2.8rem;
  --hero-tagline-size:  1.25rem;
  --tagline-size:       1.2rem;
  --tagline-color:      var(--color-accent);
  /* Header & menu */
  --header-bg:          var(--color-primary);
  --header-text:        #ffffff;
  --nav-color:          var(--header-text);
  --site-title-size:    1.35rem;
  --nav-size:           1em;
  --nav-weight:         400;
  /* Capitals read LARGER and heavier than the same size in mixed case,
     so a menu switched to caps usually wants a smaller --nav-size and a
     lighter --nav-weight to keep the same presence - and it always wants
     more --nav-tracking, because caps set solid are hard to read.
     Applies to all three menu styles. Dropdown items are NOT included:
     they have their own --nav-sub-transform, which defaults to none, so
     capitalising the main menu leaves its contents in ordinary case and
     the two stay told apart. (2026-08-20) */
  --nav-transform:      none;
  /* An em value scales with the menu size and a px value does not, so
     prefer em unless a specific hairline of space is wanted. This is not
     a text size, which is why px is allowed here at all. (2026-08-20) */
  --nav-tracking:       normal;
  --nav-pad-y:          .5rem;
  --nav-pad-x:          .7rem;
  --nav-radius:         4px;
  --nav-hover-bg:       rgba(255,255,255,.16);
  --nav-hover-color:    #ffffff;
  --nav-menu-bg:        var(--header-bg);
  /* A dropdown item that is the same size as the section it hangs off
     reads as a second row of main menu rather than as its contents.
     The mobile accordion had always set .95em for exactly this reason
     and the desktop dropdown had nothing, so the two disagreed; this is
     that value made into one control. (2026-08-19) */
  --nav-sub-size:       .95em;
  /* Square the panel AND --nav-radius together or they disagree
     visibly. Rounding the items also bends the section underline: it is
     drawn as an inset shadow, so it follows the corner and curls up at
     both ends instead of reading as a straight rule. (2026-08-19) */
  --nav-menu-radius:    0 0 6px 6px;
  /* The chrome may speak in a different voice from the reading matter,
     and on a serif site it often should: menus are signposts, not prose,
     and a display serif set at menu size goes mushy. Defaults to the body
     font, so a site that never sets it looks exactly as it did. (2026-08-19) */
  --nav-font:           var(--font-body);
  /* Separate from --nav-transform on purpose. A dropdown set in the
     same capitals as the section it hangs off reads as a second row of
     main menu; leaving it in ordinary case is most of what tells a
     reader which is which. (2026-08-20) */
  --nav-sub-transform:  none;
  /* Separate from --nav-tracking, and defaulting to normal even when
     the main menu is tracked out. Tracking is what makes CAPITALS
     readable and what makes ordinary lower case look loose and gappy,
     so a menu set in tracked caps over a dropdown set in ordinary case
     wants the two settings to disagree. (2026-08-20) */
  --nav-sub-tracking:   normal;
  /* Desktop only. Below 761px the dropdown is an indented accordion
     inside the header rather than a floating panel, and it keeps its own
     tap-sized padding - shrinking a thing you hit with a thumb is not the
     same decision as tightening a panel you hit with a cursor. (2026-08-20) */
  --nav-sub-pad:        var(--nav-pad-y) var(--nav-pad-x);
  /* The main menu keeps its underline either way; this is only the
     dropdown copy of it. Marking BOTH means two underlines on screen at
     once saying the same thing, and the one on the section is the one
     that is visible when the dropdown is shut. (2026-08-20) */
  --nav-sub-marker:     inset 0 -2px 0 0 currentColor;
  /* A Ken Burns move. Slides alternate in and out so a slideshow does
     not read as one long push one way, and the sideways drift is derived
     from this number rather than set separately, so it cannot be pushed
     past the edge of the picture. Keep it small: scaling up spends
     resolution, and past about 1.15 it stops reading as atmosphere and
     starts reading as an effect. Ignored entirely for a reader who has
     asked their computer to reduce motion. (2026-08-20) */
  --hero-zoom:          1;
  /* Pair with --nav-drop. flex-end alone bottom-aligns the menu against
     the header's CONTENT box, which still has the band's bottom padding
     under it; --nav-drop is what carries the menu the rest of the way
     onto the edge. Desktop only - below 761px the menu is a full-width
     accordion that opens downward inside this same box. (2026-08-20) */
  --header-align:       center;
  /* The point of a negative value is the 'you are here' underline: at
     0 it floats somewhere in the middle of the header, and at minus the
     band's bottom padding it lands ON the bottom edge and reads as a rule
     belonging to the header. Moves the menu ONLY, so the name keeps its
     own breathing room. (2026-08-20) */
  --nav-drop:           0;
  /* Buttons & forms */
  --button-bg:          var(--color-primary);
  --button-color:       #ffffff;
  --button-hover-bg:    var(--color-accent);
  --button-weight:      600;
  --button-radius:      4px;
  --button-pad:         .6rem 1.4rem;
  --focus-color:        var(--color-accent);
  --focus-width:        3px;
  /* Cards & panels */
  --card-border:        1px solid color-mix(in srgb, var(--color-text) 15%, transparent);
  --card-radius:        6px;
  --card-pad:           1rem;
  --card-min:           14rem;
  --grid-gap:           1rem;
  --caption-size:       .85rem;
  --breadcrumb-size:    .9rem;
  --pullquote-size:     1.15rem;
  --abstract-color:     #444444;
  /* Layout */
  --content-width:      72rem;
  --text-share:         .75;
  --text-width:         46rem;
  --reading-place:      .5;
  --gutter:             1.25rem;
  --radius:             6px;
  /* Spacing */
  --space-xs:           .75rem;
  --space-s:            clamp(1.25rem, .99rem + 1.1vw, 1.875rem);
  --space-m:            clamp(1.25rem, .73rem + 2.21vw, 2.5rem);
  --space-l:            clamp(2.5rem, 1.46rem + 4.42vw, 5rem);
  --space-xl:           clamp(3.75rem, 2.71rem + 4.42vw, 6.25rem);
  /* This is the one that makes a site-wide rhythm control possible: every
     block, pattern and standalone picture reads it instead of carrying its
     own margin, so changing it here changes every page without editing one.
     It points at a STEP rather than holding its own measurement, so the
     ladder has no overlap — 'Normal' and 'S' are the same gap, and the
     spacing control never offers two names for one amount. (2026-08-06) */
  --space-block:        var(--space-s);
  /* Footer */
  --footer-bg:          var(--color-primary);
  --footer-text:        #ffffff;
  /* Same reasoning as --nav-font: a footer is signposting, not prose.
     Set the two together unless you mean them to disagree. (2026-08-19) */
  --footer-font:        var(--font-body);
  /* An em of the body size rather than a rem, so a site that scales its
     text scales its footer with it. The footer is reference matter -
     an address, a phone number, a list of links - and usually wants to
     sit quieter than the page above it. (2026-08-19) */
  --footer-size:        1em;
  /* CHECK THE FACE EXISTS before lowering this. A weight the font does
     not carry silently snaps to the nearest one that does, and looks
     exactly like a setting that had no effect - measured on Archivo,
     300/400/500 all rendered identically until the lighter cuts were
     actually vendored. (2026-08-19) */
  --footer-weight:      400;
  /* Primary, not accent. The accent is the decorative colour — a highlight, a
     rule, a hover — and nothing about choosing it implies it can carry white
     text. Defaulting the banner to it meant every site got a WCAG failure
     unless its accent happened to be dark: Tuckerton's brand gold gives white
     3.09:1 against a required 4.5:1, and the demo theme's 3.79:1. The primary
     already carries white in the header, the footer and every button, so it is
     the one colour on the palette a site has effectively already proved. Set
     --banner-bg explicitly for a different look; the contrast checker in
     tools/design.py will say whether it holds up. (2026-08-05) */
  --banner-bg:          var(--color-primary);
  /* A token, not a hard-coded #fff, and the reason is a real failure. The
     banner used to be `color: #fff` against a background any site could set.
     Tuckerton picked a mid gold, which gives white 3.78:1 where 4.5:1 is
     required, and the contrast checker in tools/design.py never saw it — it
     can only compare two TOKENS, so a colour written into the stylesheet was
     invisible to the one thing built to catch this. Both halves nameable means
     both halves checkable. (2026-08-05) */
  --banner-text:        #ffffff;
}
/* @end-house-style */
/* ── DERIVED WIDTHS, and why they are not in the block above ─────────────
   Everything above this line is GENERATED from tools/design.py and is wiped
   and rewritten by `python tools/design.py --sync`. These two are worked out
   FROM those settings rather than being settings themselves, so they live
   here, below the line, where a sync cannot eat them.

   It also matters for design.py's own check, which is the thing that told me
   this was in the wrong place: it reads everything BELOW the house block and
   reports any setting nothing reads as "a control that would do nothing".
   With these two inside the generated block, --content-width and --text-share
   were read only by a line the check does not look at, so two live controls
   were reported dead. A derived token has to sit where the derivation is
   visible or it hides the settings it depends on.
   ───────────────────────────────────────────────────────────────────── */
:root {
/* ── HOW WIDE THINGS ARE (rewritten 2026-08-22) ──────────────────────
   Two DERIVED values, so the two rules below are stated once instead of
   six and eleven times. Both are ordinary custom properties, so a site
   that overrides --content-width or --text-share in its own :root gets
   these recomputed for free.

   Eric, 2026-08-22, having dropped a pricing table into a page and found
   it squeezed into the reading column:

     "The blocks look like ... everything is being forced within that,
      including the themes, the table, the HTML stuff as well as the
      gallery. Really everything else other than the text should go full
      width, within the margins of the site."

   and, on the gutter:

     "The main container has padding of twenty left and right ... the
      padding shouldn't be decreasing the amount of eleven-seventy if
      that's the base for the content area."

   TWO FAULTS, and they are independent:

   1. THE GUTTER WAS SUBTRACTED, NOT ADDED. Every shell here is
      `max-width: --content-width` PLUS `padding: … var(--gutter)`, and
      with border-box the padding comes out of the width: a site set to
      1152 gave 1112 of usable content. Set it to 1170 and you got 1130.
      The number you type should be the number you get, so the shells are
      capped at --shell-width and the gutter sits OUTSIDE it.

      All six shells move together or they stop lining up with each other,
      and a logo half a gutter out of line with the text underneath it is
      the kind of wrong nobody can name but everybody sees.

   2. THE READING COLUMN WAS ON THE WRAPPER. `.page` carried
      `max-width: --text-width`, so it did not narrow the TEXT, it narrowed
      the whole content area — every gallery, slider, table and pasted
      block with it. It now sits on running text only; see "The reading
      column" below the layouts.

   --reading-width is a SHARE with a CEILING, which is Eric's call between
   the two obvious answers. A bare share means widening the page later
   quietly stretches the lines back out, which is the one thing a reading
   column exists to prevent; a bare length is not a proportion of anything
   and has to be re-guessed every time the page width moves. min() is both:
   three quarters of the page, never longer than a comfortable line.

   THE SHARE IS A UNITLESS NUMBER, AND OF --content-width, NOT OF THE
   CONTAINER. It was written as `min(46rem, 75%)` first, and that is wrong
   in a way worth recording, because it looks right and reads right:

     A percentage resolves against whatever is holding it AT THE TIME. On a
     390px phone the content area is 335px, so `75%` came out at 251px and
     threw away a quarter of the only screen that cannot spare it. And the
     range where the percentage beat the ceiling at all was 0 to about
     980px — precisely the widths where narrowing text helps nobody. The
     control was inert exactly where it was wanted and harmful exactly
     where it was not.

   As a share of the CONFIGURED page width it resolves to one fixed length,
   so on any screen narrower than that the cap is simply inert and the text
   fills the column. No breakpoint, no media query, right at every size.

   TO TURN NARROWING OFF, one page at a time, use the Full-width page
   layout — that is what it is for and it is the usual answer. Site-wide it
   takes BOTH controls (share to 1 and the ceiling up), because min() takes
   the smaller and raising one leaves the other in the way. Deliberately no
   third "narrowing: off" switch: a boolean beside a number is two controls
   that can disagree, and the disagreement is invisible on screen. */
--shell-width:        calc(var(--content-width) + var(--gutter) * 2);
--reading-width:      min(var(--text-width), calc(var(--content-width) * var(--text-share)));
/* WHERE THAT COLUMN SITS, added 2026-09-02. --reading-place is the setting
   (0 = against the left margin, .5 = centred, 1 = against the right); these
   two are the leftover space split by it, and every rule that positions the
   column reads one of them instead of doing the arithmetic again.

   Why a share and not a `margin-inline: auto | 0` switch. Three rules need
   this number, and one of them is the side-by-side picture, which needs the
   OTHER side of the same split — `auto` cannot be subtracted from anything,
   so a keyword would have left the floats behind at the old centre while the
   text moved. A number works in all three and cannot disagree with itself.

   The 100% resolves against the containing block, which is `.page` for the
   furniture rule and `.page-body` for the text rule. Both are the full content
   width — the same box --reading-width is a share of — so the two agree.

   max(0px, ...) IS LOAD-BEARING AND WAS FOUND IN A BROWSER, NOT BY READING
   THIS. Below the reading width there IS no leftover: on a 390px phone the
   content box is 335 and the column wants 736, so the subtraction comes out at
   -401 and half of that is a -200px left margin — every paragraph on every
   site dragged off the left edge of the screen. The rule this replaced said
   `auto`, and auto margins on an element wider than its cap simply resolve to
   zero, so the old code had this for free and the new code has to say it.
   Clamped, a narrow screen puts both values at 0 and the column fills the
   width, which is what it did before and what it should do. */
--reading-lead:       max(0px, calc((100% - var(--reading-width)) * var(--reading-place)));
--reading-trail:      max(0px, calc((100% - var(--reading-width)) * (1 - var(--reading-place))));
}


*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
/* THE PAGE DOES NOT SCROLL SIDEWAYS. Kept deliberately, and NOT for the block
   dress — the dress stopped needing it on 2026-09-01 when the ground moved to
   a box-shadow that spreads paint without moving the box (see "a ground is a
   band" below). It stays because SITE CHROME still needs it.

   WHAT ACTUALLY DEPENDS ON IT TODAY. CAMRO's own stylesheet full-bleeds its
   bands by moving the element — `.page-body .band, .page-body .c-hero {
   margin-inline: calc(50% - 50vw); padding-inline: calc(50vw - 50%) }` — and
   50vw counts the scrollbar while the page does not, so each band hangs half
   a scrollbar off each side. Measured A/B on the real CAMRO home page at
   1280/1265, same page, same build, one line changed:

     without this rule ....... 7px of sideways scroll   (bands at −7.5 → 1272.5)
     with it ................. 0                        (same bands, contained)

   Deleting this line therefore breaks a live site. It can be deleted the day
   those two rules move to the box-shadow idiom that `.p-cta--large` and
   CAMRO's own `.p-hero--navy` already use — at which point nothing overshoots
   and there is nothing left to contain.

   IT TAKES BOTH THIS AND THE ROOT, AND NEITHER ALONE DOES ANYTHING. Overflow
   propagates: whatever the root carries is handed to the viewport and the root
   is then treated as visible; if the root is visible, the BODY's value is
   handed up instead and the body is treated as visible. So one of the two is
   always being given away and the other is always the one clipping. Measured:
   body only 8px, html only 8px, both 0. CAMRO supplies the root half in its
   own `custom_css`; this is the body half, and it is why the live site is
   clean while a site without that line would not be. A site that bleeds by
   moving elements needs BOTH.

   VERIFY BY SCROLLING, NEVER BY READING THE COMPUTED STYLE — both halves
   report `overflow-x: clip` from getComputedStyle while the page scrolls
   perfectly happily, because the computed value is the value that was given
   away:

     document.documentElement.scrollLeft = 999   // must read back 0

   `clip` and NOT `hidden`: hidden makes the body a scroll container and kills
   `position: sticky` inside it — the site header and the editor bar both
   depend on that. Horizontal only; `overflow-y` is deliberately untouched. */
body { overflow-x: clip; }
/* STANDING RULE (Eric 2026-08-02): the footer always sits at the bottom of
   the viewport, even on short pages. Flex column + margin-top:auto on the
   footer does this with no header/footer height math, and stays correct
   when a banner is injected or the header wraps. */
body {
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  font-family: var(--font-body);
  color: var(--color-text);
  background: var(--color-bg);
  line-height: var(--line-body);
  font-size: var(--text-base);
}
.site-footer { margin-top: auto; }
main, .layout-side { width: 100%; }
h1, h2, h3, h4 {
  font-family: var(--font-heading);
  line-height: var(--line-heading);
  color: var(--heading-color);
}
h1 { font-size: var(--h1); }
h2 { font-size: var(--h2); }
/* h3 and h4 carried no size at all until 2026-08-05 and took the browser's own
   (1.17em and 1em of the surrounding text). The house block declares those exact
   values, so nothing moved — but the two levels a long page leans on hardest are
   now adjustable like the rest. */
h3 { font-size: var(--h3); }
h4 { font-size: var(--h4); }
/* A heading's SIZE, apart from its level (2026-09-25): `{size=…}` on a
   heading (eleventy-base.js cuberis_heading_size) or a Section's
   heading_size. The four sizes are the site's own heading tokens, so a size
   is never a number somebody typed; unset is the size of the level. */
:is(h1, h2, h3, h4, h5, h6).h-size-small   { font-size: var(--h4); }
:is(h1, h2, h3, h4, h5, h6).h-size-medium  { font-size: var(--h3); }
:is(h1, h2, h3, h4, h5, h6).h-size-large   { font-size: var(--h2); }
:is(h1, h2, h3, h4, h5, h6).h-size-display { font-size: var(--h1); }
img { max-width: 100%; height: auto; }
a { color: var(--link-color); }
/* The site name in the header is the logo, not a link to decorate — it holds
   its colour in every state (Eric 2026-08-03). Excluded here rather than
   overridden on .site-title so each theme's own title colour still wins. */
a:not(.site-title):hover { color: var(--link-hover-color); }
:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: 2px;
}

/* Skip link */
.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--color-primary); color: #fff; padding: .6rem 1rem;
}
.skip-link:focus { left: 0; }

/* Text only a screen reader reads. Not display:none — that hides it from
   screen readers too, which is the opposite of the point. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* Leaves-this-page mark. Added automatically to every link that opens a new
   tab (documents and anywhere off this site) — see the newTabLinks build step.
   It is deliberately small and inherits the link colour: it is a signpost, not
   decoration. The nbsp keeps it from wrapping onto a line of its own. */
.ext-mark {
  width: .72em; height: .72em; margin-left: .28em;
  display: inline-block; vertical-align: baseline; flex: none;
}
a:has(> .ext-mark) { text-underline-offset: .15em; }
.site-nav .ext-mark, .site-footer .ext-mark { opacity: .8; }

/* ── THE UTILITY STRIP ABOVE THE MASTHEAD ─────────────────────────────────
   A thin full-bleed band for the ways IN to a site — a sign-in, a search —
   so the masthead below it can hold the name and the menu and nothing else.
   site-header.njk renders it only when `header.utility` exists, so a site
   without one is unchanged.

   IT WAS MARKUP WITHOUT RULES until 2026-09-08. Every site that opted in had
   to dress it in its own custom_css, which is how CAMRO came to carry four
   hand-written lines that said what these rules say. Keyed to the design
   tokens here, an opting-in site gets a strip that already matches it.

   RIGHT-ALIGNED, AND ALIGNED WITH THE MASTHEAD (Cam, 2026-09-08: put search
   and member sign-in on a bar above the header). --shell-width and --gutter
   are the masthead's own, so the last link on the strip and the last item of
   the menu below it share a right edge rather than missing it by the
   difference between two width tokens.

   WHITE INK, WRITTEN OUT. The ground is the brand primary, which every site
   on this platform sets dark enough to carry a logo reversed out of it; a
   token pair here would be two more controls to explain for a band with two
   links on it. A site that wants a different strip has custom_css. */
.header-utility {
  background: var(--color-primary); color: #fff;
  font-family: var(--nav-font); font-size: .82rem;
}
.header-utility-inner {
  max-width: var(--shell-width); margin: 0 auto;
  padding: .45rem var(--gutter);
  display: flex; align-items: center; justify-content: flex-end;
  gap: 1.5rem; flex-wrap: wrap;
}
/* Text, when a site has any, is a caption and sits at the far end: the links
   stay where the eye looks for account controls whether text is there or not. */
.header-utility-text { margin-right: auto; opacity: .88; }
.header-utility-link { color: inherit; text-decoration: none; font-weight: 600; }
.header-utility-link:hover { text-decoration: underline; text-underline-offset: 3px; }
.header-utility-link:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
/* 44px on a touch screen. The strip's type is small on purpose and small type
   is a small target; the row grows rather than the words. */
@media (pointer: coarse) {
  .header-utility-link { min-height: 44px; display: inline-flex; align-items: center; }
}

/* The search box in the strip, always open (no toggle since 2026-09-09).
   Sized in ch so the box is about as wide as what people actually type, not
   a full-width bar in a 36px strip. */
.header-utility-search { display: inline-flex; align-items: center; }
.header-utility-search-field { display: inline-flex; align-items: center; gap: .4rem; }
.header-utility-search-input {
  font: inherit; color: inherit; width: 18ch; max-width: 42vw;
  padding: .15rem .5rem; border-radius: 3px;
  border: 1px solid currentColor; background: transparent;
}
/* currentColor at full strength is the strip's text colour, which on a
   hairline strip is a heavier box than the words beside it. */
.header-utility-search-input { border-color: color-mix(in srgb, currentColor 45%, transparent); }
.header-utility-search-input::placeholder { color: inherit; opacity: .6; }
.header-utility-search-input:focus-visible,
.header-utility-search-go:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
.header-utility-search-go {
  font: inherit; font-weight: 600; color: inherit; cursor: pointer;
  padding: .15rem .55rem; border-radius: 3px;
  border: 1px solid color-mix(in srgb, currentColor 45%, transparent);
  background: transparent;
}
.header-utility-search-go:hover { background: color-mix(in srgb, currentColor 12%, transparent); }
@media (pointer: coarse) {
  .header-utility-search-input, .header-utility-search-go { min-height: 44px; }
}

/* Header */
.site-header { background: var(--header-bg); }
.site-header-inner {
  max-width: var(--shell-width); margin: 0 auto;
  padding: .75rem var(--gutter);
  display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
}
/* WHERE THE MENU SITS IN THE BAND, and why it takes two tokens rather than one.
   `--header-align: flex-end` bottom-aligns the header's children, but against
   its CONTENT box - the band's .75rem of bottom padding is still underneath,
   so the "you are here" underline stops short of the edge and reads as a rule
   floating in the middle of the header rather than one belonging to it.
   `--nav-drop` carries the menu the rest of the way, and only the menu: pulled
   down by the same .75rem the underline lands exactly on the band's bottom
   edge while the site name keeps its own breathing room above it.

   DESKTOP ONLY on purpose. Below 761px the menu is a full-width accordion that
   opens downward inside this same box, where dropping it into the padding
   would put the last item through the bottom of the header.

   The media query wraps the USE, never the declaration. A site's own tokens
   are written to :root in a <style> block after this file - same specificity,
   later in the cascade - so a `@media { :root { … } }` here would lose to any
   site that sets the plain token, and lose silently. Same trap as
   --lockup-top-size; see the note there. */
@media (min-width: 761px) {
  .site-header-inner { align-items: var(--header-align); }
  .site-header-inner > .nav-top { margin-bottom: var(--nav-drop); }
  /* ...but NOT on an immersive header, and this one is measured rather than
     tidy-minded. That header is transparent and sits directly on the home
     page's photograph, so (a) there is no container edge for the underline to
     land on, and (b) the dark screen that keeps white menu text legible over a
     slideshow is painted INSIDE this box - drop the menu onto the box's bottom
     edge and it stands where that screen has already faded out. Measured
     2026-08-19 on Tuckerton's three slides: white menu text over the bare
     picture lands at 3.06, 3.97 and 2.84 to 1. The header keeps its full
     height here and the menu keeps its cover. */
  .hero-banner .site-header-inner > .nav-top { margin-bottom: 0; }
}
.site-title {
  color: var(--header-text); text-decoration: none; font-family: var(--font-heading);
  font-size: var(--site-title-size); font-weight: 700;
  display: inline-flex; align-items: center; gap: .55rem;
}
.site-logo { height: 2.2rem; width: auto; display: block; }

/* Stacked title ("lockup"), opt-in per site — see components/site-header.njk.
   A name set as one long line reads as running text next to a menu; stacked
   over a rule it reads as a mark. Every value is a token so a site can retune
   it without a copy of this block, and the defaults alone give a usable lockup
   on a site that sets nothing but the two lines. */
:root {
  --lockup-gap:            .18rem;
  /* Fluid, NOT a fixed size plus a media query. A site's own tokens are written
     into a <style> block after this file, on :root — the same specificity, later
     in the cascade — so a `@media (max-width: 40rem) { :root { … } }` here loses
     to any site that sets the plain token, and loses silently: the phone layout
     simply keeps the desktop size. clamp() puts the responsiveness inside the
     value, where an overriding site inherits the behaviour or replaces it
     deliberately. Measured 2026-08-19: the media-query version left Tuckerton's
     lockup at 32px on a 375px screen. */
  --lockup-top-size:       clamp(1.4rem, 4.4vw, 1.9rem);
  --lockup-top-tracking:   .02em;
  /* Stated here rather than inherited from .site-title, so the mark is the same
     weight in the footer, where nothing sets it. */
  --lockup-top-weight:     700;
  /* `uppercase` turns the first line into caps. Worth knowing before you try it:
     caps read LARGER than the same size in mixed case and lose the descenders
     that gave the line its shape, so a lockup switched to caps usually wants a
     smaller size and a lighter weight to hold the same presence — which is
     exactly what happened here on 2026-08-19. Caps also need more tracking
     than mixed case at the same size. */
  --lockup-top-transform:  none;
  /* A RATIO (em), not a length, and that is load-bearing. Sized independently
     the two lines scale at different rates, so a lockup tuned to sit flush on
     a desktop comes apart on a phone: measured 2026-08-19 at 375px, the second
     line overhung the first by 17px. Expressed in em of the block — whose own
     size IS the top line — the proportion holds at every width for free. */
  --lockup-bottom-size:    .36em;
  --lockup-bottom-font:    var(--font-body);
  /* CHECK THE FACE EXISTS before trusting a value here. A weight the font does
     not carry snaps to the nearest one it does, silently, and reads as a
     setting that had no effect: 600 on Archivo rendered as 700 for as long as
     this said 600, which only showed up once the first line was lightened and
     the small line was suddenly the heavier of the two. Measure it — set the
     weight on a hidden span and compare rendered widths. (2026-08-19) */
  --lockup-bottom-weight:  600;
  /* TRACKING AND JUSTIFICATION ARE ONE SETTING, not two. The second line is
     justified out to the width of the first, and justification can only open
     the SPACES — so whatever the letters do not take up is dumped into the one
     gap between the two words. Measured here 2026-08-19 at .16em: the line came
     to 126px under a 170px first line, and all 44px of the difference went into
     that single space, which is what "why is there a hole in the middle of my
     logo" looks like. Track the line until it nearly fills on its own and the
     leftover gap becomes an ordinary word space; .4em got the natural width to
     166 of 170 and left a gap 2.5x the letter gap, which is about right for
     tracked caps. Raise the tracking, not the justification. */
  --lockup-bottom-tracking: .16em;
  /* The rule is drawn in the header's own text colour rather than the accent,
     so the lockup stays legible knocked out white over a photograph AND dark
     on a solid header, without the site having to state it twice. */
  --lockup-rule:           1px solid currentColor;
  --lockup-rule-opacity:   .45;
  --lockup-rule-space:     .22rem;
  /* The lockup standing in for the home page headline. Sized off the hero
     headline by default, so a site that turns this on needs no second
     number: every other proportion in the mark is an em of this one. Its
     own token because a mark and a sentence do not want the same size -
     stacked over a rule, a name reads considerably larger than the same
     value set as one line, and usually wants to go UP anyway because it
     is now the only thing on the picture. */
  --hero-lockup-size:      var(--hero-title-size);
}
.site-title--lockup { align-items: stretch; }
.site-title-lock {
  display: flex; flex-direction: column; gap: var(--lockup-gap);
  /* The lockup is a block of its own; without this the flex parent stretches
     the rule the full width of the header. */
  align-items: stretch;
  /* The block carries the top line's size so the second line can be a plain
     em fraction of it — see --lockup-bottom-size. */
  font-size: var(--lockup-top-size);
  /* Shrink to the mark. In the header it sits in an inline-flex link and would
     have done so anyway; in the FOOTER it is a block-level <p> in a column, and
     without this the stretched children take the whole column and the rule
     under the first line runs the full width of the footer. */
  width: fit-content;
}
.site-title-lock__top {
  font-size: 1em;
  /* Names its own face rather than inheriting one. The header wraps the lockup
     in .site-title (heading font); the footer wraps it in a footer that may
     have been switched to a different family entirely. A mark that changes
     typeface between the top and the bottom of one page is not a mark. */
  font-family: var(--lockup-top-font, var(--font-heading));
  font-weight: var(--lockup-top-weight);
  text-transform: var(--lockup-top-transform);
  letter-spacing: var(--lockup-top-tracking);
  line-height: 1;
  padding-bottom: var(--lockup-rule-space);
  border-bottom: var(--lockup-rule);
  /* Opacity on the border alone, which border-color cannot express against
     currentColor without repeating the colour. */
  border-bottom-color: color-mix(in srgb, currentColor
                       calc(var(--lockup-rule-opacity) * 100%), transparent);
}
.site-title-lock__bottom {
  font-family: var(--lockup-bottom-font);
  font-size: var(--lockup-bottom-size);
  font-weight: var(--lockup-bottom-weight);
  letter-spacing: var(--lockup-bottom-tracking);
  line-height: 1.1;
  text-transform: uppercase;
  /* The two lines must end flush, which is what makes this read as a mark
     rather than as two pieces of text. At Tuckerton's settings they happen to
     measure the same to the pixel — that is luck, and it stops being true the
     moment somebody edits the name or the size. Justifying the last (here,
     only) line spreads the shorter of the two out to the width of the longer,
     so the lockup stays locked up for any name. */
  text-align: justify;
  text-align-last: justify;
  /* Caveat for whoever uses this next: justification opens the SPACES, so a
     one-word second line ("Museum", "Society") cannot stretch and will sit
     left in the block. Set --lockup-bottom-tracking by hand for that case. */
}
/* Title hidden (home page, where the hero already prints the name): the nav is
   the only child left, and a flex row with one child puts it on the LEFT.
   Say where it goes rather than relying on space-between having two things. */
.site-header--no-title .site-header-inner { justify-content: flex-end; }

/* ── THE HEADER BUILDER (2026-09-25, HEADER_BUILDER_4C.md) ──────────────
   Every rule here answers to a value in site.config.json `header` that the
   builder writes (tools/header_config.py); a site that never used it carries
   none of these classes and none of this applies. Written one step more
   specific than a site's own `.site-header` / `.site-header-inner` rules
   (design.css loads after this file), because a layout a site's stylesheet
   silently overrides is a setting that does nothing.

   LAYOUTS ARE DESKTOP. Below 761px every header is the phone header, as it
   always was. READING ORDER IS SCREEN ORDER: the name is written before the
   menu and stays there; a split header centres the name over a gap in the
   menu rather than moving any of it. */
@media (min-width: 761px) {
  .site-header.site-header--stacked .site-header-inner,
  .site-header.site-header--below .site-header-inner {
    flex-direction: column; gap: .6rem;
  }
  .site-header.site-header--stacked .site-header-inner { align-items: center; justify-content: center; }
  .site-header.site-header--below .site-header-inner { align-items: flex-start; }
  .site-header.site-header--stacked .site-header-inner > *,
  .site-header.site-header--below .site-header-inner > * { margin-left: 0; margin-bottom: 0; }
  .site-header.site-header--action .site-header-inner > :is(.nav-top, .nav-mega) { margin-inline: auto; }

  .site-header.site-header--split .site-header-inner { position: relative; justify-content: center; }
  .site-header.site-header--split .site-title {
    position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1;
  }
  body .site-header.site-header--split .site-header-inner > :is(.nav-top, .nav-mega) { margin-inline: auto; }
  .site-header.site-header--split :is(.nav-top-list, .nav-mega-list) { justify-content: center; }
  .site-header.site-header--split-l1 :is(.nav-top-list, .nav-mega-list) > li:nth-child(1),
  .site-header.site-header--split-l2 :is(.nav-top-list, .nav-mega-list) > li:nth-child(2),
  .site-header.site-header--split-l3 :is(.nav-top-list, .nav-mega-list) > li:nth-child(3) {
    margin-right: var(--header-split-gap, 16rem);
  }
}
.site-header-action { flex: none; }

/* details */
.site-header.site-header--rule { border-bottom: 1px solid color-mix(in srgb, var(--header-text) 24%, transparent); }
.site-header.site-header--sticky { position: sticky; top: 0; z-index: 50; }
.site-header.site-header--compact .site-header-inner { padding-top: .45rem; padding-bottom: .45rem; }

/* the name's size: the site's own heading tokens, never a typed number */
.site-title.site-title--size-small   { --site-title-size: var(--h4); --lockup-top-size: var(--h4); font-size: var(--h4); }
.site-title.site-title--size-medium  { --site-title-size: var(--h3); --lockup-top-size: var(--h3); font-size: var(--h3); }
.site-title.site-title--size-large   { --site-title-size: var(--h2); --lockup-top-size: var(--h2); font-size: var(--h2); }
.site-title.site-title--size-display { --site-title-size: var(--h1); --lockup-top-size: var(--h1); font-size: var(--h1); }

/* GROUNDS: the same three variations a block's `bg=` offers, on the header
   band and the strip above it. The ink is written out on the name and the
   menu, not only through tokens: a site that colours its menu links directly
   would otherwise leave dark words on the dark ground. */
.site-header[data-bg="mist"] { --hdr-ground: color-mix(in srgb, var(--color-text) 5%, var(--color-bg)); --hdr-ink: var(--color-primary); }
.site-header[data-bg="sand"] { --hdr-ground: color-mix(in srgb, var(--color-accent) 38%, var(--color-bg)); --hdr-ink: var(--color-primary); }
.site-header[data-bg="navy"] { --hdr-ground: var(--color-primary); --hdr-ink: var(--color-bg); }
.site-header[data-bg] {
  --header-bg: var(--hdr-ground); --header-text: var(--hdr-ink); --nav-color: var(--hdr-ink);
  --nav-hover-color: var(--hdr-ink); --nav-menu-bg: var(--hdr-ground);
  --nav-hover-bg: color-mix(in srgb, var(--hdr-ink) 12%, transparent);
  background: var(--hdr-ground);
}
.site-header[data-bg] :is(.site-title, .site-title-lock__top, .site-title-lock__bottom,
  .nav-top a, .nav-top .nav-parent, .nav-mega-label, .nav-mobile-toggle, .nav-slideout-toggle) {
  color: var(--hdr-ink);
}
.header-utility[data-bg="mist"] { background: color-mix(in srgb, var(--color-text) 5%, var(--color-bg)); color: var(--color-primary); }
.header-utility[data-bg="sand"] { background: color-mix(in srgb, var(--color-accent) 38%, var(--color-bg)); color: var(--color-primary); }
.header-utility[data-bg="navy"] { background: var(--color-primary); color: var(--color-bg); }
.header-utility[data-bg] .header-utility-link { color: inherit; }
/* The header's button on the dark ground. The site's button IS the primary,
   which is that ground — so it inverts, as a Section's button does there. */
.site-header[data-bg="navy"] .site-header-action.button:not(.btn-accent):not(.btn-ghost) {
  background: var(--color-bg); color: var(--color-primary);
}
.site-header[data-bg="navy"] .site-header-action.btn-ghost {
  border-color: currentColor; color: var(--color-bg);
}

/* Main column */
/* NO PAGE PADDING ABOVE THE FOOTER (Cam, 2026-09-08, having asked four
   times — the first three times I measured the wrong thing and then argued
   the number was normal, which it was not).

   Measured ink to ink on the home page: 226px of white between the last word
   of the page and the first word of the footer. `3rem` of that was main's own
   bottom padding, and on a page whose last block is DRESSED it is the worst
   48px on the page: a pine band ends, 48px of pale paper, then the pine
   footer. A stripe of ground between two coloured bands reads as a mistake
   because it is one.

   Blocks own their trailing space — a dressed .blk carries 84px of its own,
   and an undressed one carries none by design — and the footer owns its top
   padding. The page does not need a third opinion between them. Box edges are
   invisible: the 48px was never seen as padding, only as a stripe. */
main {
  max-width: var(--shell-width); margin: 0 auto;
  padding: 1.5rem var(--gutter) 0;
}

/* AND THE LAST BLOCK DOES NOT PAY ITS RHYTHM TWICE. A block's bottom padding
   is the space between it and the NEXT block. When it is the last one there is
   no next block, only the footer, which brings its own top padding — so the
   two stack: 80px + 57.6px of nothing on an ordinary page, measured on
   /about/, which is the rest of what Cam kept seeing after main's own 3rem
   came off.

   ONLY WHEN THE BLOCK HAS NO GROUND. A dressed block's padding is the SHAPE OF
   ITS BAND, not rhythm — remove it and the colour crops tight against the last
   line. The home page ends on a pine band and must keep every pixel of its 84.
   `:where()` so this stays at one class and a site can still say otherwise.

   TWO SELECTORS BECAUSE THE LAST CHILD COMES IN TWO SHAPES. On the home page
   it is a .blk wrapping a section; on an ordinary page the section is the
   direct child of .page-body and there is no .blk at all. My first version
   only wrote the .blk form, matched nothing on /about/, and the measurement
   said 174px before and 174px after — a rule that changed a number nowhere.
   The second version was worse: I opened no comment before this paragraph, so
   the prose itself became a stray selector and the parser dropped BOTH rules.
   Same reading either way, 174px, and nothing on the page to say why. */
/* MARGIN GOES WHATEVER THE LAST CHILD IS, padding only when it is undressed.
   The third reading found the rest of it: these pages do not end on padding at
   all. /news/ ends on a `ul.collection-grid` with 30px of MARGIN and
   /membership/ on a `.p-columns` with 84px — margin below the last thing on
   the page is page ground before the footer, which is the stripe, whether the
   block is dressed or not. Padding is different: on a dressed block it is the
   band's own shape and it stays.

   QUALIFIED WITH `main` FOR ONE MORE POINT OF SPECIFICITY, and that is the
   fourth reading. `.page-body > .p-columns { margin-block: … clamp(3rem,…) }`
   is (0,2,0) and lands AFTER core.css, so an equally weighted rule of mine
   lost silently: the CSSOM said margin-bottom: 0 was the only matching rule,
   because the winner writes the LOGICAL property `margin-block` and my scan
   was only looking for `margin-bottom`. A rule can be beaten by a name for
   the same thing. `main` costs an element and settles it. */
main .page-body > :last-child,
/* …and the pages that have no .page-body at all. /contact/ ends on a form
   whose last paragraph carries 15px, straight into the footer. */
main .page-full > :last-child { margin-bottom: 0; }
main .page-body > :last-child:where(:not([data-bg]), [data-bg="paper"]) {
  padding-bottom: 0;
}
main .page-body > .blk:last-child:where(:not([data-bg]), [data-bg="paper"]) > * {
  padding-bottom: 0;
}
/* ── The reading column (rewritten 2026-08-22) ────────────────────────────
   This used to be one line — `.page { max-width: var(--text-width) }` — and
   that line was the bug. It reads as "text is narrow" and it means "the whole
   content area is narrow", so a gallery, a slider, a markdown table and a
   pasted HTML block all inherited a 736px cap they had no reason to want.

   The narrowing now sits on RUNNING TEXT and nothing else. Two rules, and the
   direction of each is deliberate:

   FURNITURE (direct children of the article) is narrow BY DEFAULT and widened
   by name. There are four of them and they change about once a year; a new one
   is almost always more words, and words belong in the column.

   CONTENT (children of the page body) is wide by default and narrowed by name.
   That is the opposite way round on purpose: a block invented next year should
   arrive full width without anybody remembering a list, which is exactly what
   Eric asked for — "everything else other than the text should go full width".

   `.page-full` needs no rule at all now. It is the article class the
   Full-width page layout uses, and since none of the selectors below name it,
   its text is full width too — which is the second case Eric described,
   "there may be cases where we want the text to go full width".            */
/* A pasted block is the one thing on a page nobody here wrote, and the one
   shape it reliably arrives in is a table with hard column widths. Eric's
   pricing table came out 1056px wide and shoved a 390px phone sideways —
   which is the page's fault, not the table's: wide content belongs in its own
   scroller, never in the document's. Same rule the pattern workshop follows.
   Scoped to this block because it is the only one whose insides are outside
   our control; a scroll box round every block would clip things that are
   meant to hang out of one. */
.block-html { overflow-x: auto; }

.page > * { max-width: var(--reading-width); margin-inline: var(--reading-lead) auto; }
/* The three that are pictures or containers by design. `.page-body` is the
   container the content rules below work inside, so it has to be full width
   for them to have anything to narrow against. */
/* margin-inline: 0 IS NOT REDUNDANT. It used to be, because the rule above
   said `auto` and auto margins on an element with no max-width resolve to
   zero. The moment that became --reading-lead it stopped being zero, and
   these three would have been shoved right by the same amount as the text
   while still measuring full width — off the right edge of the page. Say it. */
.page > .page-body,
.page > .page-hero,
.page > .page-header-editorial { max-width: none; margin-inline: 0; }

/* Running text. An h1 lands here on a page whose title is drawn in the body
   rather than by the header component, and headings have to travel with the
   paragraphs under them: a full-width h2 over a narrow paragraph does not read
   as a wide heading, it reads as a mistake.

   THE TWO GUARDS ARE NOT DECORATION. A tag is not a good enough answer to
   "is this running text", because a block is free to be built out of one:
   the gallery renders as a <ul>, and without `:not([data-block])` every
   gallery on the pilot site came out at 736 while the feature and finder
   blocks beside them took the full 1152 — found on Tuckerton's Publications
   and Visit pages, and it looks exactly like a bug in the gallery. The quote
   PATTERN is a <blockquote> and sets its own `margin-inline: 0`, which the
   `auto` below would have quietly overridden.

   Written as a skip rather than as a wider rule with an override after it, so
   a block's own stylesheet is never in a specificity argument with this one. */
/* THE THIRD GUARD, added 2026-09-02, and it is the same lesson as the second
   one arriving a second time. A collection grid (types.js) and a taxonomy term
   index (taxonomies.js) are both <ul>, neither is a block, and both were being
   narrowed to the reading column and centred — so on /events/ the search bar
   ran the full 1152 and the event cards sat in a 736 column starting 208px
   further right. Eric, 2026-09-02, looking at it: "the content is almost in
   the middle of the page rather than lining up on the left margin".

   The mark is on the GENERATOR, not a class name here, so a listing invented
   next year is furniture without anybody editing this selector — which is the
   whole point of writing these as skips. */
.page > .page-body
  > :is(p, h1, h2, h3, h4, h5, h6, ul, ol, dl, blockquote, pre):not([data-block]):not([data-pattern]):not([data-listing]) {
  max-width: var(--reading-width);
  /* Inline only. The vertical rhythm is set by --space-block on these same
     elements and a shorthand here would flatten every gap on every page. */
  margin-inline: var(--reading-lead) auto;
}

/* A picture set to sit BESIDE the text is the one thing this change cannot
   leave alone. A float positions against its containing block, which is now
   the full content width, while the paragraph beside it is narrow and centred
   — so the picture would sit out in the left margin with the text starting
   somewhere to its right and never wrapping round it. Pulling the float in to
   the column's own edge puts it back where it was, and the cap keeps a
   "40% of the page" figure from eating two thirds of a narrower column. */
.page > .page-body > .fig.align-left,
.page > .page-body > .fig.align-right {
  max-width: calc(var(--reading-width) * 0.45);
}
/* These two were `/ 2` — the leftover split evenly, i.e. a centred column
   assumed. They now read the same two derived values the text does, so a site
   that moves its reading column to the left margin takes its side-by-side
   pictures with it instead of leaving them behind at the old centre. */
.page > .page-body > .fig.align-left  { margin-left:  var(--reading-lead); }
.page > .page-body > .fig.align-right { margin-right: var(--reading-trail); }
@media (max-width: 40rem) {
  /* Below this the figure is a block again (see the rule further down), so the
     inset would be a stray indent on a phone. */
  .page > .page-body > .fig.align-left,
  .page > .page-body > .fig.align-right { margin-left: auto; margin-right: auto; max-width: 100%; }
}
.page-hero { border-radius: var(--radius); margin: .5rem 0 1rem; }
/* Why somebody landed on the search page rather than the address they typed.
   A retired page forwards here carrying its own title as the query (see
   sitemap.delete_page); this is the line that says so, instead of leaving them
   in front of a search box that filled itself in for no visible reason. */
.search-why {
  background: var(--surface-muted, rgba(0, 0, 0, .04));
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  padding: .75rem 1rem;
  margin: 0 0 1.25rem;
}
.home-tagline {
  font-size: var(--tagline-size);
  color: var(--tagline-color);
  font-family: var(--font-heading);
}

/* Menus speak in --nav-font, whichever nav style the site uses. Set on the
   containers rather than on each link, so a control added later inherits it
   without a matching rule; .nav-parent is a <button> and already takes
   `font: inherit`, which resolves to this. */
.nav-top, .nav-side, .nav-slideout, .nav-slideout-open {
  font-family: var(--nav-font);
}

/* ── Nav: top (default) ─────────────────────────────────────────── */
.nav-top-list {
  list-style: none; margin: 0; padding: 0;
  display: flex; gap: .25rem; flex-wrap: wrap; align-items: center;
}
.nav-top .nav-item { position: relative; display: flex; align-items: center; }
.nav-top a {
  color: var(--nav-color); text-decoration: none;
  padding: var(--nav-pad-y) var(--nav-pad-x);
  border-radius: var(--nav-radius); display: block;
  font-size: var(--nav-size); font-weight: var(--nav-weight);
  text-transform: var(--nav-transform); letter-spacing: var(--nav-tracking);
}
.nav-top a:hover, .nav-top a[aria-current="page"] {
  background: var(--nav-hover-bg);
  color: var(--nav-hover-color);
}
.nav-parent {
  background: none; border: 0; color: var(--nav-color);
  cursor: pointer;
  padding: var(--nav-pad-y) var(--nav-pad-x);
  border-radius: var(--nav-radius); font: inherit;
  font-size: var(--nav-size); font-weight: var(--nav-weight);
  text-transform: var(--nav-transform); letter-spacing: var(--nav-tracking);
}
.nav-parent:hover, .nav-item.open > .nav-parent {
  background: var(--nav-hover-bg);
}
/* WHERE AM I — house standard since 2026-08-07 (Eric). The section you are
   inside stays lit however deep you go, so a cemetery page three levels down
   still says "Research". Deliberately QUIETER than the exact-page state above:
   a section marker that shouts as loudly as the current page makes two things
   look equally current. An underline in the accent colour reads as "this
   branch" without competing, and it survives every one of the 56 design
   tokens because it borrows the nav's own colour rather than inventing one. */
.nav-top .nav-item.in-section > a,
.nav-top .nav-item.in-section > .nav-parent {
  box-shadow: inset 0 -2px 0 0 currentColor;
}
/* The dropdown's copy of the same mark, separated out so it can be turned off
   on its own. With a dropdown open, marking both the section AND the item
   inside it puts two underlines on screen saying one thing - and the section's
   is the one that still means something once the panel shuts. */
.nav-top .submenu a.in-section { box-shadow: var(--nav-sub-marker); }
.nav-side a.in-section, .nav-slideout a.in-section,
.nav-side .nav-group-label.in-section, .nav-slideout .nav-group-label.in-section {
  box-shadow: inset 2px 0 0 0 currentColor;
}
/* The exact page still wins: it has the filled background, and it should not
   ALSO carry the section rule or it reads as two states at once. */
.nav-top a[aria-current="page"], .nav-side a[aria-current="page"],
.nav-slideout a[aria-current="page"] { box-shadow: none; }
.nav-group-label { display: block; padding: .35rem .5rem; font-weight: 700; }
.nav-slideout .nav-group-label { color: #f2e9d8; color: rgba(255,255,255,.85); }
.nav-top .submenu {
  list-style: none; margin: 0; padding: .35rem;
  position: absolute; top: 100%; left: 0; min-width: 13rem; z-index: 50;
  background: var(--nav-menu-bg);
  border-radius: var(--nav-menu-radius);
  box-shadow: 0 6px 18px rgba(0,0,0,.25);
  display: none;
}
/* Subordinate to the section it hangs off, not a second row of main menu. The
   mobile accordion had always done this and the desktop dropdown never did. */
.nav-top .submenu a {
  font-size: var(--nav-sub-size);
  /* Beats the --nav-transform set on .nav-top a above (three classes to one),
     so capitalising the main menu leaves its contents in ordinary case unless
     the site asks for both. */
  text-transform: var(--nav-sub-transform);
  letter-spacing: var(--nav-sub-tracking);
  padding: var(--nav-sub-pad);
}
.nav-top .nav-item.open > .submenu { display: block; }
/* Hover + keyboard reveal (click still works for touch) */
@media (min-width: 761px) {
  .nav-top .nav-item.has-children:not(.dismissed):hover > .submenu,
  .nav-top .nav-item.has-children:not(.dismissed):focus-within > .submenu { display: block; }
}
.nav-mobile-toggle, .nav-slideout-open {
  display: none; background: none; border: 1px solid rgba(255,255,255,.5);
  color: #fff; border-radius: 4px; padding: .45rem .8rem; cursor: pointer; font-size: 1rem;
}
.nav-mobile-toggle-icon::before { content: "☰ "; }
/* 44px ON A TOUCH SCREEN. Measured 2026-09-08 at 375: the Menu button was
   85x37, and on a phone this is the ONLY way into the navigation — the one
   control on the page least able to afford a near miss. Padding sets the
   height at every other width, so this raises the floor rather than
   restyling the button. */
@media (pointer: coarse) {
  /* HEIGHT ONLY, NEVER `display`. The base rule hides this button and the
     760px query is what shows it; setting a display here would put a Menu
     button on every touch LAPTOP, at a width where the full menu is already
     on screen. Raising the floor cannot move the bug somewhere else. */
  .nav-mobile-toggle, .nav-slideout-open { min-height: 44px; padding-block: .7rem; }
}

@media (max-width: 760px) {
  .nav-mobile-toggle { display: inline-block; }
  .nav-top-list { display: none; flex-direction: column; align-items: stretch; width: 100%; padding-top: .5rem; }
  .nav-top-list.open { display: flex; }
  /* ONE ROW on a phone: the site name on the left, "Menu" on the right, the
     list opening below them full-width. `.nav-top` used to take the whole
     row, so the toggle sat on a line of its own under the name and the
     header was three bands deep before the hero (A-15, measured 2026-09-07:
     510 of 812px). Now the nav is a flex row that wraps, the toggle rides on
     the name's row and the list wraps under both. Two selectors deep so a
     site's own `nav.nav-top{margin-left:auto}` does not win the width back. */
  .site-header-inner > .nav-top { flex: 1 1 auto; display: flex; flex-wrap: wrap; justify-content: flex-end; }
  .site-header-inner { row-gap: 0; }
  /* The utility strip is a desktop convenience — address, phone, sign-in. On
     a phone the address wraps to three lines and the sign-in link goes to a
     fourth. Keep the links (a sign-in is a way in), drop the text. */
  .header-utility-text { display: none; }
  .header-utility-inner { justify-content: flex-end; }
  /* The masthead logo at its desktop width is the second-biggest band on the
     page. Height-capped here and the width follows; site rules set width,
     so `max-height` plus `width: auto` wins without an argument. */
  .site-header .site-logo { max-height: 3rem; width: auto; max-width: 60vw; }
  .site-header .site-header-inner { padding-top: .6rem; padding-bottom: .6rem; }
  /* Mobile = accordion: only top-level items show; tapping a section reveals
     its children BELOW it, indented and visually subordinate. */
  .nav-top .nav-item { display: block; }
  .nav-top-list > .nav-item > a, .nav-parent { width: 100%; text-align: left; font-weight: 700; }
  .nav-top .submenu { position: static; display: none; box-shadow: none; border-radius: 0; }
  .nav-top .nav-item.open > .submenu {
    display: block; margin: 0 0 .4rem .9rem; padding-left: .4rem;
    border-left: 2px solid rgba(255,255,255,.35);
  }
  .nav-top .submenu a { font-size: var(--nav-sub-size); padding: .45rem .7rem; opacity: .92; font-weight: 400; }
}

/* ── Nav: side ──────────────────────────────────────────────────── */
.layout-side {
  max-width: var(--shell-width); margin: 0 auto;
  display: flex; gap: 2rem; padding: 0 var(--gutter);
}
.layout-side main { margin: 0; padding: 1.5rem 0 0; flex: 1; min-width: 0; }
.nav-side { width: 15rem; flex-shrink: 0; padding-top: 1.5rem; }
.nav-side ul { list-style: none; margin: 0; padding: 0; }
.nav-side a {
  display: block; padding: .35rem .5rem; text-decoration: none;
  border-radius: var(--nav-radius);
  font-size: var(--nav-size); font-weight: var(--nav-weight);
  text-transform: var(--nav-transform); letter-spacing: var(--nav-tracking);
}
.nav-side a:hover, .nav-side a[aria-current="page"] { background: color-mix(in srgb, var(--color-primary) 12%, transparent); }
.nav-side-sublist { padding-left: 1rem !important; font-size: .95rem; }
@media (max-width: 860px) {
  .nav-side { display: none; }
  body.nav-style-side .nav-slideout-open { display: inline-block; }
  .layout-side { display: block; }
  .layout-side main { padding: 1.5rem 0 0; }
}
@media (min-width: 861px) {
  body.nav-style-side .nav-slideout-open { display: none; }
}

/* ── Nav: slideout drawer ───────────────────────────────────────── */
body.nav-style-slideout .nav-slideout-open { display: inline-block; }

/* A desk that lives on another host says so while it is loading, and stops
   saying it the moment the desk announces itself. Neutral rather than alarming
   on purpose: at the moment it is drawn, nothing has gone wrong — it is only
   the honest description of a frame that may stay empty. */
.desk-note {
  margin: 0 0 1rem; padding: .75rem 1rem;
  background: var(--card-bg); border-left: 0;
  font-size: .92rem; line-height: 1.5;
  color: color-mix(in srgb, var(--color-text) 78%, transparent);
}

/* ── MEGA NAV (body.nav-style-mega) ──────────────────────────────────────
   Every rule here is scoped to the mega style, so the `top` style's stylesheet
   is untouched. There is deliberately NO :hover or :focus-within rule that
   reveals a panel — the panel is opened and closed by nav.js alone. Hover
   turns the chevron and darkens the label; that is the whole affordance. */
body.nav-style-mega .nav-mega { margin-left: auto; display: flex; align-items: center; gap: 2rem; }
body.nav-style-mega .nav-mega-list {
  list-style: none; margin: 0; padding: 0; display: flex; align-items: center; gap: 2rem;
}
body.nav-style-mega .nav-mega-item { position: static; }
body.nav-style-mega .nav-mega-label {
  font: inherit; font-size: var(--nav-size); font-weight: var(--nav-weight);
  color: var(--nav-color); background: none; border: 0; padding: .4rem 0;
  cursor: pointer; display: inline-flex; align-items: center; gap: .4rem; text-decoration: none;
  position: relative;
}
body.nav-style-mega .nav-mega-chev {
  width: 9px; height: 9px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translateY(-2px); opacity: .55; transition: transform .15s ease, opacity .15s ease;
}
/* HOVER CHANGES THE LABEL, NOT THE PAGE (Cam, 2026-09-07, on v1: "whenever I
   hover over it, it underlines the entire thing"). Colour and the chevron,
   nothing else — no underline, and deliberately no rule anywhere in this file
   that REVEALS a panel on :hover or :focus-within. The panel is opened and
   closed by nav.js alone, which is what keeps Escape simple: in the `top`
   style a CSS hover reveal is exactly why Escape needed a `dismissed` class to
   stop focus returning to the button from re-opening what it had just shut. */
body.nav-style-mega .nav-mega-label { text-decoration: none; }
body.nav-style-mega .nav-mega-label:hover { color: var(--nav-hover-color); text-decoration: none; }
body.nav-style-mega .nav-mega-label[aria-expanded="true"]::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -.55rem;
  height: 2px; background: var(--color-primary);
}
body.nav-style-mega .nav-mega-label:hover .nav-mega-chev { opacity: 1; }
body.nav-style-mega .nav-mega-label[aria-expanded="true"] .nav-mega-chev { transform: rotate(225deg) translateY(-2px); opacity: 1; }
body.nav-style-mega .nav-mega-item.in-section > .nav-mega-label,
body.nav-style-mega .nav-mega-label[aria-current="page"] { color: var(--nav-hover-color); }

/* The panel: full width of the header's container, a layer over the page. */
/* A panel is a LAYER only when script is driving it (nav-mega.njk adds the
   class during parse). Without JavaScript these are ordinary blocks in the
   flow and the menu reads as a long list — every address reachable, nothing
   overlapping. Rendered "open" but still absolute, all four sat on top of one
   another at the same offset and only the last was readable. */
body.nav-style-mega .nav-mega-panel {
  background: var(--nav-menu-bg); border-top: 1px solid var(--card-border);
  border-bottom: 1px solid var(--card-border);
}
/* ANCHORED UNDER ITS LABEL, NOT ACROSS THE WINDOW (Cam: "it's full width …
   you kinda just made a very simple mega menu"). A panel that spans the
   viewport has to be full-bleed whatever it holds; one that is sized to its
   own content can be three columns or one, and reads as belonging to the
   label it came from. Right-shifted back into the window by the rule below if
   a late label would push it off the edge. */
body.nav-style-mega .nav-mega--js .nav-mega-panel {
  position: absolute; top: 100%; left: 0; right: auto; z-index: 40;
  width: max-content; max-width: min(58rem, calc(100vw - 2 * var(--gutter)));
  border-radius: 16px; border: 1px solid var(--card-border);
  box-shadow: 0 28px 50px -30px rgba(0,0,0,.34);
  margin-top: .55rem;
}
/* A WIDE PANEL IS ANCHORED TO THE MENU, A DROPDOWN TO ITS LABEL.
   Anchoring a 900px panel under its own label puts it off the right of the
   window for every label except the first — measured: "For clerks" opened at
   x=503 and ran to 1431 in a 1180 viewport. A dropdown is narrow enough to sit
   under its label and belongs there; a panel is wide enough that it has to
   line up with something, and the menu's right edge is the one edge every
   panel can share without a single measurement in JavaScript. The open
   label's accent bar is what ties the panel back to the label. */
body.nav-style-mega .nav-mega { position: relative; }
body.nav-style-mega .nav-mega--js .nav-mega-panel { left: auto; right: 0; }
body.nav-style-mega .nav-mega--js .nav-mega-item--menu { position: relative; }
body.nav-style-mega .nav-mega--js .nav-mega-item--menu .nav-mega-panel {
  left: 0; right: auto;
}
/* THE LAST ITEM'S DROPDOWN OPENS LEFTWARD. A dropdown hangs from its label's
   left edge, which is right until the label is the last one in a right-aligned
   menu: measured 2026-09-08, "About & news" opened at x=1204 and ran to 1444
   in a 1440 window — four pixels of it off the screen. Anchoring the last
   one to its own right edge cannot overflow, because the label is already
   inside. Only the last, deliberately: right-anchoring every dropdown would
   push the FIRST one off the left of a left-aligned menu, which is the same
   bug facing the other way. */
body.nav-style-mega .nav-mega--js .nav-mega-list > li:last-child .nav-mega-panel--menu {
  left: auto; right: 0;
}
/* The compact form: a section whose depth is a handful of pages does not need
   columns, a blurb per link or a featured card. */
body.nav-style-mega .nav-mega-panel--menu .nav-mega-panel-inner {
  display: block; padding: .6rem; min-width: 15rem;
}
body.nav-style-mega .nav-mega-panel--menu .nav-mega-col ul { gap: 0; }
body.nav-style-mega .nav-mega-panel--menu .nav-mega-col a {
  display: block; padding: .55rem .8rem; border-radius: 8px; font-size: .96rem;
}
body.nav-style-mega .nav-mega-panel--menu .nav-mega-col a:hover {
  background: var(--nav-menu-bg); text-decoration: none;
}
body.nav-style-mega .nav-mega-panel--menu .nav-mega-sub { display: none; }
/* A panel that declares no featured card must not reserve its track (V-01:
   the live Membership panel kept a 19rem column of nothing). Written as a
   class from the template rather than :has(), so it holds without it. */
body.nav-style-mega .nav-mega-panel--nofeat .nav-mega-panel-inner {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
body.nav-style-mega .nav-mega-list { flex-wrap: wrap; }
body.nav-style-mega .nav-mega-panel-inner {
  padding: 2rem 2.25rem 2.25rem;
  display: grid; grid-template-columns: repeat(3, minmax(0,1fr)) 17rem; gap: 2.5rem;
}
/* AS MANY TRACKS AS THERE ARE COLUMNS, and no more. Three was hard-coded, so a
   panel that declared two got two narrow columns and an empty third track
   before the featured card — measured 2026-09-08 on CAMRO at 1440: columns
   155px each with 155px of nothing beside them, which is what "thin and
   crushed to the left" was. The count comes from the template, which is the
   only place that knows it. These sit after the generic rule and after
   --nofeat, and each carries the same weight as the rule it replaces. */
body.nav-style-mega .nav-mega-panel-inner--c1 { grid-template-columns: minmax(0,1fr) 17rem; }
body.nav-style-mega .nav-mega-panel-inner--c2 { grid-template-columns: repeat(2, minmax(0,1fr)) 17rem; }
body.nav-style-mega .nav-mega-panel-inner--c3 { grid-template-columns: repeat(3, minmax(0,1fr)) 17rem; }
body.nav-style-mega .nav-mega-panel-inner--c4 { grid-template-columns: repeat(4, minmax(0,1fr)) 17rem; }
body.nav-style-mega .nav-mega-panel--nofeat .nav-mega-panel-inner--c1 { grid-template-columns: minmax(0,1fr); }
body.nav-style-mega .nav-mega-panel--nofeat .nav-mega-panel-inner--c2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
body.nav-style-mega .nav-mega-panel--nofeat .nav-mega-panel-inner--c3 { grid-template-columns: repeat(3, minmax(0,1fr)); }
body.nav-style-mega .nav-mega-panel--nofeat .nav-mega-panel-inner--c4 { grid-template-columns: repeat(4, minmax(0,1fr)); }
/* With fewer tracks the panel no longer NEEDS its old width, and max-content
   would let it shrink to the longest link — a two-column panel collapsing to
   something narrower than the three-column one it replaced. A floor keeps the
   columns worth reading; the cap above still stops it leaving the window. */
body.nav-style-mega .nav-mega--js .nav-mega-panel:not(.nav-mega-panel--menu) {
  min-width: min(46rem, calc(100vw - 2 * var(--gutter)));
}
/* NOT THE ACCENT TOKEN. This heading is 11.5px tracked capitals, and
   `--color-accent` is a site's DISPLAY colour — the one chosen to be seen as a
   fill, on a button or a rule. CAMRO's next palette makes that #F0567A, which
   is 3.1:1 on the page ground and fails at any size, on 216 of these headings;
   the site can patch its own copy, but the engine would hand the same failure
   to the next client with a light accent. So the engine paints small text with
   the heading colour, and a site that wants an accent here opts in with its own
   rule and its own contrast. */
body.nav-style-mega .nav-mega-col-title {
  font-size: .72rem; font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
  color: var(--heading-color); margin: 0 0 .35rem;
}
body.nav-style-mega .nav-mega-col-note { margin: 0 0 1.25rem; font-size: .92rem; line-height: 1.45; }
body.nav-style-mega .nav-mega-col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .9rem; }
body.nav-style-mega .nav-mega-col a {
  font-size: 1.02rem; font-weight: 600; text-decoration: none; display: block;
}
body.nav-style-mega .nav-mega-col a:hover { text-decoration: underline; text-underline-offset: .16em; }
body.nav-style-mega .nav-mega-sub {
  display: block; font-size: .86rem; font-weight: 400; margin-top: .1rem;
  color: color-mix(in srgb, var(--color-text) 68%, transparent);
}
/* The card is as tall as it needs to be. A grid item stretches to its row by
   default, so a four-line card beside a nine-link column became a block of
   empty surface as tall as the panel. */
body.nav-style-mega .nav-mega-feat { padding: 1.75rem; background: var(--card-bg); align-self: start; }
body.nav-style-mega .nav-mega-feat-title {
  display: block; font-size: 1.35rem; font-weight: 600; text-decoration: none; margin: .2rem 0 .5rem;
}
body.nav-style-mega .nav-mega-feat p { margin: 0 0 1.1rem; font-size: .92rem; line-height: 1.5; }
body.nav-style-mega .nav-mega-feat-go { font-weight: 600; font-size: .95rem; text-decoration: none; }
body.nav-style-mega .nav-mega-feat-go::after { content: "\2192"; margin-left: .4rem; }

/* Search and member sign-in: the only two things the chrome owes a visitor. */
body.nav-style-mega .nav-mega-util {
  display: flex; align-items: center; gap: 1.5rem;
  padding-left: 1.75rem; border-left: 1px solid var(--card-border);
}
body.nav-style-mega .nav-mega-util-link { font-size: .9rem; font-weight: 600; text-decoration: none; }
body.nav-style-mega .nav-mega-util-link:hover { text-decoration: underline; }

/* Phone: the same four panels ARE the accordion. */
@media (max-width: 760px) {
  body.nav-style-mega .site-header-inner { display: flex; flex-wrap: wrap; align-items: center; }
  body.nav-style-mega .nav-mega { display: contents; }
  body.nav-style-mega .nav-mobile-toggle { display: inline-block; margin-left: auto; }
  body.nav-style-mega .nav-mega-list { display: none; flex-direction: column; align-items: stretch; gap: 0; flex-basis: 100%; width: 100%; }
  body.nav-style-mega .nav-mega-list.open { display: flex; }
  body.nav-style-mega .nav-mega-item { border-top: 1px solid var(--card-border); }
  body.nav-style-mega .nav-mega-label { width: 100%; justify-content: space-between; padding: 1.05rem 0; font-size: 1.05rem; font-weight: 600; }
  body.nav-style-mega .nav-mega-panel { position: static; border: 0; box-shadow: none; background: var(--card-bg); }
  body.nav-style-mega .nav-mega-panel-inner { display: block; padding: .5rem 1rem 1.4rem; }
  body.nav-style-mega .nav-mega-col { padding-top: 1rem; }
  body.nav-style-mega .nav-mega-feat { margin-top: 1.2rem; background: var(--nav-menu-bg); }
  body.nav-style-mega .nav-mega-util { flex-basis: 100%; width: 100%; padding: 1.1rem 0 0; border-left: 0; }
}
.nav-slideout {
  position: fixed; top: 0; left: 0; bottom: 0; width: min(20rem, 85vw); z-index: 60;
  background: var(--nav-menu-bg);
  color: var(--nav-color);
  padding: 1.25rem; overflow-y: auto;
  box-shadow: 4px 0 24px rgba(0,0,0,.35);
}
.nav-slideout ul { list-style: none; margin: 0; padding: 0; }
.nav-slideout a {
  color: var(--nav-color); text-decoration: none;
  display: block; padding: .45rem .5rem; border-radius: var(--nav-radius);
  font-size: var(--nav-size); font-weight: var(--nav-weight);
  text-transform: var(--nav-transform); letter-spacing: var(--nav-tracking);
}
.nav-slideout a:hover, .nav-slideout a[aria-current="page"] {
  background: var(--nav-hover-bg);
  color: var(--nav-hover-color);
}
.nav-slideout-sublist { padding-left: 1rem !important; font-size: .95rem; }

/* ── Breadcrumb ─────────────────────────────────────────────────── */
.breadcrumb ol {
  list-style: none; margin: 0 0 1rem; padding: 0; display: flex; flex-wrap: wrap;
  gap: .35rem; font-size: var(--breadcrumb-size);
}
.breadcrumb li + li::before { content: "›"; margin-right: .35rem; color: var(--color-accent); }

/* ── Record lists: one grid for every content type ──────────────────
   Events, museum objects, publications, news and anything a site defines for
   itself all draw through here (platform/types.js). It used to be four: a card
   grid, an events list, an object grid, and the collection block's own — so
   "make the cards a bit tighter" was four edits, and in practice was one, and
   the four drifted. `.citem` is a record card; the layout modifier on the list
   decides whether the cards sit in a grid, stack as rows, or run wide.

   These rules live in core.css rather than in the collection block's block.css
   because they are no longer the block's: every generated listing page uses
   them whether or not that block appears on the page. */

/* @root .collection-grid, .collection-empty, .collection-missing for collection
   — the collection block's root rule lives HERE, not in its own block.css, so
   `python tools/design.py` has to be told where to look or it cannot see it.
   That blind spot is not hypothetical: this rule read `margin: 1.5rem 0` until
   2026-08-07, so the site-wide gap setting and the page's tight/roomy dial did
   nothing to the collection block or to any generated record listing, and the
   check that exists to catch exactly that reported all clear. */
.collection-grid {
  list-style: none; margin-block: var(--space-block); margin-inline: 0; padding: 0;
  display: grid; gap: var(--grid-gap);
  grid-template-columns: repeat(auto-fill, minmax(var(--card-min), 1fr));
}
/* A type that states how many across gets exactly that, at three widths. Same
   spelling as the gallery block's `columns` — one number, or "6/3/2". */
.collection-grid[data-cols] { grid-template-columns: repeat(var(--cols-wide), minmax(0, 1fr)); }
@media (max-width: 60rem) {
  .collection-grid[data-cols] { grid-template-columns: repeat(var(--cols-tablet), minmax(0, 1fr)); }
}
@media (max-width: 34rem) {
  .collection-grid[data-cols] { grid-template-columns: repeat(var(--cols-phone), minmax(0, 1fr)); }
}

.citem { min-width: 0; }
.citem-img {
  width: 100%; height: auto; aspect-ratio: 5 / 3; object-fit: cover;
  object-position: var(--focus, 50% 35%);
  border-radius: var(--card-radius); display: block; margin-bottom: .5rem;
  background: color-mix(in srgb, var(--color-text) 4%, transparent);
}
.citem-title { font-weight: 700; line-height: 1.3; display: block; }
.citem-link { display: block; text-decoration: none; color: inherit; }
.citem-link .citem-title { text-decoration: underline; text-underline-offset: .12em; }
.citem-link:hover .citem-title { color: var(--link-hover-color); }
.citem-link:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 3px; }
.citem-date, .citem-meta {
  color: color-mix(in srgb, var(--color-text) 65%, transparent);
  margin: .2rem 0 0; font-size: .9rem;
}
.citem-summary { margin: .35rem 0 0; font-size: .92rem; line-height: 1.5; }
/* "Past event" beside the date on a dated card whose day has gone (types.js
   grid). A word, not a badge: it qualifies the date, it does not decorate it. */
.citem-past { font-size: .8em; text-transform: uppercase; letter-spacing: .06em; margin-left: .5em; }

/* Cards proper: the panel chrome the design settings control. Only this layout
   takes it — a list row is not a card, and giving it a border would be reading
   "Panel background" as "put a box round everything". */
.collection-cards .citem {
  background: var(--card-bg);
  border: var(--card-border);
  border-radius: var(--card-radius);
  padding: var(--card-pad);
}

/* A list rather than a grid: where the title is the thing and a picture would
   be decoration — annual reports, minutes, publications. */
.collection-list { grid-template-columns: minmax(0, 1fr); gap: 0; }
.collection-list .citem {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .1rem .75rem;
  padding: .7rem 0; border-bottom: 1px solid color-mix(in srgb, var(--color-text) 12%, transparent);
  align-items: baseline;
}
.collection-list .citem:first-child { border-top: 1px solid color-mix(in srgb, var(--color-text) 12%, transparent); }
.collection-list .citem-img { display: none; }
.collection-list .citem-date { grid-column: 2; grid-row: 1; margin: 0; white-space: nowrap; }
.collection-list .citem-summary { grid-column: 1 / -1; }

/* Rows WITH a thumbnail: the shape the events list has always had, now
   available to news, exhibitions or anything else that wants it.

   THE PICTURE IS TAKEN OUT OF THE FLOW ON PURPOSE (2026-08-07). The obvious
   spelling — a two-column grid on `.citem-link` — only ever put the TITLE beside
   the picture, because the date, the place and the summary are siblings of the
   link, not children of it. Everything below the title therefore started under
   the picture, leaving a column of white space to its right and breaking the row
   into two unrelated halves. Eric, on the Tuckerton events list: "the title
   aligns right to the image, but then the dates and the location and the text is
   below the picture."

   The link has to keep wrapping the picture AND the title (one link per record,
   and the picture should be clickable), so the fix cannot be a grid: the picture
   is pinned to the row's top-left corner and the row is padded to clear it. Then
   every line of text — title first — flows in one column beside it, and the top
   of the picture always lines up with the top of the title.

   `min-height` is what stops a short record (a title and a date, no summary)
   from letting the next row ride up over its picture. 11rem wide at the 5/3
   aspect the thumbnails are cropped to is 6.6rem tall, plus the row's own
   1rem top and bottom padding. */
.collection-media { grid-template-columns: minmax(0, 1fr); gap: 0; }
.collection-media .citem {
  padding: 1rem 0; border-bottom: 1px solid color-mix(in srgb, var(--color-text) 12%, transparent);
}
.collection-media .citem:has(.citem-img) {
  position: relative;
  padding-left: 12rem;
  min-height: calc(6.6rem + 2rem);
}
.collection-media .citem-link { display: block; }
/* `margin: 0` is load-bearing, not tidiness: a listing sits inside `.page-body`,
   where every picture is given `margin: 1rem 0` so pictures in prose breathe.
   Left on, that margin lands UNDER `top: 1rem` and drops the thumbnail a further
   16px, so it no longer lines up with the title it belongs to. */
.collection-media .citem-link > picture,
.collection-media .citem-link > .citem-img {
  position: absolute; top: 1rem; left: 0; width: 11rem; margin: 0;
}
.collection-media .citem-img { margin-bottom: 0; }
.collection-media .citem-title { font-size: 1.1rem; }
@media (max-width: 560px) {
  .collection-media .citem:has(.citem-img) { padding-left: 0; min-height: 0; }
  .collection-media .citem-link > picture,
  .collection-media .citem-link > .citem-img {
    position: static; width: 100%; display: block; margin: 0 0 .5rem;
  }
}

/* Wide cards: a big picture and the summary beside it. For a handful of
   exhibitions, where each one deserves the room. */
.collection-wide { grid-template-columns: minmax(0, 1fr); gap: 1.5rem; }
.collection-wide .citem {
  display: grid; grid-template-columns: minmax(0, 18rem) minmax(0, 1fr); gap: .25rem 1.25rem;
}
.collection-wide .citem-link { grid-row: 1 / span 3; }
.collection-wide .citem-img { aspect-ratio: 3 / 2; }
@media (max-width: 46rem) {
  .collection-wide .citem { grid-template-columns: minmax(0, 1fr); }
  .collection-wide .citem-link { grid-row: auto; }
}

/* The same block when it draws nothing — still the chunk's root, so it follows
   the same scale. Left hand-written, a list that happened to be empty would sit
   at a different gap from the same list with records in it. */
.collection-empty, .collection-missing {
  margin-block: var(--space-block); margin-inline: 0; padding: .9rem 1.1rem;
  border-left: 4px solid var(--color-accent);
  background: color-mix(in srgb, var(--color-text) 3.5%, transparent);
}

/* ── Taxonomies: term chips, term archives, the index of terms ──────
   A chip is a LINK and has to look like one at a glance — the first draft was
   a soft grey pill that read as a status badge, and nobody clicked it. Border
   plus the link colour, and a real hover state.

   `.is-flat` is a term with nothing to point at: one that no record uses, or
   one whose address another page already owns. It renders as plain text
   rather than as a link nobody can follow. */
.record-terms { margin: 2rem 0 0; display: flex; flex-direction: column; gap: .6rem; }
.term-group { display: flex; flex-wrap: wrap; gap: .4rem; align-items: baseline; }
.term-group-label {
  font-size: .85rem; text-transform: uppercase; letter-spacing: .06em;
  color: color-mix(in srgb, var(--color-text) 60%, transparent);
  margin-right: .2rem;
}
.term-chip {
  display: inline-block; padding: .18rem .6rem; font-size: .92rem;
  border: 1px solid color-mix(in srgb, var(--color-text) 22%, transparent);
  border-radius: 999px; text-decoration: none; color: var(--link-color);
  line-height: 1.5;
}
a.term-chip:hover {
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  border-color: var(--color-accent); color: var(--link-hover-color);
}
a.term-chip:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 2px; }
.term-chip.is-flat {
  color: color-mix(in srgb, var(--color-text) 65%, transparent);
  border-style: dashed;
}

/* The subscribe link under a list that publishes a feed. Quiet on purpose:
   almost nobody uses it, the few who do go looking for it, and a loud orange
   RSS button on a museum's news page dates the whole site. */
.feed-link { margin: 1.75rem 0 0; font-size: .92rem; }
.feed-link a { color: color-mix(in srgb, var(--color-text) 65%, transparent); }
.feed-link a:hover { color: var(--link-hover-color); }

/* Related records: what this one points at. A full grid, drawn by the ordinary
   card renderer, so a related object looks exactly like the same object in the
   catalogue — "related" must not become its own little design. */
.related { margin: 2.5rem 0 0; }
.related-h { margin: 0 0 .9rem; font-size: 1.25rem; }
.related + .related { margin-top: 2rem; }

/* A term archive lists several kinds of thing at once, each under its own
   heading. Spaced like a section break, not like a paragraph heading. */
.term-group-heading { margin: 2.25rem 0 .9rem; }
.term-group-heading:first-child { margin-top: .5rem; }

/* @root .b-text for text — the text block names its root .b-text, not .text,
   so design.py's name-derived roots ({.text, .block-text}) miss it and the
   block sits outside the spacing checks entirely. Same escape hatch as
   .collection-grid below, and the sanctioned route for any future block
   styled with a `b-` prefix, until someone decides `b-` should be first-class
   in the derivation.

   THIS LINE ALONE DOES NOT PUT THE BLOCK UNDER THE MARGIN SCAN. _core_roots()
   satisfies the "nothing sets a root rule" report, but the scan that catches a
   raw margin takes its roots from the name-derived set. The companion change in
   tools/design.py (_component_stylesheets unions in the declared roots) is what
   actually checks it. An @root line without that is a green light over an
   unchecked block. */

/* @root .pic for picture — the picture block names its root `.pic`, not
   `.picture`, so design.py's name-derived roots ({.picture, .block-picture})
   miss it and the block sits outside the spacing checks entirely. Same escape
   hatch as .b-text above.

   IT IS NAMED `.pic` ON PURPOSE AND MUST NOT BE "TIDIED" TO `.picture`. CAMRO's
   own custom_css already styles `figure.picture` for its image columns; a block
   whose root took that name would start inheriting a site's rules for something
   else, and the block's own <figure> would silently pick up that site's margin,
   width and caption styling. The abbreviation is the thing keeping the two
   apart, and this declaration is the sanctioned way to say so.

   THIS LINE ALONE DOES NOT PUT THE BLOCK UNDER THE MARGIN SCAN. _core_roots()
   satisfies the "nothing sets a root rule" report, but the scan that catches a
   raw margin takes its roots from the name-derived set; the companion in
   tools/design.py (_component_stylesheets unions in the declared roots) is what
   actually checks it. That companion HAS landed — verified by planting a raw
   margin in the block and watching the scan catch it, then removing it. An
   @root line without that is a green light over an unchecked block. */

/* @root .term-index for termindex — a generated listing with no block folder of
   its own, so its root rule can only live here. Same drift as .collection-grid
   had, and it would have been just as invisible. */
.term-index {
  list-style: none; padding: 0; margin-block: var(--space-block); margin-inline: 0;
  display: grid; gap: .1rem;
}
.term-index-item {
  padding: .55rem 0;
  border-bottom: 1px solid color-mix(in srgb, var(--color-text) 10%, transparent);
}
.term-index-item > a { font-weight: 600; }
/* The count sits beside the name, quietly. It answers "is there anything in
   here?" before the click, which is the one thing an index of terms is for. */
.term-count {
  margin-left: .5rem; font-size: .85rem;
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
}
.term-desc {
  margin: .2rem 0 0; font-size: .95rem;
  color: color-mix(in srgb, var(--color-text) 75%, transparent);
}

/* ── A record's own page ────────────────────────────────────────────
   One template (layouts/record.njk) for every type, so one set of rules. The
   fact table had no styling at all before this: only the object page had a
   <dl>, and only the object page's rules knew about it, so a publication's
   author and ISBN rendered as an unstyled browser default. */
.record-meta {
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  margin: -.3rem 0 1rem; font-size: 1rem;
}
/* The standfirst: the sentence under a headline. Bigger than the body and
   quieter than the title, which is what makes it read as an introduction
   rather than as the first paragraph in a larger font. */
.record-standfirst {
  font-size: 1.15rem; line-height: 1.55; margin: 0 0 1.5rem;
  max-width: 40em;
  color: color-mix(in srgb, var(--color-text) 82%, transparent);
}
.record-past-note {
  background: color-mix(in srgb, var(--color-accent) 14%, transparent);
  padding: .5rem .8rem; border-radius: var(--card-radius); display: inline-block;
}
.record-hero {
  width: 100%; height: auto; border-radius: var(--radius);
  object-position: var(--focus, 50% 35%); display: block; margin: 0 0 1.25rem;
}
.record-layout { display: flex; gap: 1.5rem; flex-wrap: wrap; margin-bottom: 1rem; }
.record-images { flex: 1 1 18rem; }
.record-images img { width: 100%; height: auto; border-radius: var(--radius); margin-bottom: .75rem; display: block; }
.record-facts { flex: 1 1 14rem; margin: 0 0 1rem; }
.record-facts dt { font-weight: 700; color: var(--heading-color); }
.record-facts dd { margin: 0 0 .6rem; }
/* "Kind: Article" on one line, rather than the label on its own line with the
   answer under it. Six facts were twelve lines and 364px of a catalogue entry
   before the reader reached anything they came for. Opt-in per site
   (`records.facts_layout: "inline"`), because it is a house-style preference
   rather than a fix, and sites already built should not move under anyone.

   A GRID rather than literally-inline text: with `max-content` the labels form
   a column and every answer starts at the same place, which is what makes a
   list of facts scannable. A long answer still wraps in its own column instead
   of running back under the labels. */
.record-facts--inline {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  column-gap: .6rem;
  row-gap: .35rem;
}
.record-facts--inline dt::after { content: ":"; }
.record-facts--inline dd { margin: 0; }
.record-back { margin-top: 2rem; }

/* ── Pager: real addresses for page two onwards ─────────────────── */
.pager { display: flex; gap: .75rem; align-items: center; flex-wrap: wrap; margin: 2rem 0 0; }
.pager-nums { display: flex; gap: .35rem; flex-wrap: wrap; }
.pager a, .pager-num {
  padding: .4rem .7rem; border-radius: var(--card-radius);
  border: 1px solid color-mix(in srgb, var(--color-text) 20%, transparent);
  text-decoration: none; min-width: 2.5rem; text-align: center;
}
.pager .is-current {
  background: var(--color-primary); color: #fff; font-weight: 700;
  border-color: var(--color-primary);
}

/* ── Filter bar ─────────────────────────────────────────────────── */
.filter-bar { display: flex; gap: 1rem; flex-wrap: wrap; align-items: end; margin: 1rem 0; }
.filter-bar label { display: flex; flex-direction: column; gap: .25rem; font-size: .9rem; font-weight: 600; }
.filter-bar select, .filter-bar input { padding: .45rem .6rem; font-size: 1rem; border: 1px solid color-mix(in srgb, var(--color-text) 35%, transparent); border-radius: 4px; min-width: 7rem; }
/* A <select> is as wide as its LONGEST option, even while showing "All". One
   long term name ("The bay and the baymen") therefore set the width of the whole
   menu and was enough on its own to push the last filter onto a second line.
   Capped: the closed menu shows the chosen value, which is short. */
.filter-bar select { max-width: 13rem; }
/* The search box used to be `flex: 1 1 14rem` and ate every spare pixel, so on a
   wide page one text field ran the width of the screen and pushed the menus onto
   a second line. It is a box you type "lecture" into; it does not need to be
   half the page. Sized, not greedy — the whole bar then sits on one line at any
   normal width and still wraps on a phone. */
.filter-search { flex: 0 1 14rem; }
/* The result count. Sits under the bar rather than in it: the bar is a flex
   row of controls and a sentence is not a control. Written only once a
   filter is set - see platform/assets/js/filter.js. */
.filter-count { margin: -.25rem 0 1rem; font-size: .92rem; opacity: .8; }

/* ── Footer ─────────────────────────────────────────────────────── */
/* margin-top stays `auto` (set in the sticky-footer rule up top) — do NOT
   give the footer a fixed margin-top here or short pages lose the pin. */
.site-footer {
  background: var(--footer-bg);
  color: var(--footer-text);
  font-family: var(--footer-font);
  font-size: var(--footer-size);
  font-weight: var(--footer-weight);
}
.site-footer a { color: var(--footer-text); }
.site-footer-inner {
  max-width: var(--shell-width); margin: 0 auto;
  padding: 2rem var(--gutter) 1rem;
  display: flex; justify-content: space-between; gap: 2rem; flex-wrap: wrap;
}
.footer-name { font-family: var(--font-heading); font-weight: 700; font-size: 1.1rem; margin-top: 0; }
/* The same mark as the header, quieter. It is the sign-off on the page, not the
   masthead, so it gets its own size rather than inheriting the header's. */
.footer-name--lockup { font-size: inherit; margin-bottom: .9rem; }
.footer-name--lockup .site-title-lock {
  font-size: var(--footer-lockup-size, 1.4rem);
}
.footer-links { list-style: none; margin: 0; padding: 0; columns: 2; gap: 2rem; }
.footer-links a { text-decoration: none; line-height: 1.9; }
.footer-links a:hover { text-decoration: underline; }
.footer-colophon { text-align: center; padding: .5rem 0 1.25rem; margin: 0; font-size: .85rem; opacity: .8; }

/* ── THE FOOTER BUILDER (2026-09-25; the header builder's model) ─────────
   Every rule here answers to a value in site.config.json `footer` that the
   builder writes (tools/footer_config.py); a site that never used it carries
   none of these classes and none of this applies. One step more specific than
   a site's own `.site-footer` / `.site-footer-inner` / `.footer-col a` rules
   (design.css loads after this file — CAMRO sets its footer grid and its link
   colours outright), because a layout a site's stylesheet silently overrides
   is a setting that does nothing.

   LAYOUTS ARE DESKTOP; below 761px every footer stacks, as it always did.
   READING ORDER IS SCREEN ORDER: the name, the contact lines, the columns,
   the accounts, the small print — arranged in place, never reordered. */
@media (min-width: 761px) {
  .site-footer.site-footer--centered .site-footer-inner {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, max-content));
    justify-content: center; text-align: center; gap: 1.5rem 3rem;
  }
  .site-footer.site-footer--centered .site-footer-inner > :first-child { grid-column: 1 / -1; }
  .site-footer.site-footer--centered :is(.footer-logo, .footer-tagline) { margin-inline: auto; }
  .site-footer.site-footer--centered .footer-social { justify-content: center; }
  .site-footer.site-footer--centered .footer-colophon { justify-content: center; text-align: center; }

  .site-footer.site-footer--split .site-footer-inner {
    display: flex; flex-wrap: wrap; gap: 2rem 3rem; grid-template-columns: none;
  }
  .site-footer.site-footer--split .site-footer-inner > :first-child { flex: 1 1 16rem; margin-right: auto; }
  .site-footer.site-footer--split .site-footer-inner > nav { flex: 0 1 auto; }
  .site-footer.site-footer--split .footer-colophon { display: flex; justify-content: space-between; gap: 1rem 2rem; flex-wrap: wrap; text-align: left; }

  .site-footer.site-footer--minimal .site-footer-inner {
    display: block; grid-template-columns: none; padding-bottom: .5rem;
  }
  .site-footer.site-footer--minimal .site-footer-inner > div {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: .4rem 1.75rem;
  }
  .site-footer.site-footer--minimal .site-footer-inner > div > p { margin: 0; }
  .site-footer.site-footer--minimal .footer-colophon { text-align: left; }
}
.site-footer .footer-logo { display: block; max-height: 4.5rem; width: auto; max-width: 16rem; }
.site-footer .footer-tagline { max-width: 28rem; opacity: .85; }
.site-footer .footer-social {
  list-style: none; display: flex; flex-wrap: wrap; gap: .45rem 1.25rem;
  max-width: var(--shell-width); margin: 0 auto; padding: .25rem var(--gutter) .75rem;
}
.site-footer .footer-social-link { font-weight: 600; text-decoration: none; }
.site-footer .footer-social-link:hover { text-decoration: underline; }
.site-footer .footer-small-print { margin-inline: .5rem; }

/* details */
.site-footer.site-footer--rule .footer-colophon {
  border-top: 1px solid color-mix(in srgb, currentColor 22%, transparent); padding-top: 1rem;
}
.site-footer.site-footer--compact { padding-top: 0; padding-bottom: 0; }
.site-footer.site-footer--compact .site-footer-inner { padding-top: 1.1rem; padding-bottom: .4rem; }

/* GROUNDS: the same variations a block's `bg=` offers. The ink is written out
   on the links and the small print, not only through tokens: a site that
   colours its footer links outright would otherwise leave pale words on a
   pale ground. */
.site-footer[data-bg="mist"] { --ftr-ground: color-mix(in srgb, var(--color-text) 5%, var(--color-bg)); --ftr-ink: var(--color-text); --ftr-link: var(--color-primary); }
.site-footer[data-bg="sand"] { --ftr-ground: color-mix(in srgb, var(--color-accent) 38%, var(--color-bg)); --ftr-ink: var(--color-text); --ftr-link: var(--color-primary); }
.site-footer[data-bg="navy"] { --ftr-ground: var(--color-primary); --ftr-ink: var(--color-bg); --ftr-link: var(--color-bg); }
.site-footer[data-bg] {
  --footer-bg: var(--ftr-ground); --footer-text: var(--ftr-ink);
  background: var(--ftr-ground); color: var(--ftr-ink);
}
.site-footer[data-bg] :is(p, .footer-col-heading, .footer-colophon, .footer-tagline) { color: var(--ftr-ink); }
.site-footer[data-bg] :is(a, .footer-col a, .footer-legal a, .footer-links a, .footer-social-link) { color: var(--ftr-link); }

/* ── Immersive hero (full-bleed image behind nav, dark screen) ──── */
.hero-banner {
  position: relative; display: flex; flex-direction: column;
  min-height: 55vh; overflow: hidden;
}
.hero-slide {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: var(--focus, 50% 50%);
  opacity: 0; animation-iteration-count: infinite;
  /* Two animations on one element, and the inline style on each slide sets a
     single animation-delay and animation-duration that both of them take. The
     fade is eased so a crossfade has a soft start and stop; the drift is LINEAR
     because a Ken Burns move that accelerates reads as a camera being shoved. */
  animation-name: hero-fade, hero-ken-burns;
  animation-timing-function: ease-in-out, linear;
}
/* THE POSITION COMES FROM A CLASS, never from an of-type selector. Every one
   of these images is wrapped in its own <picture> by the image plugin, so in
   the built page each <img> is the only img among its siblings and all three
   of :first-of-type, :only-of-type and :nth-of-type read wrong - the first two
   match EVERY slide and the third matches none. See base.njk. */
.hero-slide--alt { animation-name: hero-fade, hero-ken-burns-out; }
/* ONE picture is not a slideshow. hero-fade takes every slide to opacity 0 for
   the back half of its cycle, which is right when something else is covering
   it and wrong when nothing is: a lone hero image blinks out for a bit over
   half of every seven seconds. It sits still and stays put. */
.hero-slide--solo { animation: none; opacity: 1; }
.hero-slide--lead { opacity: 1; }
@keyframes hero-fade {
  0% { opacity: 0; } 4% { opacity: 1; }
  33% { opacity: 1; } 42% { opacity: 0; } 100% { opacity: 0; }
}
/* KEN BURNS. --hero-zoom is the whole control and 1 means a still picture, so
   every site that does not ask for this gets exactly what it had - the
   animation still runs, it just moves nothing.

   The sideways drift is DERIVED from the zoom rather than being a second
   number, and that is what makes it safe to hand out: at scale S the picture
   overhangs its frame by (S-1)/2 on each side, and the drift asks for
   (S-1)*.15 of that - roughly a third of what is available, and exactly zero
   when the zoom is off. A pan written as its own value can be set past the
   edge of the picture, and the failure looks like a flickering white margin
   for two seconds every rotation.

   The move runs to 45% and snaps back a thousandth of a per cent later, which
   is INSIDE the dark part of the cycle: hero-fade has already taken this slide
   to opacity 0 by 42%. Resetting a transform on a picture somebody can see is
   a jolt; resetting it three per cent of a cycle after it went dark is free.

   Keep the zoom small. Scaling up is throwing away resolution, and 1.08 on a
   2000px hero is asking a 1852px crop to fill it - invisible. Past about 1.15
   it starts to show on the detail, and it stops reading as atmosphere and
   starts reading as an effect. */
@keyframes hero-ken-burns {
  0%   { transform: scale(1) translate(0, 0); }
  45%  { transform: scale(var(--hero-zoom))
                    translate(calc((var(--hero-zoom) - 1) * -15%),
                              calc((var(--hero-zoom) - 1) * -9%)); }
  45.001%, 100% { transform: scale(1) translate(0, 0); }
}
@keyframes hero-ken-burns-out {
  0%   { transform: scale(var(--hero-zoom))
                    translate(calc((var(--hero-zoom) - 1) * 13%),
                              calc((var(--hero-zoom) - 1) * 8%)); }
  45%  { transform: scale(1) translate(0, 0); }
  45.001%, 100% { transform: scale(var(--hero-zoom))
                    translate(calc((var(--hero-zoom) - 1) * 13%),
                              calc((var(--hero-zoom) - 1) * 8%)); }
}
/* Dragging the focal point onto a moving, scaled picture sets it several per
   cent away from where the pointer was, and the mistake is invisible until the
   page is rebuilt. Crop mode puts the slides back the way they were. */
body.adm-cropping .hero-slide { animation-name: hero-fade; }
@media (prefers-reduced-motion: reduce) {
  .hero-slide { animation: none !important; opacity: 0; }
  .hero-slide--lead { opacity: 1; }
}
.hero-overlay {
  position: absolute; inset: 0;
  background: linear-gradient(rgba(0,0,0,.55), rgba(0,0,0,.15) 35%,
              rgba(0,0,0, var(--hero-screen, .35)));
}
/* .hero-banner scoping outranks any theme's .site-header background/border,
   so immersive headers stay transparent under every theme. */
.hero-banner .site-header {
  position: relative; background: transparent; border-bottom: 0;
  background-image: linear-gradient(rgba(0,0,0,.35), transparent);
}
/* Menu and title sitting directly ON a photograph.
   Measured on Tuckerton's three home-page slides (2026-08-19): under the menu
   the brightest pixels reach 0.92, 0.67 and 0.99 relative luminance, so white
   text lands at 3.06:1, 3.97:1 and 2.84:1 against them. Getting the worst of
   those to the 4.5:1 bar with a dark screen ALONE needs about 82% black over
   the picture, which is no longer a photograph. And a slideshow means you meet
   the bright frame sooner or later whatever the still looked like.
   A shadow gives every glyph its own dark ground regardless of what is behind
   it. Same technique .hero-title already uses on the headline; this extends it
   to the menu, which had nothing. Keep BOTH shadows: the tight one carries the
   letterform, the wide one lifts it off a busy background. */
.hero-banner .nav-top > .nav-top-list > .nav-item > a,
.hero-banner .nav-top > .nav-top-list > .nav-item > .nav-parent,
.hero-banner .nav-mobile-toggle,
.hero-banner .site-title {
  text-shadow: 0 1px 2px rgba(0,0,0,.9), 0 0 12px rgba(0,0,0,.55);
}
/* ...but NOT inside an opened dropdown, which has its own solid panel behind
   it. A shadow there is just muddy text on a flat colour. */
.hero-banner .submenu a { text-shadow: none; }
.hero-title {
  position: relative; margin-top: auto;
  padding: 3.5rem var(--gutter) 2.5rem;
  max-width: var(--shell-width); margin-left: auto; margin-right: auto; width: 100%;
}
/* White, not the heading colour: this text sits on a photograph behind a dark
   screen, and there is no site colour that stays legible over every image. */
.hero-title h1 {
  color: #fff; font-size: var(--hero-title-size); margin: 0;
  text-shadow: 0 2px 14px rgba(0,0,0,.55);
}
.hero-title h1::after { display: none; }
/* The home page headline as the site's mark - see base.njk. Nothing else needs
   restating here: .site-title-lock carries its own family, weight, tracking and
   rule, so the mark on the photograph is the same mark as the one in the header
   and the footer, at a different size. */
.hero-title-name .site-title-lock { font-size: var(--hero-lockup-size); }
.hero-tagline {
  color: #fff; font-size: var(--hero-tagline-size); margin: .4rem 0 0;
  text-shadow: 0 1px 8px rgba(0,0,0,.6);
}

/* Card thumbnails, their uniform crop and their per-image focal point, now live
   with the record list above (`.citem-img`) — there is one card. */

/* ── Announcements: banner + popup ──────────────────────────────── */
.site-banner {
  background: var(--banner-bg); color: var(--banner-text);
  display: flex; align-items: center; justify-content: center; gap: 1rem;
  padding: .55rem 2.5rem .55rem 1rem; position: relative; text-align: center;
  /* The banner now defaults to the same colour as the header (it had to — see
     --banner-bg), so without a rule between them the two merge into one navy
     slab and the announcement stops looking like an announcement. A hairline
     is enough to read as a separate strip, and being translucent white it
     works over any banner colour a site picks without needing its own token.
     Decorative, so no contrast requirement of its own. */
  border-bottom: 1px solid rgba(255, 255, 255, .28);
}
.site-banner p { margin: 0; font-weight: 600; }
.site-banner a { color: var(--banner-text); text-decoration: underline; }
.banner-close {
  position: absolute; right: .5rem; top: 50%; transform: translateY(-50%);
  background: none; border: 0; color: var(--banner-text); font-size: 1.4rem;
  cursor: pointer; padding: .2rem .5rem; line-height: 1;
}
.site-popup {
  border: 0; border-radius: 10px; padding: 1.5rem 1.75rem; max-width: 28rem;
  box-shadow: 0 12px 40px rgba(0,0,0,.35);
  font-family: var(--font-body); color: var(--color-text);
}
.site-popup::backdrop { background: rgba(0,0,0,.55); }
.site-popup h2 { margin-top: 0; }
.popup-close {
  background: var(--color-primary); color: #fff; border: 0; border-radius: 6px;
  padding: .55rem 1.2rem; font-size: 1rem; cursor: pointer; margin-top: .5rem;
}
.popup-close:hover { background: var(--color-accent); }

/* ── Preview gate + ribbon ──────────────────────────────────────── */
.preview-ribbon {
  position: fixed; top: .8rem; left: -2.6rem; z-index: 300; transform: rotate(-45deg);
  background: #c0392b; color: #fff; font: 700 .8rem/1 sans-serif;
  letter-spacing: .12em; padding: .45rem 3rem; pointer-events: none;
}
.preview-gate { max-width: 34rem; margin: 3rem auto; text-align: center; }

@media (prefers-reduced-motion: no-preference) {
  .nav-slideout { transition: transform .2s ease; }
}

/* ── New-content review markers (staging only) ─────────────────────────── */
/* Deliberately loud and deliberately ugly. These exist so a client scanning a
   migrated site can see instantly which words are NEW and which came off their
   old site — they are review scaffolding, not design. The marker says "new",
   never "AI": the client is being asked whether the words are right, not who
   typed them (Eric 2026-08-03). Alt text is the one place we DO label machine
   authorship, because there it contrasts with the museum's own wording.
   Nothing here renders once site.mode is "live" — see `newReview` in
   eleventy-base.js. */

.new-block {
  position: relative;
  border: 2px dashed #c0392b;
  border-radius: 8px;
  background: rgba(192, 57, 43, .04);
  padding: 1.6rem 1.1rem 1.1rem;
  margin: 2rem 0;
}
.new-block > :last-child { margin-bottom: 0; }
.new-flag {
  position: absolute; top: -.72rem; left: .9rem;
  margin: 0; padding: .25rem .6rem;
  background: #c0392b; color: #fff; border-radius: 4px;
  font: 700 .66rem/1.3 -apple-system, "Segoe UI", sans-serif;
  letter-spacing: .09em; text-transform: uppercase;
  max-width: calc(100% - 1.8rem);
}
.new-blocknote {
  margin: 0 0 .9rem; color: #7a2f26; font-size: .85rem; font-style: italic;
}

.new-page-flag {
  border: 2px dashed #c0392b; border-left-width: 7px;
  background: rgba(192, 57, 43, .05);
  border-radius: 6px;
  padding: .9rem 1.1rem; margin: 0 0 2rem;
}
.new-page-flag p { margin: 0; font-size: .93rem; }
.new-page-flag strong { color: #a02b1e; }
.new-page-flag .new-page-note {
  margin-top: .4rem; color: #555; font-size: .85rem; font-style: italic;
}

/* ── Page-header variants (2026-08-03) ─────────────────────────────────── */

/* STANDING RULE, NARROWED 2026-08-06: body text never wraps beside a content
   image BY DEFAULT — every image in page content is a hard block with space
   above and below. The rule was written for one particular situation and was
   never meant to be the answer everywhere (Eric), so a single picture may be
   set to sit beside the text from its own settings (`align=left|right`). It
   is opt-in per picture, one at a time; there is no site-wide switch and
   there should not be one. The float rules live with .fig further down. */
.page-body img, .page-body picture { display: block; margin: 1rem 0; }
.page-body picture img { margin: 0; }

/* Textural header: nav header expands into a decorative image band under a
   heavy contrast screen. Scoped like .hero-banner so themes can't repaint it. */
.header-band {
  position: relative;
  background-image:
    linear-gradient(rgba(14, 20, 28, .68), rgba(14, 20, 28, .68)),
    var(--band-img);
  background-size: cover; background-position: center;
}
.header-band .site-header {
  background: transparent; border-bottom: 0;
}
.band-title {
  /* 1.5rem rather than var(--gutter) is this band's own inset, deliberately
     a little wider than the site gutter; the CAP still has to be the shell
     or the title stops lining up with the content below it. Was a hard
     `72rem` until 2026-08-22, which meant a site that changed its page
     width moved everything except this. */
  padding: 4.5rem 1.5rem 2rem;
  max-width: var(--shell-width); margin: 0 auto; width: 100%;
  box-sizing: border-box;
}
/* Texture-only band (image-landing): no title, just enough depth to read as
   an expanded header. */
.band-pad { height: 5.5rem; }
.band-title h1 {
  color: #fff; margin: 0; font-size: clamp(1.7rem, 4vw, 2.5rem);
  text-shadow: 0 2px 12px rgba(0, 0, 0, .5);
}

/* Editorial header: the featured image is content, not decoration — large on
   the left, title + abstract right, page body below. */
.page-header-editorial {
  display: grid; grid-template-columns: minmax(15rem, 24rem) 1fr;
  gap: 2rem; align-items: center; margin: 1.75rem 0 2.25rem;
}
.page-header-editorial img {
  width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
  object-position: var(--focus, 50% 50%); border-radius: 8px;
}
.page-header-editorial h1 { margin: 0 0 .6rem; }
.page-abstract {
  font-size: 1.08rem; line-height: var(--line-body);
  color: var(--abstract-color); margin: 0;
}
@media (max-width: 700px) {
  .page-header-editorial { grid-template-columns: 1fr; gap: 1.1rem; }
}

/* Footer texture: a hero image as monochrome background grain — heavy overlay
   via low opacity + grayscale so footer text always reads. */
.site-footer.has-texture {
  position: relative; overflow: hidden;
  /* Give the image room. A short footer band crops a wide photo down to a
     meaningless sliver — at ~7rem all you saw of the cemetery-gate shot was
     treetops. Roughly double the height, so the subject reads. */
  min-height: 20rem;
  display: flex; flex-direction: column; justify-content: center;
}
/* The footer became a flex container above, so the inner block would otherwise
   shrink-to-fit and drift off the page's left margin. Keep it full width. */
.site-footer.has-texture .site-footer-inner {
  width: 100%; padding-top: 3.5rem; padding-bottom: 2rem;
}
.site-footer.has-texture::before {
  content: ""; position: absolute; inset: 0;
  background-image: var(--footer-img);
  background-size: cover;
  /* A wide, short band crops a photo hard. Default to the middle; sites whose
     subject sits high or low in the frame set site.footer_focus (e.g.
     "center bottom") rather than being stuck with a band of sky or grass. */
  background-position: var(--footer-focus, center center);
  filter: grayscale(1); opacity: .14; pointer-events: none;
}
@media (max-width: 640px) {
  .site-footer.has-texture { min-height: 26rem; }
}
.site-footer.has-texture > * { position: relative; }

/* -- Buttons + contact form (2026-08-03) --------------------------------- */
.button {
  display: inline-block; border: 0; cursor: pointer;
  border-radius: var(--button-radius);
  background: var(--button-bg);
  color: var(--button-color);
  font: inherit; font-weight: var(--button-weight);
  padding: var(--button-pad);
  /* A LINK drawn as a button is still a link, and the site-wide link rules are
     wrong on it in exactly two ways. The browser underlines it, so the words
     inside the button arrived underlined; and `a:not(.site-title):hover` above
     repaints every link with --link-hover-color, so on hover the LABEL went to
     the accent at the same moment `--button-hover-bg` took the button there
     too. On CAMRO both are #B08E4F, so hovering a button made its words
     disappear into it (Cam, 2026-09-01: "the words are underlined ... both the
     words and the button turns gold"). `.p-cta__button` has carried these same
     two lines since 2026-08-08 for the same reason; this is that fix, moved to
     the class every other button on the site actually uses. */
  text-decoration: none;
}
.button:hover { background: var(--button-hover-bg); }
/* `a.button` rather than `.button`, on purpose. It matches the specificity of
   `a:not(.site-title):hover` and wins on source order — the same way
   `a.p-cta__button:hover` does — and it leaves the contact form's real
   <button> alone, which the link rules never reached in the first place. */
a.button:hover { color: var(--button-color); }

/* -- The four button looks, from the site's own colours (2026-09-25) --------

   Cam: "it pulls the style from the tool and then it gives them a few
   different options ... just the outline ... full color ... the accent color
   button ... just the text one ... a pointer icon." The button panel offers
   exactly these, as the skin words in tools/patternparts.py SKIN_CLASSES:

     solid  -> .btn               the primary colour, filled
     accent -> .btn.btn-accent    the accent colour, filled
     ghost  -> .btn.btn-ghost     an outline in the primary colour
     bare   -> .btn.btn-bare      the words alone, as a strong link
     + .btn-arrow on any of them  a trailing arrow

   THE BASE IS WRAPPED IN :where() so it weighs nothing: a site skin that
   already draws its own `.btn` (CAMRO's design.css does) wins without a
   fight, and a site that never wrote one still gets a real button rather than
   a bare link. Only the ACCENT look and the DARK-BAND swap are written at full
   weight, because those are the two things a site's own `.btn` would
   otherwise paint over with its primary colour. */
:where(.btn) {
  display: inline-flex; align-items: center; gap: .5em;
  border: 1px solid var(--btn-fill, var(--button-bg));
  border-radius: var(--button-radius);
  background: var(--btn-fill, var(--button-bg));
  color: var(--btn-on, var(--button-color));
  font: inherit; font-weight: var(--button-weight);
  padding: var(--button-pad);
  text-decoration: none; cursor: pointer;
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
:where(a.btn):hover { color: var(--btn-on, var(--button-color)); filter: brightness(.92); }
:where(.btn-ghost) { background: transparent; color: var(--btn-fill, var(--button-bg)); }
:where(a.btn-ghost):hover {
  background: var(--btn-fill, var(--button-bg)); color: var(--btn-on, var(--button-color)); filter: none;
}

/* A BUTTON INSIDE WORDS IS STILL A BUTTON. A text block's prose rule
   (`.b-text__inner a` on CAMRO) underlines every link in it and outweighs a
   one-class `.btn`, so a button placed in a paragraph arrived underlined.
   `a[class~="btn"][href]` weighs two attributes and an element — just enough to
   clear a prose rule, and still less than a site rule that names its own
   button on purpose. */
a[class~="btn"][href], a[class~="btn"][href]:hover { text-decoration: none; }

/* TEXT LINK, at two classes for the reason the accent is: a site's own
   `.btn` would otherwise paint it solid. */
.btn.btn-bare, a.btn.btn-bare:hover {
  background: transparent; border-color: transparent; padding-inline: 0;
  color: var(--btn-fill, var(--button-bg)); filter: none;
}
a.btn.btn-bare:hover { text-decoration: underline; text-underline-offset: .2em; }

/* ACCENT. Two classes, so it outranks a site's single-class `.btn`. Its words
   are white unless the site says otherwise (--button-accent-ink), and a
   browser that can work out the readable ink itself is allowed to. */
.btn.btn-accent, a.btn.btn-accent:hover {
  --btn-fill: var(--color-accent);
  --btn-on: var(--button-accent-ink, #fff);
  background: var(--color-accent); border-color: var(--color-accent);
  color: var(--button-accent-ink, #fff);
}
@supports (color: contrast-color(red)) {
  .btn.btn-accent, a.btn.btn-accent:hover {
    color: var(--button-accent-ink, contrast-color(var(--color-accent)));
  }
}

/* THE ARROW. Drawn in CSS, not typed into the words, so a screen reader reads
   "Join us" and not "Join us right arrow", and so it takes the button's own
   ink on every ground. */
.btn-arrow::after {
  content: "\2192"; display: inline-block; margin-inline-start: .45em;
  transition: transform .15s ease;
}
.btn-arrow:hover::after { transform: translateX(3px); }
@media (prefers-reduced-motion: reduce) { .btn-arrow:hover::after { transform: none; } }

/* THE LIGHT VERSIONS, for a button on a ground a pattern paints dark itself
   (a hero) rather than a dressed band. The editor writes these on its own
   when it measures the ground as dark (buttonFields); the person only ever
   picks Solid, Outline or Text link. Solid and Outline weigh nothing, since a
   skin that defines them wins; the text link is written at two classes for
   the reason .btn-bare is, or `btn` under it paints it solid. */
:where(.btn-invert) {
  --btn-fill: var(--color-bg); --btn-on: var(--color-primary);
  background: var(--color-bg); border-color: var(--color-bg); color: var(--color-primary);
}
:where(.btn-ghost-invert) {
  background: transparent; color: var(--color-bg);
  border-color: color-mix(in srgb, var(--color-bg) 60%, transparent);
}
.btn.btn-bare-invert, a.btn.btn-bare-invert:hover {
  background: transparent; border-color: transparent; padding-inline: 0;
  color: var(--color-bg); filter: none;
}
a.btn.btn-bare-invert:hover { text-decoration: underline; text-underline-offset: .2em; }

/* INSIDE ANY BAND the band paints every link with its deep colour
   (`.blk[data-bg] a`, below), which is right for a link and wrong for the
   words on a filled button: on a paper band a solid button's words went the
   same green as the button and vanished. The two FILLED looks take their own
   ink back here; the outline and the text link keep the band's colour, which
   is what they are meant to wear. */
.blk[data-bg] a.btn:not(.btn-ghost):not(.btn-bare):not(.btn-accent):not([class*="-invert"]),
.blk[data-bg] a.btn:not(.btn-ghost):not(.btn-bare):not(.btn-accent):not([class*="-invert"]):hover {
  color: var(--btn-on, var(--button-color));
}
.blk[data-bg] a.btn.btn-accent, .blk[data-bg] a.btn.btn-accent:hover {
  color: var(--button-accent-ink, #fff);
}
@supports (color: contrast-color(red)) {
  .blk[data-bg] a.btn.btn-accent, .blk[data-bg] a.btn.btn-accent:hover {
    color: var(--button-accent-ink, contrast-color(var(--color-accent)));
  }
}

/* ON A DARK BAND THE BUTTON TURNS ITSELF OVER (Cam, 2026-09-25: automatic,
   not a second list of "for a dark background" styles). The band already
   names its deep colour and its ground (--blk-deep / --blk-ground, above), so
   a solid button becomes the page colour with the band's colour for words,
   an outline takes the page colour, and the plain link follows the band's
   link colour. The accent look is left alone: it was chosen to stand out and
   does so on either ground. The old -invert skins still work as written. */
.blk[data-bg="navy"] .btn:not(.btn-accent) {
  --btn-fill: var(--blk-deep); --btn-on: var(--blk-ground);
  background: var(--blk-deep); border-color: var(--blk-deep); color: var(--blk-ground);
}
.blk[data-bg="navy"] .btn.btn-ghost {
  background: transparent; color: var(--blk-deep);
  border-color: color-mix(in srgb, var(--blk-deep) 60%, transparent);
}
.blk[data-bg="navy"] a.btn.btn-ghost:hover { background: var(--blk-deep); color: var(--blk-ground); }
.blk[data-bg="navy"] .btn.btn-bare { background: transparent; border-color: transparent; color: var(--blk-deep); }

.contact-form .field {
  margin-bottom: 1rem;
}

.contact-form label {
  display: block;
  font-weight: 600;
  margin-bottom: 0.25rem;
}

.contact-form .optional {
  font-weight: 400;
  opacity: 0.7;
}

.contact-form input,
.contact-form textarea {
  width: 100%;
  padding: 0.6rem 0.7rem;
  border: 1px solid rgba(0, 0, 0, 0.25);
  border-radius: 4px;
  font: inherit;
  background: #fff;
  color: inherit;
}

.contact-form input:focus-visible,
.contact-form textarea:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: 1px;
}

/* Honeypot. Off-screen rather than display:none — scripted bots that skip
   hidden inputs still see this one, and aria-hidden keeps it off the
   accessibility tree for real people. */
.contact-form .hp-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.contact-form button[disabled] {
  opacity: 0.65;
  cursor: progress;
}

.form-status {
  margin-top: 0.75rem;
  min-height: 1.4em;
  font-size: 0.95rem;
}

.form-status.is-working {
  opacity: 0.8;
}

.form-status.is-ok {
  color: #1a6b2f;
  font-weight: 600;
}

.form-status.is-error {
  color: #a1241c;
  font-weight: 600;
}

.cf-turnstile {
  margin: 1rem 0;
}

/* ── a form that carries a recording ─────────────────────────────────────
   Only the share-a-recording form uses these; every other form is untouched. */

/* A hint belongs BETWEEN the label and the box it explains, and it has to be
   readable — this is where "a rough date is fine" lives, which is the sentence
   that stops somebody abandoning the form because they cannot remember. */
.contact-form .field-hint {
  margin: 0 0 .35rem;
  font-size: .9rem;
  color: var(--abstract-color);
  font-weight: 400;
}

/* The consent box. The control comes FIRST and the label sits beside it, which
   is the only arrangement that reads as a thing you tick. The whole row is a
   generous target, because this is the one box that must not be missed. */
.contact-form .field-check {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: .25rem .6rem;
  align-items: start;
  margin-bottom: 1.25rem;
}

.contact-form .field-check input {
  width: 1.15rem;
  height: 1.15rem;
  margin: .15rem 0 0;
  accent-color: var(--color-primary);
}

.contact-form .field-check label {
  margin: 0;
  font-weight: 400;
  cursor: pointer;
}

.contact-form .field-check .field-hint {
  grid-column: 2;
  margin: 0;
}

.contact-form .field-file input[type="file"] {
  padding: .5rem;
  background: #fff;
  cursor: pointer;
}

/* What was picked, said in words with its size. A file input shows a name and
   nothing else, and "that is a big one" is the most useful thing to know before
   pressing send. */
.contact-form .field-chosen {
  margin: .5rem 0 0;
  font-size: .9rem;
  color: var(--color-text);
  font-weight: 600;
}

/* REAL progress, not a spinner. How far an upload has got is genuinely
   knowable, and on a long recording over a phone connection the difference
   between a percentage and a spinner is the difference between waiting and
   giving up. Hidden until an upload starts — a bar sitting at 0% before
   anyone has pressed anything reads as something already wrong. */
.upload-progress {
  margin: 1rem 0 0;
}

.upload-bar {
  height: .6rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--color-text) 12%, transparent);
  overflow: hidden;
}

.upload-bar-fill {
  height: 100%;
  width: 0;
  border-radius: 999px;
  background: var(--color-primary);
  transition: width .25s ease;
}

@media (prefers-reduced-motion: reduce) {
  .upload-bar-fill { transition: none; }
}

.upload-note {
  margin: .4rem 0 0;
  font-size: .9rem;
  color: var(--color-text);
}

/* ─────────────────────────────────────────────────────────────────────
   THE BLOCK DRESS — the header and the ground every block gets free.

   Written by platform/blocks/_dress.js from the `eyebrow=`, `heading=`,
   `lede=` and `bg=` keywords; see that file for why it exists. A block
   given none of them is not wrapped at all, so nothing here applies to any
   page that has not asked for it.

   BEFORE the @blocks marker on purpose: a block's own rules should be able
   to say something different about its header without a specificity fight.

   The four grounds are VARIATIONS, not colours. Each one sets five custom
   properties, and every rule below — plus any a block writes — reads those
   rather than naming a colour outright. That is what makes the control safe
   to hand to a client: there is no combination that produces unreadable
   text, because the accent MOVES on the dark ground rather than the
   contrast rule being broken.
   ───────────────────────────────────────────────────────────────────── */
.blk[data-bg] {
  background: var(--blk-ground);
  color: var(--blk-ink);
  padding: var(--blk-pad, clamp(1.6rem, 3vw, 2.6rem)) var(--blk-pad-x, clamp(1.25rem, 3vw, 2.4rem));
}
/* Paper is the page itself, so it takes no ground and no padding — a block
   on paper must sit exactly where an undressed block sat. */
.blk[data-bg="paper"] { background: none; padding: 0; }

/* A GROUND IS A BAND, AND A BAND REACHES BOTH EDGES OF THE SCREEN.
   Cam, 2026-09-01: "whenever you do the background, it has the background for
   that small block, but it doesn't go full width across ... the background,
   generally, for the block needs to go all the way across the whole
   horizontal space."

   He is describing a real fault and not a preference. The ground was painted
   on the block where the block sits — inside `.page-body`, which is capped at
   --content-width — so a navy section came out as an 1152px rectangle floating
   in white while the site header directly above it ran edge to edge. Nothing
   else on the page is coloured that way, so it did not read as a section; it
   read as a box somebody drew.

   SPREAD THE PAINT, DO NOT MOVE THE BOX. This is `.p-cta--large`'s idiom
   (platform/patterns/cta/pattern.css), and it is used here for the reason
   that comment already gives: a shadow spread to 100vmax with a matching
   clip-path, "rather than 100vw, which is wider than the page on any browser
   drawing a classic scrollbar and puts a horizontal scrollbar on every page
   carrying one."

   That was written before this rule existed and it predicted exactly what
   this rule did on its first two attempts. Both moved the ELEMENT — negative
   margins out to 100vw with the inset put back as padding, then an absolutely
   positioned backdrop doing the same arithmetic — and both put 8px of
   sideways scroll on every banded page on Windows and Linux, plus a 7.5px
   content misalignment nobody on a Mac could see. Nothing here moves, so
   there is nothing to overshoot, nothing to clip at the page level, and no
   inset to put back: the wrapper sits exactly where an undressed block sits
   and only the paint is wider. Two viewport measurements no longer have to
   agree for the layout to be right.

   THE CLIP IS VERTICAL ONLY, AND ITS PRICE IS MEASURED IN PAINT.
   `inset(0 -100vmax)` keeps the whole horizontal spread and cuts flush at the
   top and bottom edges, which is what stops one band's ground washing over
   the band above it. The cost is that it also cuts anything a BLOCK draws
   past those same two edges.

   Counted in rendered pixels rather than inferred from geometry — a child
   casting a shadow N px past its own box, screenshotted at 1280 and the
   coloured pixels counted, by the session that owns the hug rules:

     overhang     unhugged edge        hugged edge
       8px        survives             survives
      12px        survives             survives
      16px        survives             cut back to 12px
      24px        survives             cut back to 12px
      40px        partly cut           gone entirely

   So the clearance IS the wrapper's padding, and the hug is what sets the
   floor: 38.4px unhugged at 1280, 12px on a hugged edge. A focus ring (3px)
   and an ordinary card shadow sit comfortably inside both.

   THE RULE TO REMEMBER: on a hugged edge a child may draw up to 12px outside
   itself and no further. That is the tightest case in the system. What it
   would not survive is a block that deliberately hangs something out of its
   own top or bottom — a pull-quote breaking the band, an image overlapping
   the join. No block does that today. If one is ever written, it and this
   rule cannot both be right, and this is the note that says so.

   THE EDITOR PAYS THIS PRICE TOO, and needs one line to avoid it. The kit
   marks a block by putting `cb-spot` on the element carrying `data-block` —
   which on a dressed block is this wrapper — and draws with `outline-offset:
   5px`, i.e. OUTSIDE the box, where this clip removes the top and bottom and
   leaves a half-drawn outline. The companion rule lives in editor-client.css:

     .blk[data-bg]:not([data-bg="paper"]).cb-spot { outline-offset: -5px }

   Five pixels inward, against 12px of clearance in the worst case. Widening
   the clip instead is the wrong trade: it is the only thing keeping one
   ground off the band above it.

   THIS RULE NEEDS NO PAGE-LEVEL CLIP. Nothing here moves and ink overflow is
   not scrollable, so the `overflow-x: clip` pair near the top of this file is
   NOT held open by the dress — see the note there for what does still need it.

   KNOWN LIMIT, ACCEPTED (owner's call, 2026-09-01): a box-shadow is a solid
   colour. All four grounds are solid today. A textured or gradient ground
   would need a different mechanism and this decision would be revisited.

   PAPER IS EXCLUDED because paper paints nothing.

   STILL NOT UNDER A SIDE NAV, AND THIS SPECIAL CASE IS NOT REDUNDANT. It was
   put to the test rather than reasoned about, because the reasoning went the
   other way — the expectation was that paint spreading from an element beside
   a side nav would simply start at the element. It does not. A spread reaches
   outward on EVERY side, and the clip above deliberately keeps the whole
   horizontal spread, so the ground runs left straight under the navigation.

   Measured at 1280, side-nav layout, with this rule removed:

     .nav-side occupies ........................  56.5 → 296.5
     the block's border box starts at ..........  328.5
     the spread reaches left to ................ -951.5   (328.5 − 1280)

   which is the whole nav and most of the screen beyond it. `.layout-side` is
   a flex row, so "the whole horizontal space" there is the article's and not
   the screen's; the ground stays on the wrapper, where it already was. */
.blk[data-bg]:not([data-bg="paper"]) {
  box-shadow: 0 0 0 100vmax var(--blk-ground);
  clip-path: inset(0 -100vmax);
}
.layout-side .blk[data-bg]:not([data-bg="paper"]) {
  box-shadow: none;
  clip-path: none;
}

.blk[data-bg="paper"] {
  --blk-ground: var(--color-bg);
  --blk-ink:    var(--color-text);
  --blk-line:   color-mix(in srgb, var(--color-text) 14%, transparent);
  --blk-deep:   var(--color-primary);
  --blk-accent: var(--color-accent);
  --blk-quiet:  color-mix(in srgb, var(--color-text) 62%, var(--color-bg));
}
.blk[data-bg="mist"] {
  --blk-ground: color-mix(in srgb, var(--color-text) 5%, var(--color-bg));
  --blk-ink:    var(--color-text);
  --blk-line:   color-mix(in srgb, var(--color-text) 16%, transparent);
  --blk-deep:   var(--color-primary);
  --blk-accent: var(--color-accent);
  --blk-quiet:  color-mix(in srgb, var(--color-text) 62%, var(--color-bg));
}
/* Sand: the accent BECOMES the ground, so the accent itself moves to the
   primary — an accent-on-accent link is the one combination this ground
   could produce and it is the one it must not. */
.blk[data-bg="sand"] {
  --blk-ground: color-mix(in srgb, var(--color-accent) 38%, var(--color-bg));
  --blk-ink:    var(--color-text);
  --blk-line:   color-mix(in srgb, var(--color-text) 24%, transparent);
  --blk-deep:   var(--color-primary);
  --blk-accent: var(--color-primary);
  --blk-quiet:  color-mix(in srgb, var(--color-text) 68%, var(--color-accent));
}
/* Navy: a full inversion, and the accent LIGHTENS. The house accent on the
   house primary is around 2.4:1 — well under the 4.5:1 body-text floor — so
   it is mixed toward the page until it clears it. Mixing rather than naming
   a second gold keeps a site that changed its accent honest. */
.blk[data-bg="navy"] {
  --blk-ground: var(--color-primary);
  --blk-ink:    var(--color-bg);
  --blk-line:   color-mix(in srgb, var(--color-bg) 25%, transparent);
  --blk-deep:   var(--color-bg);
  --blk-accent: color-mix(in srgb, var(--color-accent) 72%, var(--color-bg));
  --blk-quiet:  color-mix(in srgb, var(--color-bg) 70%, var(--color-primary));
}

/* the header itself.

   NO RULE ACROSS THE TOP (Cam, 2026-08-31). The eyebrow and a bare heading
   each used to carry `border-top: 2px solid` — a deliberate house mark, but
   one that appeared on EVERY dressed block, which since the dress shipped is
   every block the "+" drops: "I didn't think that that blue line across the
   top was actually a real thing ... I don't really like that blue line across
   the top, and it seems like it appears on almost all blocks."
   The header is spaced by its own margin now and nothing is drawn. */
.blk-head { margin: 0 0 1.5rem; }
.blk-eyebrow {
  font-family: var(--font-ui, var(--font-body));
  font-size: .72rem; font-weight: 700; letter-spacing: .16em;
  text-transform: uppercase; color: var(--blk-quiet, var(--color-text));
  margin: 0 0 .5rem;
}
.blk-head h2 { margin: 0; color: var(--blk-deep, var(--heading-color)); }
.blk-lede {
  margin: .6rem 0 0; max-width: 38em;
  color: var(--blk-quiet, var(--color-text));
  font-size: 1.08rem;
}

/* What a ground reaches. Listed together rather than smeared through the
   blocks so the mapping can be read in one place — and so a block written
   after this inherits the whole system by using the properties. */
.blk[data-bg] a { color: var(--blk-deep); }
.blk[data-bg] a:hover { color: var(--blk-accent); }
.blk[data-bg] h2, .blk[data-bg] h3, .blk[data-bg] h4 { color: var(--blk-deep); }
.blk[data-bg] hr { border-color: var(--blk-line); }
.blk[data-bg] :focus-visible { outline-color: var(--blk-accent); }

/* ─────────────────────────────────────────────────────────────────────
   Content components — THE BLOCKS. Each one owns its own rules, which are
   injected here at build time from platform/blocks/<name>/block.css.

   Injected AT THIS POINT and not appended to the end, deliberately: these
   rules used to live here, and several of them share specificity with rules
   further down (.fig img, .page-body img). Moving them would change which one
   wins without changing a single declaration — the kind of difference that
   shows up as one wrong margin on one page, months later.

   Catalogue and house rules: platform/COMPONENTS.md
   ───────────────────────────────────────────────────────────────────── */
/* ── block: accordion (house) ─────────────────────── */
/* Accordion — native <details>/<summary>, so it works with no JavaScript,
   with the keyboard, with find-in-page, and on paper. */
.accordion { margin-block: var(--space-block); border-top: 1px solid var(--color-rule, #ddd); }
.accordion-item { border-bottom: 1px solid var(--color-rule, #ddd); }
.accordion-label {
  cursor: pointer; padding: .85rem 2rem .85rem 0; position: relative;
  font-weight: 600; line-height: 1.4; list-style: none;
}
.accordion-label::-webkit-details-marker { display: none; }
.accordion-label::after {                    /* own marker, so it can rotate */
  content: ""; position: absolute; right: .4rem; top: 1.25rem;
  width: .5rem; height: .5rem; border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor; transform: rotate(45deg);
  transition: transform .18s ease;
}
.accordion-item[open] > .accordion-label::after { transform: rotate(-135deg); }
.accordion-label:hover { color: var(--color-accent); }
.accordion-body { padding: 0 0 1rem; }
.accordion-body > :first-child { margin-top: 0; }
.accordion-body > :last-child { margin-bottom: 0; }
@media (prefers-reduced-motion: reduce) { .accordion-label::after { transition: none; } }
@media print { .accordion-item > .accordion-body { display: block !important; } }

/* ── block: attachments (house) ─────────────────────── */
/* Attachments — the files and addresses hanging off one record.

   Built as rows rather than as prose links because a census set is thirty-two
   of them and a wall of underlined blue sentences is unreadable at that length.
   Each row is one tap target the full width of the list, which is also what
   makes it usable on a phone. */
.attachments { margin-block: var(--space-block); }
.attachments-title {
  font-family: var(--font-body); font-size: .72rem; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; opacity: .6;
  margin: 0 0 .5rem; color: inherit;
}
.att-list {
  list-style: none; margin: 0; padding: 0;
  border: 1px solid color-mix(in srgb, var(--color-text) 14%, transparent);
  border-radius: var(--radius); overflow: hidden;
}
.att-item + .att-item {
  border-top: 1px solid color-mix(in srgb, var(--color-text) 12%, transparent);
}
.att-inner {
  display: flex; align-items: center; gap: .65rem;
  padding: .55rem .8rem; text-decoration: none; color: var(--link-color);
  font-size: .95rem;
}
a.att-inner:hover {
  background: color-mix(in srgb, var(--color-accent) 10%, transparent);
  color: var(--link-hover-color);
}
a.att-inner:focus-visible {
  outline: var(--focus-width) solid var(--focus-color); outline-offset: -3px;
}
.att-label { flex: 1; min-width: 0; }
a.att-inner .att-label { text-decoration: underline; text-underline-offset: .14em; }

/* The format sits BEFORE the label, at a fixed width, so thirty-two rows read
   as a column rather than as thirty-two different-length sentences. */
.att-format {
  flex: none; min-width: 2.8rem; text-align: center;
  font-family: var(--font-body); font-size: .62rem; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase;
  padding: .1rem .3rem; border-radius: 3px; color: inherit; opacity: .65;
  border: 1px solid color-mix(in srgb, var(--color-text) 22%, transparent);
}
.att-note {
  flex: none; display: inline-flex; align-items: center; gap: .25rem;
  font-family: var(--font-body); font-size: .72rem; opacity: .6; color: var(--color-text);
}
.att-out {
  width: .7rem; height: .7rem; fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}

/* A part the museum holds but has not published. Named, not dropped — the
   record that it exists is the thing worth keeping. */
.att-item.is-unlinked .att-inner { color: var(--color-text); opacity: .6; }
.att-item.is-unlinked .att-label { text-decoration: none; }

.att-count {
  margin: .4rem 0 0; font-family: var(--font-body);
  font-size: .75rem; opacity: .6;
}

/* ── block: collection (house) ─────────────────────── */
/* This block has no rules of its own, on purpose.

   It draws a grid of records — and so does every content type's own listing
   page, through the same function, producing the same markup. Those rules
   therefore belong to the platform rather than to this block, and live in
   core.css under "Record lists". Kept as a pointer rather than deleted, because
   an absent block.css reads as "nobody has styled this yet" and the next person
   would style it a second time.

   The distinction worth keeping visible: a `people` card IS the content —
   everything about a trustee fits on the card. A record card POINTS AT content:
   each one is a doorway, so it is a link, it carries a summary, and it looks
   clickable. A type declared `detail: false` renders cards that are NOT links,
   and they deliberately lose the hover and the underline, because nothing is
   worse than a card that looks clickable and is not. */

/* ── block: docs (house) ─────────────────────── */
/* Documents — a list of things to download.

   The type and size sit AFTER the link text, outside the link, so the link's
   accessible name is the document's name and not "1850 census PDF 2.4 MB". The
   new-tab mark the build adds lands on the link where it belongs. */
.docs { margin-block: var(--space-block); }
.docs-label {
  margin: 0 0 .5rem; font-weight: 700; font-size: .8rem;
  text-transform: uppercase; letter-spacing: .05em;
}
.docs-list {
  list-style: none; margin: 0; padding: 0;
  border-top: 1px solid var(--color-rule, #ddd);
}
.doc {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .35rem .75rem;
  padding: .6rem 0;
  border-bottom: 1px solid var(--color-rule, #ddd);
}
.doc-link { flex: 1 1 14rem; min-width: 0; }
.doc-meta {
  display: inline-flex; align-items: baseline; gap: .5rem;
  font-size: .82rem; opacity: .8; white-space: nowrap;
}
.doc-kind {
  font-weight: 700; letter-spacing: .04em;
  border: 1px solid var(--color-rule, #ccc); border-radius: 3px;
  padding: .05rem .3rem; font-size: .72rem;
}
.doc-size { font-variant-numeric: tabular-nums; }

/* ── block: feature (house) ─────────────────────── */
/* Feature — a short piece of text with its pictures beside it, in a block the
   eye reads as one thing. The shape a museum reaches for after the gallery:
   one exhibit, one object, one programme.

   Two columns on a wide screen, stacked on a narrow one. The pictures column
   is given a fixed share rather than sized to the images, so a row of exhibits
   lines up down the page instead of each one setting its own margin. The
   standing rule that content images are hard blocks still holds — text never
   wraps around a picture here, it sits beside it in its own column. */
.feature {
  display: grid; gap: 1.25rem 1.5rem; align-items: start;
  grid-template-columns: minmax(0, 1fr) minmax(0, 18rem);
  margin-block: var(--space-block); padding: 1.1rem 1.25rem;
  border: 1px solid var(--color-rule, rgba(0, 0, 0, .16));
  border-radius: 4px;
}
.feature-left { grid-template-columns: minmax(0, 18rem) minmax(0, 1fr); }
.feature-left .feature-body { order: 2; }
.feature-left .feature-media { order: 1; }
.feature-plain { grid-template-columns: minmax(0, 1fr); }
.feature-body > :first-child { margin-top: 0; }
.feature-body > :last-child { margin-bottom: 0; }
.feature-media { display: grid; gap: .75rem; }
.feature-media img {
  width: 100%; height: auto; border-radius: 3px; display: block;
}
/* One picture beside a long passage should not be stretched to match it. */
.feature-media > :last-child { margin-bottom: 0; }

@media (max-width: 46rem) {
  .feature, .feature-left { grid-template-columns: minmax(0, 1fr); }
  .feature-left .feature-body, .feature-left .feature-media { order: 0; }
  .feature-media {
    grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  }
}

/* ── block: finder (house) ─────────────────────── */
/* Finder — a search box, a rail of counted facets, and the type's own grid.

   NOT ONE COLOUR OF ITS OWN. Everything below reads the site's tokens, so a
   finder on a navy-and-gold historical society and a finder on a slate-and-teal
   art museum are the same code and look like they belong to their own sites.
   The one thing it introduces is `--finder-rail`, a hair of tint for the rail,
   mixed from the site's own text colour.

   THE CONTROLS ARE HIDDEN UNTIL THE SCRIPT RUNS. See block.js — a rail of
   checkboxes that tick and do nothing is worse than a plain complete list. */
.finder {
  --finder-rail: color-mix(in srgb, var(--color-text) 4%, transparent);
  --finder-line: color-mix(in srgb, var(--color-text) 14%, transparent);
  /* The search box reads as a THING TO TYPE IN rather than as more page
     (Eric, 2026-08-12). It was --color-bg, so on a site whose page colour is
     already a warm cream the box was the same colour as everything around it
     and only its hairline border said it was a field at all.

     Mixed from the site's own accent rather than named as a tan, so this stays
     the rule the top of this file states: a finder on a navy-and-gold society
     and a finder on a slate-and-teal museum are the same code. Tuckerton's
     accent is its brand gold, so 10% of it over the cream IS the lighter tan;
     a cooler palette gets the same lift in its own colour. */
  --finder-field-bg: color-mix(in srgb, var(--color-accent) 10%, var(--color-bg));
  margin-block: var(--space-block);
}

/* ── A FINDER TAKES THE WHOLE PAGE COLUMN (Eric, 2026-08-07) ──────────
   "Page body class limits the width… but for the actual archive content
    that should go full width. It doesn't need to be artificially narrowed."

   `.page` is capped at --text-width (46rem ≈ 735px) because PROSE wants a
   short line — nobody reads a 1150px paragraph. A facet rail beside a card
   grid is not prose, and squeezed into a reading column it gets a 15rem rail
   and one-and-a-bit cards beside it, which is the whole layout wasted.

   Done here, in the block's own stylesheet, rather than by widening the page
   layout or adding a frontmatter flag. `:has()` means the rule can only fire
   on a page that actually contains a finder, so no page anywhere changes
   because this file exists — and a page that later drops its finder goes back
   to reading width on its own, with nobody having to remember to unset a flag.

   Everything ELSE on the page stays at reading width: the title, the intro
   paragraphs, the new-content banner. Only the finder stretches. */
.page:has(> .page-body > .finder) { max-width: none; }
.page:has(> .page-body > .finder) > :not(.page-body),
.page:has(> .page-body > .finder) > .page-body > :not(.finder) {
  max-width: var(--text-width);
}
.finder-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

/* No script: the controls never appear, and the results take the full width. */
.finder-controls { display: none; }
.finder.is-live { display: grid; gap: var(--space-s) 2rem; align-items: start;
  grid-template-columns: minmax(0, 15rem) minmax(0, 1fr); }
.finder.is-live .finder-controls { display: block; position: sticky; top: 1rem; }
/* The two columns start level (Eric, 2026-08-12). The card grid carries a top
   margin of its own — right when it follows prose, wrong here, where it made
   the first row of cards sit 30px below the search box beside it and the whole
   layout look dropped. Only inside a live finder, and only on the first thing
   in the column: without the script the grid follows the page text with no
   search box beside it, and there the margin is doing its job. */
.finder.is-live .finder-results > :first-child { margin-top: 0; }
@media (max-width: 55rem) {
  .finder.is-live { grid-template-columns: minmax(0, 1fr); }
  .finder.is-live .finder-controls { position: static; }
}

/* ── search ─────────────────────────────────────────────────────────── */
.finder-q { position: relative; display: block; }
.finder-q input {
  width: 100%; font: inherit; font-size: 1rem;
  padding: .6rem .75rem .6rem 2.35rem;
  border: 1px solid var(--finder-line); border-radius: var(--radius);
  background: var(--finder-field-bg); color: inherit;
}
.finder-q input:focus-visible {
  outline: var(--focus-width) solid var(--focus-color); outline-offset: 1px;
}
.finder-q-icon {
  position: absolute; left: .7rem; top: 50%; transform: translateY(-50%);
  width: 1.05rem; height: 1.05rem; pointer-events: none;
  fill: none; stroke: currentColor; stroke-width: 1.7; opacity: .55;
}

/* ── the count line and the sort control ────────────────────────────── */
.finder-toolbar {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: .75rem; flex-wrap: wrap; margin: .85rem 0 .5rem;
}
.finder-count { margin: 0; font-size: .92rem; opacity: .8; }
.finder-count b { font-size: 1.15rem; opacity: 1; }
.finder-sort select {
  font: inherit; font-size: .85rem; padding: .35rem .5rem;
  border: 1px solid var(--finder-line); border-radius: var(--radius);
  background: var(--color-bg); color: inherit;
}

/* ── active-filter chips ────────────────────────────────────────────── */
.finder-chips { display: flex; flex-wrap: wrap; gap: .35rem; margin: 0 0 .9rem; }
.finder-chip {
  display: inline-flex; align-items: center; gap: .4rem; cursor: pointer;
  font: inherit; font-size: .82rem; line-height: 1.3;
  padding: .2rem .5rem .2rem .6rem; border-radius: 2rem;
  border: 1px solid color-mix(in srgb, var(--color-accent) 45%, transparent);
  background: color-mix(in srgb, var(--color-accent) 12%, var(--color-bg));
  color: inherit;
}
.finder-chip span[aria-hidden] { font-size: 1.05em; line-height: 1; opacity: .7; }
.finder-chip:hover { border-color: var(--color-accent); }
.finder-chip:focus-visible {
  outline: var(--focus-width) solid var(--focus-color); outline-offset: 2px;
}
.finder-chip.is-clear {
  background: transparent; border-color: var(--finder-line); opacity: .8;
}

/* ── the facet rail ─────────────────────────────────────────────────── */
.finder-rail {
  background: var(--finder-rail); border: 1px solid var(--finder-line);
  border-radius: var(--radius); padding: .35rem .85rem .85rem;
}
.finder-rail-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: .6rem;
}
.finder-rail-title {
  font-family: var(--font-body); font-size: .7rem; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; opacity: .6;
  margin: .75rem 0 .25rem; color: inherit;
}
/* Open or close every group at once. Quiet on purpose — it is a convenience
   beside the heading, not one of the choices the rail is offering. */
.facet-all {
  background: none; border: 0; cursor: pointer; padding: .1rem 0;
  font: inherit; font-size: .75rem; color: var(--link-color);
  text-decoration: underline; text-underline-offset: .15em; white-space: nowrap;
}
.facet-all:focus-visible {
  outline: var(--focus-width) solid var(--focus-color); outline-offset: 2px;
}
/* A group is separated from the NEXT group by more space than anything inside
   it (Eric, 2026-08-12). There was no bottom padding at all, so the last thing
   in a group — usually "Show fewer" — sat against the next group's rule and
   read as belonging to the group below it rather than to its own. Above the
   button is now the tighter gap of the two; see .facet-more. */
.facet { border: 0; margin: 0; padding: .55rem 0 .75rem; border-top: 1px solid var(--finder-line); }
.facet:first-of-type { border-top: 0; }
/* The rail has its own bottom padding; doubling it leaves a hole under the
   last group. */
.facet:last-of-type { padding-bottom: .1rem; }
.facet legend { padding: 0; width: 100%; }
.facet-head {
  width: 100%; display: flex; align-items: center; justify-content: space-between;
  gap: .5rem; cursor: pointer; background: none; border: 0; padding: .1rem 0 .4rem;
  font-family: var(--font-body); font-size: .72rem; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; opacity: .65; color: inherit;
}
.facet-head:hover { opacity: 1; }
.facet-head:focus-visible {
  outline: var(--focus-width) solid var(--focus-color); outline-offset: 2px;
}
.facet-chev {
  width: .85rem; height: .85rem; flex: none; fill: none;
  stroke: currentColor; stroke-width: 2.2; transition: transform .15s;
}
.facet.is-closed .facet-chev { transform: rotate(-90deg); }
.facet.is-closed .facet-body { display: none; }

.facet-opt {
  display: flex; align-items: center; gap: .5rem; cursor: pointer;
  font-size: .88rem; padding: .16rem 0; line-height: 1.35;
}
.facet-opt input { flex: none; width: 1rem; height: 1rem; accent-color: var(--color-primary); }
.facet-opt input:focus-visible {
  outline: var(--focus-width) solid var(--focus-color); outline-offset: 2px;
}
.facet-ico {
  width: 1.05rem; height: 1.05rem; flex: none; fill: none;
  stroke: currentColor; stroke-width: 1.6; stroke-linecap: round;
  stroke-linejoin: round; opacity: .7;
}
.facet-lbl { flex: 1; min-width: 0; }
.facet-n { font-size: .78rem; opacity: .55; font-variant-numeric: tabular-nums; }
/* An option nothing can match is dimmed rather than removed: a rail that drops
   the kinds this museum happens not to have stops describing the collection. */
.facet-opt.is-empty { opacity: .4; cursor: default; }

/* A long facet (places, surnames) shows a handful until asked. Four, since
   2026-08-10 — see the note beside `many` in render.js. The two numbers must
   move together: the label on the button counts the OPTIONS, and this decides
   how many of them are on screen, so a mismatch reads as a button that does
   nothing for the first two clicks. */
.facet.is-long:not(.is-open) .facet-opt:nth-of-type(n + 5) { display: none; }
.facet-more {
  /* Tight to the options it belongs to. The breathing room goes UNDERNEATH,
     on .facet — that is the pairing that says which group this button is part
     of, and it used to be the wrong way round. */
  background: none; border: 0; cursor: pointer; padding: .1rem 0 0;
  font: inherit; font-size: .8rem; color: var(--link-color);
  text-decoration: underline; text-underline-offset: .15em;
}
.facet-more:focus-visible {
  outline: var(--focus-width) solid var(--focus-color); outline-offset: 2px;
}

/* ── results ────────────────────────────────────────────────────────── */
.finder-empty {
  border: 1px dashed var(--finder-line); border-radius: var(--radius);
  padding: 2.5rem 1.5rem; text-align: center; opacity: .75;
}

/* ── "Show more" ─────────────────────────────────────────────────────────
   Both children carry `hidden` in the built HTML and are revealed by the
   script, so a visitor with no JavaScript — who already has every record on
   screen — is never offered a button that would do nothing.

   A real button, not a link: it changes what is on this page and goes nowhere,
   and a link that does not navigate is the wrong promise to a screen reader.
   Wide and unmissable, because a research archive that stops after sixty
   entries with a quiet grey link underneath reads as an archive of sixty. */
.finder-more { margin-top: var(--space-m); text-align: center; }
.finder-showing {
  margin: 0 0 .6rem; font-size: .82rem; font-family: var(--font-body);
  opacity: .7;
}
.finder-more-btn {
  font: inherit; cursor: pointer; width: 100%; max-width: 22rem;
  padding: .7rem 1.5rem; border-radius: var(--radius);
  color: var(--color-primary); background: var(--color-bg);
  border: 1px solid color-mix(in srgb, var(--color-primary) 35%, transparent);
}
.finder-more-btn:hover {
  background: color-mix(in srgb, var(--color-primary) 8%, var(--color-bg));
  border-color: color-mix(in srgb, var(--color-primary) 55%, transparent);
}
.finder-more-btn:focus-visible {
  outline: var(--focus-width) solid var(--focus-color); outline-offset: 2px;
}
/* A card given focus by the button press must not draw a permanent outline —
   it is being focused programmatically so the keyboard does not lose its
   place, and :focus-visible already covers the case where that matters. */
.citem:focus { outline: none; }
.citem:focus-visible {
  outline: var(--focus-width) solid var(--focus-color); outline-offset: 2px;
}

/* ── HIDING A CARD MUST WIN. Fixed 2026-08-08 ───────────────────────────
   The `hidden` attribute is how BOTH this block and the platform's own
   `filter.js` take a record out of a filtered list, and it works by the user
   agent's `[hidden] { display: none }` — the weakest rule in the cascade. Any
   author rule that gives `.citem` a `display` beats it, and the record stays
   on screen while the counts, the chips and the address bar all say it went.
   Nothing errors; the filters simply appear to do nothing.

   `.collection-cards .citem { display: flex }` below did exactly that here, and
   core.css has done it to `.collection-list` and `.collection-media` since
   before the finder existed — so this rule is the general fix, not a local one.
   `!important` is right for once: an element asked to be hidden is hidden, and
   no future layout rule should be able to take that back by accident. */
.citem[hidden] { display: none !important; }

/* ── the card additions the finder brings ───────────────────────────── */
/* The icon tile. Drawn on the card rather than only in the rail, because the
   kind is the first thing that tells a visitor whether a result is a document
   they can read now or a website they are about to be sent to. */
.citem-icon {
  display: inline-grid; place-items: center; width: 2.25rem; height: 2.25rem;
  border-radius: var(--radius); margin-bottom: .5rem;
  border: 1px solid color-mix(in srgb, var(--color-accent) 35%, transparent);
  background: color-mix(in srgb, var(--color-accent) 12%, var(--color-bg));
}
.citem-icon svg {
  width: 1.25rem; height: 1.25rem; fill: none; stroke: var(--color-accent);
  stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round;
}
.citem-kind {
  display: block; font-family: var(--font-body); font-size: .68rem;
  font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  color: var(--color-accent); margin-bottom: .1rem;
}
/* The kind label reads BEFORE the title, which is the order a person scans and
   the reverse of the source order — written after the link so that the link
   wraps only the picture and the title, and a screen reader announces the
   record once. */
.collection-cards .citem { display: flex; flex-direction: column; }
.collection-cards .citem .citem-kind { order: -1; }
.collection-cards .citem .citem-icon { order: -2; }

/* THE STRIP SITS ON THE BOTTOM EDGE (Eric, 2026-08-08). Cards in a row are
   already the same height — the grid stretches them — so a strip that simply
   follows the summary lands at a different height in every card, and the rules
   above them make a ragged staircase across the row. `margin-top: auto` in a
   flex column eats the leftover space above the strip instead, so the summary
   keeps whatever room it needs and every rule in the row lines up. The summary
   keeps a real bottom margin so the rule never jams against the last line in a
   card whose text happens to fill it exactly. */
.collection-cards .citem-summary { margin-bottom: .55rem; }
.collection-cards .citem-facts { margin-top: auto; }
.citem-facts {
  margin: .55rem 0 0; padding-top: .5rem; font-size: .78rem;
  font-family: var(--font-body); opacity: .75;
  border-top: 1px solid var(--finder-line, color-mix(in srgb, var(--color-text) 14%, transparent));
  display: flex; flex-wrap: wrap; align-items: center; gap: .3rem;
}
.citem-facts i { font-style: normal; opacity: .45; }
.citem-status {
  font-size: .68rem; letter-spacing: .02em; padding: .08rem .4rem;
  border-radius: 2rem; border: 1px solid; margin-left: auto; opacity: 1;
}
/* Three tones and no more. A pill per possible value would be a legend nobody
   reads; these say "you can have it", "it is somewhere else", "you cannot". */
.citem-status[data-tone="good"] { color: #2f6b3d; border-color: #b9d3be; background: #f1f7f2; }
.citem-status[data-tone="info"] { color: var(--color-primary); border-color: color-mix(in srgb, var(--color-primary) 30%, transparent); background: color-mix(in srgb, var(--color-primary) 7%, var(--color-bg)); }
.citem-status[data-tone="warn"] { color: #8d2f26; border-color: #e0c0bc; background: #fbf1ef; }

@media (prefers-color-scheme: dark) {
  .citem-status[data-tone="good"] { color: #9ed4ab; border-color: #2f6b3d; background: transparent; }
  .citem-status[data-tone="warn"] { color: #e8a79f; border-color: #8d2f26; background: transparent; }
}

/* The list layout drops the icon tile — a column of 2.25rem squares down the
   left of a dense list is decoration competing with the titles. */
.collection-list .citem-icon { display: none; }
.collection-list .citem-kind { display: inline; margin-right: .5rem; }

/* ── the offloaded finder ─────────────────────────────────────────────
   A collection above its `finder_offload` threshold fetches its records
   instead of carrying them. Two states exist here that the DOM finder has no
   need for, and nothing else about the block changes. */

/* Fetching. Deliberately mild: the cards already on screen are still the right
   cards until the new ones arrive, so this is a hint that something is
   happening, not a curtain over the page. A spinner here would flash on every
   keystroke in the search box. */
.finder.is-busy .finder-results { opacity: .55; transition: opacity .18s ease; }
.finder.is-busy .finder-results { cursor: progress; }
@media (prefers-reduced-motion: reduce) {
  .finder.is-busy .finder-results { transition: none; }
}

/* What a visitor with no JavaScript is told, in place of the rest of a
   collection the script would have fetched. Styled as a real notice rather
   than a footnote, because without it the page claims a twenty-thousand
   record archive holds twenty-four things. */
.finder-noscript {
  margin: var(--space-m) 0 0;
  padding: .75rem 1rem;
  font-family: var(--font-body);
  font-size: .9rem;
  border: 1px solid var(--finder-line, color-mix(in srgb, var(--color-text) 14%, transparent));
  border-radius: var(--radius);
  background: var(--finder-rail, color-mix(in srgb, var(--color-text) 3%, var(--color-bg)));
}
.finder-noscript a { color: var(--link-color); }

/* ── block: gallery (house) ─────────────────────── */
/* Gallery — a responsive grid, never an auto-advancing carousel.

   Column counts come in as --cols-l / --cols-m / --cols-s on the element
   itself, so any number works and one gallery can differ from another without
   a new rule here. Two shapes:

     .gallery-grid     equal cells, pictures cropped to a common ratio. Reads
                       as a tidy contact sheet; right for most things.
     .gallery-masonry  pictures keep their own proportions and pack by height.
                       Right when the shapes vary a lot and cropping would
                       cut the subject out of half of them.

   Masonry is CSS multi-column, not a grid and not JavaScript: it needs no
   script, cannot get out of step with the page, and degrades to a plain list.
   The trade-off is real and worth knowing — multi-column orders items DOWN
   each column, not across the rows. For a wall of photographs that is fine.
   For anything where the order carries meaning, use the grid. */
.gallery { list-style: none; margin-block: var(--space-block); padding: 0; }

.gallery-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(var(--cols-l, 3), minmax(0, 1fr));
}
.gallery-masonry {
  column-count: var(--cols-l, 3);
  column-gap: 1rem;
}
.gallery-masonry .gallery-item {
  break-inside: avoid;
  margin-bottom: 1rem;
}

.gallery-item { min-width: 0; }
.gallery-item figure { margin: 0; }
.gallery-item img {
  width: 100%; height: auto; border-radius: 3px; display: block;
}
/* Only the even grid crops to a common shape. In masonry the whole point is
   that each picture keeps its own. */
.gallery-grid .gallery-item img { aspect-ratio: 4 / 3; object-fit: cover; }
.gallery-item figcaption { font-size: .85rem; line-height: 1.4; margin-top: .35rem; opacity: .85; }

/* The enlarge link wraps the picture; it must not add a line box of its own. */
.gallery-item a.lightbox { display: block; }

@media (max-width: 60rem) {
  .gallery-grid { grid-template-columns: repeat(var(--cols-m, 2), minmax(0, 1fr)); }
  .gallery-masonry { column-count: var(--cols-m, 2); }
}
@media (max-width: 34rem) {
  .gallery-grid { grid-template-columns: repeat(var(--cols-s, 1), minmax(0, 1fr)); }
  .gallery-masonry { column-count: var(--cols-s, 1); }
}

/* Older galleries wrote their column count as a class. Kept working rather
   than rewritten, because content on a client site is content. */
.gallery-2 { --cols-l: 2; --cols-m: 2; --cols-s: 1; }
.gallery-3 { --cols-l: 3; --cols-m: 2; --cols-s: 1; }
.gallery-4 { --cols-l: 4; --cols-m: 2; --cols-s: 2; }

/* ── block: html (house) ─────────────────────── */
/* The one-off block owns almost no styling on purpose — whatever is inside it
   brought its own. What it does provide is the two things a one-off should
   never have to remember: it sits in the page's own measure and rhythm like
   every other block, and its pictures obey the standing rule that a content
   image is a hard block rather than something text wraps around. */
.block-html {
  margin-block: var(--space-block);
}
.block-html > :first-child { margin-top: 0; }
.block-html > :last-child { margin-bottom: 0; }

/* Nothing in a one-off may push the page sideways. A stray fixed width is the
   single most common way hand-written markup breaks a phone, and it does it
   silently — the page looks right on the machine it was written on. */
.block-html img,
.block-html iframe,
.block-html video,
.block-html table { max-width: 100%; }
.block-html img { display: block; height: auto; }

/* ── block: pairs (house) ─────────────────────── */
/* Pairs — label and value, the shape hours and admission actually are.
   A <dl>, not a table: there are no column or row headers here, and a table
   would tell a screen reader there were. */
.pairs { margin-block: var(--space-block); }
.pairs-label {
  margin: 0 0 .5rem; font-weight: 700; font-size: .8rem;
  text-transform: uppercase; letter-spacing: .05em;
}
.pairs dl {
  margin: 0;
  display: grid;
  grid-template-columns: minmax(6rem, max-content) minmax(0, 1fr);
  gap: 0;
  border-top: 1px solid var(--color-rule, #ddd);
}
.pairs dt, .pairs dd {
  margin: 0;
  padding: .55rem .9rem .55rem 0;
  border-bottom: 1px solid var(--color-rule, #ddd);
}
.pairs dt { font-weight: 600; }
.pairs dd { padding-right: 0; }

/* Below this a two-column grid squeezes the value into three words a line.
   Stacking each label above its own value keeps the pairing visible, which is
   the entire point of the block. */
@media (max-width: 32rem) {
  .pairs dl { grid-template-columns: minmax(0, 1fr); }
  .pairs dt { padding-bottom: 0; border-bottom: 0; }
  .pairs dd { padding-top: .1rem; }
}

/* ── block: people (house) ─────────────────────── */
/* People — staff, board, trustees.

   THE DESIGN PROBLEM WORTH KNOWING: almost every one of these lists is half
   illustrated. If a card without a photograph leaves a hole where one would be,
   half the board looks like a broken page. So a card with no photo is a
   deliberately different, tighter card — it does not reserve the space. */
.people { margin-block: var(--space-block); }
.people-label {
  margin: 0 0 .75rem; font-weight: 700; font-size: .8rem;
  text-transform: uppercase; letter-spacing: .05em;
}
.people-grid {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: 1.25rem 1rem;
  grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
}
.person { min-width: 0; }
.person-photo {
  width: 100%; height: auto; aspect-ratio: 1 / 1; object-fit: cover;
  border-radius: 3px; display: block; margin-bottom: .5rem;
}
.person-name { font-weight: 700; line-height: 1.3; display: block; }
.person-role { margin: .1rem 0 0; font-size: .9rem; opacity: .85; line-height: 1.35; }
.person-note { margin: .35rem 0 0; font-size: .88rem; line-height: 1.45; opacity: .9; }

/* A person with a page of their own: ONE link around the picture and the name,
   so it is announced once and the whole card head is the target. */
.person-link { display: block; text-decoration: none; color: inherit; }
.person-link .person-name { text-decoration: underline; text-underline-offset: .12em; }
.person-link:hover .person-name { color: var(--color-accent); }
.person-link:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 3px; }

/* Contact icons. Sized in em so they follow the surrounding text, and given a
   real hit area — 2.2rem is close to the 24px minimum target size, on a row
   where the next icon is only a few pixels away. */
.person-contacts {
  margin: .45rem 0 0; display: flex; gap: .15rem; align-items: center;
}
.person-contact {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.2rem; height: 2.2rem; margin-left: -.45rem;
  border-radius: 4px; color: var(--color-primary);
}
.person-contact:first-child { margin-left: -.5rem; }
.person-contact svg { width: 1.05em; height: 1.05em; display: block; }
.person-contact:hover { color: var(--color-accent); background: var(--color-tint, rgba(0, 0, 0, .05)); }
.person-contact:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 1px; }

/* A group with no photographs at all is a name list, and a grid of tall empty
   cards is the wrong shape for it — pack it tighter. */
.people-grid:not(:has(.person-photo)) {
  grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
  gap: .85rem 1rem;
}

/* ── block: picture (house) ─────────────────────── */
/* Picture — one photograph as a section of the page, at a chosen width, with
   the words that belong beside it.

   All of this block's presentation is here, on classes render.js puts on the
   wrapper. The picture in the body stays a plain markdown picture with no
   settings braces of its own, so nothing here can be contradicted by what the
   click-a-picture panel writes. */
.pic { margin-block: var(--space-block); }
.pic-frame { margin: 0; }
.pic-frame img { display: block; width: 100%; height: auto; border-radius: 3px; }
/* markdown-it puts a lone picture in a paragraph of its own. It is the figure
   that does the spacing here, so that paragraph must not add any. */
.pic-frame p { margin: 0; }
.pic-frame figcaption {
  margin-top: .5rem; font-size: .9em;
  color: var(--color-quiet, rgba(0, 0, 0, .62));
}

/* CONTENT WIDTH is the unnamed default: the picture at its own size, never
   wider than the block it sits in. FULL WIDTH fills the block's width, which
   is what `w=full` means for a picture in prose — cssLen() in eleventy-base.js
   turns "full" into width:100%, and this is the same promise kept the same way.
   Neither one moves the element out past the page: full-bleed by margin is the
   idiom that puts sideways scroll on every page whose site has not also
   supplied the root half of the overflow pact (core.css, "THE PAGE DOES NOT
   SCROLL SIDEWAYS", measured at 7px on CAMRO). A block dropped by the picker
   has to be safe on a site that has never heard of that pact. */
.pic-frame { max-width: 100%; }
.pic:not(.pic-full):not(.pic-beside) .pic-frame img { width: auto; max-width: 100%; }

/* THE TWO CROPS MIRROR IMG_SHAPES IN platform/eleventy-base.js (the
   `IMG_SHAPES` const beside imgStyle, around line 2807, which is where a
   picture written into prose gets the same two names).

   THEY ARE DUPLICATED ON PURPOSE AND THE DUPLICATION IS A PACT: imgStyle is a
   closure inside the Eleventy config function and the module exports only
   buildPaths, so it cannot be required from a block — its own test string
   -scrapes the source and evals it rather than importing it. Change one of
   these ratios and you must change the other, or the same "Tall crop" means
   two different shapes depending on whether the picture is in a block or in a
   paragraph. There is no test that can catch that for you. */
.pic-shape-wide .pic-frame img { aspect-ratio: 16 / 9; object-fit: cover; }
.pic-shape-tall .pic-frame img { aspect-ratio: 3 / 4; object-fit: cover; }

/* WITH CONTENT — the words in a column beside the picture.

   The picture column is a fixed share rather than sized to the image, so a run
   of these down a page lines up instead of each one setting its own margin.
   The standing rule that content images are hard blocks still holds: the text
   sits beside the picture in its own column, it never wraps around it. */
.pic-beside {
  display: grid; gap: 1.25rem 1.5rem; align-items: start;
  grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);
}
.pic-beside .pic-frame img { width: 100%; }
.pic-left .pic-frame { order: 1; }
.pic-left .pic-words { order: 2; }
/* The picture is always written first, so putting it on the right is a matter
   of ordering the two columns, not of reordering the page's content. */
.pic-right { grid-template-columns: minmax(0, 1fr) minmax(0, 22rem); }
.pic-right .pic-frame { order: 2; }
.pic-right .pic-words { order: 1; }
.pic-words > :first-child { margin-top: 0; }
.pic-words > :last-child { margin-bottom: 0; }

@media (max-width: 46rem) {
  /* On a phone there is no room for a column of words next to a picture, so it
     goes back to sitting on its own — the same thing the prose panel promises
     in as many words when it asks which side the picture goes. */
  .pic-beside { grid-template-columns: minmax(0, 1fr); }
  .pic-left .pic-frame, .pic-right .pic-frame { order: 0; }
  .pic-left .pic-words, .pic-right .pic-words { order: 1; }
}

/* ── block: recording (house) ─────────────────────── */
/* Recording — a player and its transcript, which are one thing and not two.

   THE BASE STATE IS THE NO-JAVASCRIPT STATE and it is complete: a native
   player, and the whole transcript as plain readable text at full length. The
   panel height, the scrolling and the seek buttons are all added by block.js.
   Nothing here may impose a height or hide a passage, because a box that is
   cut short by CSS and made scrollable by a script that did not run is a
   transcript with words nobody can reach.

   COLOUR IS NEVER THE ONLY SIGNAL. The passage being spoken carries a bar down
   its left edge and a bolder time as well as a tint, so it is still findable
   with a tinted background nobody can see. */

.recording {
  margin: var(--space-block) 0;
  padding: 0;
}

.recording__title {
  font-family: var(--font-heading);
  font-size: 1.15rem;
  line-height: var(--line-heading);
  color: var(--heading-color);
  margin: 0 0 .6rem;
}

/* ── the player ─────────────────────────────────────────────────────── */

.recording__media {
  display: block;
  width: 100%;
  border-radius: var(--radius);
}

.recording--audio .recording__media {
  /* An audio element has no intrinsic height and browsers give it a default
     width that is not the column. */
  height: 54px;
}

.recording--video .recording__media {
  aspect-ratio: 16 / 9;
  background: #000;
}

/* NO STICKY PLAYER, and the reason is worth keeping because the idea is an
   obvious one to have again.

   The player was pinned to the top so that clicking a passage two thousand
   words down would not leave the controls off-screen. It looked right on audio
   — where the bar is 54px — and it was wrong, twice over:

   1. The scrolling transcript panel ALREADY solves that problem. Passages live
      inside a bounded box, so selecting one never moves the page and the
      controls never go anywhere. Sticky was answering a question the panel had
      already answered.
   2. On video it actively broke the block. Pinned, the video held its place
      while the panel slid up UNDERNEATH it, so scrolling a little left a 342px
      video and about ninety pixels of transcript. The player had eaten the
      feature it exists to serve. Measured on a 1180x900 viewport, 2026-08-13.

   Two mechanisms for one problem, and the one that composed badly was the one
   that was not needed. The figure is sized to sit within a screen instead. */

/* Video is capped so that the frame and its transcript can share one screen:
   16:9 across a 46rem column is 414px, which is 46% of a laptop viewport before
   a word of transcript. object-fit lets the picture letterbox inside the cap
   rather than stretch — the black is then the film's own margin. */
/* Capped on WIDTH, not height, and centred. Capping the height of a
   full-width box leaves the picture letterboxed inside it with black down both
   sides — the frame obeys the limit and the wasted space stays on the page.
   Constraining the width to whatever gives 38vh at 16:9 makes the box the size
   of the picture, so the cap costs nothing.

   object-fit still matters underneath: a 4:3 tape inside the 16:9 box letterboxes
   rather than stretching, which is the right answer for old footage. */
.recording--video .recording__media {
  max-width: calc(38vh * 16 / 9);
  max-height: 38vh;
  margin-inline: auto;
  object-fit: contain;
}

/* Short screens — a laptop under three toolbars, a phone in landscape. There
   is no height to share, so let the picture be itself and let the page scroll. */
@media (max-height: 34rem) {
  .recording--video .recording__media { max-height: none; }
}

.recording__nosupport {
  margin: 0;
  padding: .75rem;
  font-size: .95rem;
}

/* ── shared furniture ───────────────────────────────────────────────── */

.recording__legend {
  margin: 1.1rem 0 .4rem;
  font-weight: 700;
  font-size: .8rem;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--color-text);
}

.recording__hint {
  margin: 0 0 .5rem;
  font-size: .85rem;
  /* --abstract-color, not a lighter grey: this is instructions, and
     instructions that are hard to read are instructions nobody follows. */
  color: var(--abstract-color);
}

.recording__t {
  font-variant-numeric: tabular-nums;
  font-size: .85rem;
  white-space: nowrap;
}

/* The seek button: a button that reads as a time, not as a control panel. */
.recording__seek {
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  font-variant-numeric: tabular-nums;
  font-size: .85rem;
  color: var(--link-color);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
  white-space: nowrap;
}

.recording__seek:hover {
  color: var(--link-hover-color);
}

.recording__seek:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: 2px;
  border-radius: 2px;
}

/* ── chapters ───────────────────────────────────────────────────────── */

/* Deliberately tight. Chapters sit between the player and the transcript, so
   every pixel here is a pixel of transcript pushed off the screen — at the
   library's ordinary line-height a seven-chapter list was 200px of the ~560px
   a laptop has left once the video has taken its share. */
.recording__chapters ol {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0;
}

.recording__chapter {
  display: grid;
  grid-template-columns: 4.5rem 1fr;
  gap: .5rem;
  align-items: baseline;
  padding: .1rem .3rem;
  line-height: 1.35;
  font-size: .95rem;
  border-radius: 3px;
}

.recording__chapter.is-seekable { cursor: pointer; }
.recording__chapter.is-seekable:hover {
  background: color-mix(in srgb, var(--color-text) 6%, transparent);
}

/* ── the transcript ─────────────────────────────────────────────────── */

.recording__passages {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: .55rem;
}

/* Only once the script has made it scrollable AND focusable. Never in the
   base state — see the note at the top of this file. */
.recording__transcript.is-panel .recording__passages {
  max-height: min(60vh, 34rem);
  /* the two panes have to add up to less than a screen — see the video cap */
  overflow-y: auto;
  overscroll-behavior-y: contain;
  padding: .25rem .5rem .25rem 0;
  border: 1px solid color-mix(in srgb, var(--color-text) 18%, transparent);
  border-radius: var(--radius);
}

/* Video takes 38vh above it, so its transcript gets the rest of the screen
   rather than the audio player's share of it. 38 + 44 leaves room for the
   caption, the switch and the page's own furniture. */
.recording--video .recording__transcript.is-panel .recording__passages {
  max-height: min(44vh, 30rem);
}

.recording__transcript.is-panel .recording__passages:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: 2px;
}

.recording__passage {
  display: grid;
  grid-template-columns: 4.5rem 1fr;
  gap: .6rem;
  align-items: baseline;
  padding: .3rem .4rem;
  /* The bar that marks the passage being spoken lives here, transparent,
     so nothing moves sideways when it appears. */
  border-left: 3px solid transparent;
  border-radius: 0 3px 3px 0;
}

.recording__passage.is-seekable { cursor: pointer; }

.recording__passage.is-seekable:hover {
  background: color-mix(in srgb, var(--color-text) 6%, transparent);
}

.recording__words { line-height: var(--line-body); }

.recording__speaker {
  font-weight: 700;
  /* The speaker's name reads as a label rather than as emphasis inside the
     sentence, which is what a <b> would otherwise look like mid-paragraph. */
  color: var(--heading-color);
}

/* The passage being spoken. Three signals, only one of them colour:
   the bar, the heavier time, and the tint. */
.recording__passage.is-current {
  background: color-mix(in srgb, var(--color-accent) 12%, var(--color-bg));
  border-left-color: var(--color-accent);
}

.recording__passage.is-current .recording__seek,
.recording__passage.is-current .recording__t {
  font-weight: 700;
}

/* ── what you can see (video's text alternative) ────────────────────── */

/* Visible to everyone, not tucked behind a control. A description of five men
   in uniform in a bright room is worth reading, and hiding it would help
   nobody while making it easy to forget it is there. */
.recording__describes {
  margin: 1rem 0 0;
  padding: .1rem 0 .1rem 1rem;
  border-left: 3px solid color-mix(in srgb, var(--color-text) 20%, transparent);
}

.recording__describes p { margin: .4rem 0; }
.recording__describes .recording__legend { margin-top: .4rem; }

/* ── the follow-along switch ────────────────────────────────────────── */

.recording__follow {
  display: flex;
  align-items: center;
  gap: .4rem;
  margin: 0 0 .5rem;
  font-size: .85rem;
  color: var(--abstract-color);
}

.recording__follow input { margin: 0; }
.recording__follow label { cursor: pointer; }

.recording__follow input:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: 2px;
}

/* ── when something is wrong ────────────────────────────────────────── */

/* Addressed to whoever is building the page. Deliberately plain and
   deliberately not red-on-pink: it has to be readable, and it is information
   rather than an alarm. */
.recording__trouble,
.recording-missing {
  margin: .75rem 0 0;
  padding: .7rem .9rem;
  font-size: .9rem;
  color: var(--color-text);
  background: color-mix(in srgb, var(--color-text) 5%, transparent);
  border-left: 4px solid var(--color-accent);
  border-radius: 0 var(--radius) var(--radius) 0;
}

.recording__download {
  margin: .8rem 0 0;
  font-size: .9rem;
}

/* ── narrow screens ─────────────────────────────────────────────────── */

@media (max-width: 34rem) {
  .recording__passage,
  .recording__chapter {
    grid-template-columns: 3.6rem 1fr;
    gap: .45rem;
  }
}

/* ── reduced motion ─────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  .recording__passages { scroll-behavior: auto; }
}

/* ── print ──────────────────────────────────────────────────────────── */

/* A transcript is a document. Printed, it should be the words — the whole
   panel at full length, with no scrollbar, no player and no controls. This is
   also what "reader mode" and "save as PDF" produce, which is how a researcher
   actually takes an interview away with them. */
@media print {
  .recording__media,
  .recording__follow,
  .recording__hint,
  .recording__download { display: none; }

  .recording__transcript.is-panel .recording__passages {
    max-height: none;
    overflow: visible;
    border: 0;
  }

  .recording__passage {
    break-inside: avoid;
    border-left-color: transparent;
    background: none;
  }

  .recording__seek {
    color: inherit;
    text-decoration: none;
  }
}

/* ── block: section (house) ─────────────────────── */
/* Section — a photo, words and one button, whose arrangement is a setting.

   Every colour here is a token or a ground property (--blk-*, set by the
   dress when a background is chosen), so a section follows the site and
   every ground without naming a colour. Every layout stacks below 46rem, in
   the order it is written — render.js writes each layout's parts in the order
   they are shown, so nothing here reorders anything. The gap around it is the
   spacing system's, like every other block's (check_contract.py). */
.block-section { margin-block: var(--space-block); }
.sec {
  position: relative;
  display: grid; gap: clamp(1.25rem, 3vw, 2.5rem);
  align-items: center;
}
.sec > :first-child { margin-top: 0; }
.sec-words > :first-child, .sec-col > :first-child { margin-top: 0; }
.sec-words > :last-child, .sec-col > :last-child { margin-bottom: 0; }
.sec-media { display: grid; gap: .75rem; }
.sec-media img, .sec-media svg {
  display: block; width: 100%; height: auto;
  border-radius: var(--card-radius);
}
.sec-media p { margin: 0; }

/* the arrangements */
.sec--photo-left  { grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); }
.sec--photo-right { grid-template-columns: minmax(0, 6fr) minmax(0, 5fr); }
.sec--photo-above, .sec--two-photos, .sec--words, .sec--centered { grid-template-columns: minmax(0, 1fr); }
.sec--two-photos .sec-media { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.sec--photo-above .sec-media-many { grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); }
.sec--words .sec-words { max-width: var(--reading-width); }
.sec--centered { justify-items: center; text-align: center; }
.sec--centered .sec-words { max-width: var(--reading-width); }
.sec--centered .sec-media { max-width: 36rem; width: 100%; }
.sec--centered .sec-actions { justify-content: center; }

.sec-cols { display: grid; gap: clamp(1.25rem, 3vw, 2.5rem); align-items: start; }
.sec-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.sec-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.sec-cols-between { grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr) minmax(0, 1fr); align-items: center; }
.sec-foot { grid-column: 1 / -1; }

/* the actions: the site's own .button, then ordinary links.
   The button keeps the button's own ink on a ground: `.blk[data-bg] a`
   (core.css) repaints every link inside a dressed block in the ground's deep
   colour, which on paper is the button's own colour — so the words vanished
   into it. Found placing the first Section on CAMRO. */
.sec a.sec-button, .blk[data-bg] .sec a.sec-button,
.sec a.sec-button:hover, .blk[data-bg] .sec a.sec-button:hover { color: var(--button-color); }
.sec-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: .75rem 1.5rem;
  margin: 1.5rem 0 0;
}
.sec-foot .sec-actions { margin-top: 0; }

/* the sample picture — drawn in the site's colours, labelled as a sample */
.sec-sample-pic { margin: 0; }
.sec-sample-pic svg { aspect-ratio: 16 / 10; }
.sec-sp-ground { fill: color-mix(in srgb, var(--color-primary) 10%, var(--color-bg)); }
.sec-sp-sun    { fill: color-mix(in srgb, var(--color-accent) 70%, var(--color-bg)); }
.sec-sp-far    { fill: color-mix(in srgb, var(--color-primary) 35%, var(--color-bg)); }
.sec-sp-near   { fill: color-mix(in srgb, var(--color-primary) 65%, var(--color-bg)); }
.sec-sp-word {
  fill: var(--color-bg); font: 700 7px/1 var(--font-ui, var(--font-body));
  letter-spacing: .18em;
}

/* DETAILS — finishing touches, each one a switch on the Style tab */

/* Accent line: a short rule above the section's heading (the dress header
   when there is one, otherwise the first heading in the words). */
.blk:has(> .sec--accent) > .blk-head::before,
.sec--accent:not(.blk > .sec) .sec-words > :is(h2, h3):first-child::before {
  content: ""; display: block; width: 3rem; height: 3px; margin-bottom: .9rem;
  background: var(--blk-accent, var(--color-accent));
}
.sec--centered.sec--accent .sec-words > :is(h2, h3):first-child::before,
.blk:has(> .sec--centered.sec--accent) > .blk-head::before { margin-inline: auto; }

/* Borders and dividers: hairlines above and below, and between columns. */
.sec--borders {
  border-block: 1px solid var(--blk-line, var(--color-rule, rgba(0, 0, 0, .16)));
  padding-block: clamp(1.25rem, 3vw, 2rem);
}
.sec--borders .sec-cols > .sec-col + .sec-col,
.sec--borders .sec-cols > .sec-media + .sec-col,
.sec--borders .sec-cols > .sec-col + .sec-media {
  border-inline-start: 1px solid var(--blk-line, var(--color-rule, rgba(0, 0, 0, .16)));
  padding-inline-start: clamp(1.25rem, 3vw, 2.5rem);
}
.sec--borders .sec-media img, .sec--borders .sec-media svg {
  outline: 1px solid var(--blk-line, var(--color-rule, rgba(0, 0, 0, .16)));
  outline-offset: 4px;
}

/* Corner accents: two marks, top left and bottom right, in the accent. */
.sec--corners { padding: clamp(1.25rem, 3vw, 2rem); }
.sec--corners::before, .sec--corners::after {
  content: ""; position: absolute; width: 1.75rem; height: 1.75rem;
  border: 0 solid var(--blk-accent, var(--color-accent)); pointer-events: none;
}
.sec--corners::before { top: 0; left: 0; border-top-width: 3px; border-left-width: 3px; }
.sec--corners::after { bottom: 0; right: 0; border-bottom-width: 3px; border-right-width: 3px; }

@media (max-width: 46rem) {
  .sec, .sec-cols, .sec-cols-2, .sec-cols-3, .sec-cols-between,
  .sec--two-photos .sec-media { grid-template-columns: minmax(0, 1fr); }
  .sec--borders .sec-cols > * + * {
    border-inline-start: 0; padding-inline-start: 0;
    border-top: 1px solid var(--blk-line, var(--color-rule, rgba(0, 0, 0, .16)));
    padding-top: 1.25rem;
  }
}

/* On the dark ground the button is the ground's own colour, so it inverts:
   paper face, primary words — the same pairing .btn-invert uses. */
.blk[data-bg="navy"] .sec a.sec-button { background: var(--color-bg); color: var(--color-primary); }
.blk[data-bg="navy"] .sec a.sec-button:hover { background: var(--blk-accent); color: var(--color-primary); }

/* The section's own heading size (heading_size, Style tab). The dress writes
   the heading, so the size reaches it from the section inside, as the accent
   line does. Same four tokens as h-size-* in core.css; unset is the H2's. */
.blk:has(> .sec--head-small) > .blk-head h2   { font-size: var(--h4); }
.blk:has(> .sec--head-medium) > .blk-head h2  { font-size: var(--h3); }
.blk:has(> .sec--head-large) > .blk-head h2   { font-size: var(--h2); }
.blk:has(> .sec--head-display) > .blk-head h2 { font-size: var(--h1); }

/* ── block: slider (house) ─────────────────────── */
/* Slider — a scrolling row of pictures that a script upgrades with controls.

   The base state IS the no-JavaScript state, and it is usable: a snapping,
   swipeable, keyboard-scrollable row. The controls are added by block.js, so
   there is never a previous/next button that does nothing. */
.slider { margin-block: var(--space-block); }
.slider-label {
  margin: 0 0 .75rem; font-weight: 700; font-size: .8rem;
  text-transform: uppercase; letter-spacing: .05em;
}
.slider-track {
  list-style: none; margin: 0; padding: 0;
  display: flex;
  gap: .75rem;
  overflow-x: auto;
  /* PROXIMITY, NOT MANDATORY, and this is a bug fix rather than a preference.
     With `mandatory`, Chromium cancels a programmatic smooth scroll outright:
     the previous/next buttons updated the dots and the announcement while the
     pictures never moved — the worst kind of broken, because everything said it
     had worked. Slides are full width, so proximity still lands on one every
     time. */
  scroll-snap-type: x proximity;
  overscroll-behavior-x: contain;
}
.slider-track:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 3px; }
.slide {
  flex: 0 0 100%;
  scroll-snap-align: center;
  min-width: 0;
}
.slide figure { margin: 0; }
.slide img {
  width: 100%; height: auto; display: block; border-radius: 3px;
  aspect-ratio: 3 / 2; object-fit: cover;
}
.slide figcaption { font-size: .85rem; line-height: 1.4; margin-top: .35rem; opacity: .85; }

/* Controls, only present once the script has run. */
.slider-controls {
  display: flex; align-items: center; justify-content: center; gap: .75rem;
  margin-top: .6rem;
}
.slider:not(.slider-ready) .slider-controls { display: none; }
.slider-btn {
  border: 1px solid var(--color-rule, #ccc); background: transparent;
  color: var(--color-primary); cursor: pointer;
  width: 2.4rem; height: 2.4rem; border-radius: 50%;
  font-size: 1.4rem; line-height: 1; display: flex;
  align-items: center; justify-content: center;
}
.slider-btn:hover:not(:disabled) { background: var(--color-tint, rgba(0, 0, 0, .05)); }
.slider-btn:disabled { opacity: .35; cursor: default; }
.slider-btn:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 2px; }
.slider-dots { display: flex; gap: .1rem; align-items: center; }
.slider-dot {
  border: 0; background: transparent; cursor: pointer; padding: .5rem .25rem;
  /* The dot is small; the button around it is not. */
  width: 1.6rem; height: 1.9rem; display: flex; align-items: center; justify-content: center;
}
.slider-dot::before {
  content: ""; width: .5rem; height: .5rem; border-radius: 50%;
  background: var(--color-rule, #bbb);
}
.slider-dot.is-current::before { background: var(--color-accent); }
.slider-dot:focus-visible { outline: 3px solid var(--color-accent); outline-offset: -2px; }

/* A rotation that keeps moving while someone reads is the complaint. Under a
   reduced-motion preference the script never starts one; this belt-and-braces
   rule also kills the smooth scroll it would use. */
@media (prefers-reduced-motion: reduce) {
  .slider-track { scroll-behavior: auto; }
}

/* ── block: tabs (house) ─────────────────────── */
/* Tabs.

   The strip is a row of ruled words, not a row of boxes: the current one is
   marked by a rule under it, which is the same move the site's own menu makes
   for the page you are on. Boxes with a lifted corner are the other way to
   draw tabs and they date badly.

   NOTHING HERE APPLIES BEFORE THE SCRIPT RUNS except hiding the strip, which
   the `hidden` attribute in the markup does on its own. The `.tabs--live`
   class is added by the script, so the panel headings are only taken out of
   sight once there is a tab strip standing in for them. */
.tabs { margin-block: var(--space-block); }

.tabs-list {
  display: flex; flex-wrap: wrap; gap: 0 1.5rem;
  border-bottom: 1px solid color-mix(in srgb, var(--color-text) 14%, transparent);
  margin-bottom: 1.4rem;
}
.tabs-tab {
  appearance: none; background: none; border: 0; border-radius: 0;
  cursor: pointer; font: inherit; color: var(--color-text);
  font-family: var(--font-ui, var(--font-body));
  font-size: .82rem; font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase;
  /* the underline is drawn as a box-shadow so it sits ON the list's own rule
     rather than a pixel above or below it, whatever the font's descender */
  padding: .75rem 0; margin-bottom: -1px;
  box-shadow: inset 0 -2px 0 transparent;
  transition: box-shadow .15s ease, color .15s ease;
}
.tabs-tab:hover { color: var(--color-accent); }
.tabs-tab[aria-selected="true"] {
  color: var(--color-primary);
  box-shadow: inset 0 -2px 0 var(--color-primary);
}
.tabs-tab:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 2px; }

.tabs-panel { margin: 0; }
.tabs-panel > :first-child { margin-top: 0; }
.tabs-panel > :last-child { margin-bottom: 0; }
.tabs-panel:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 4px; }
/* Two panels stacked with no strip need telling apart; once the strip is
   there, only one is ever on screen and the gap is the block's own. */
.tabs:not(.tabs--live) .tabs-panel + .tabs-panel { margin-top: 2rem; }

/* Once the strip is live the tab IS the panel's visible label, so the heading
   goes out of sight — and stays in the document, because the page outline and
   the search index are not the tab strip's to take away. */
.tabs--live .tabs-panel-label {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .tabs-tab { transition: none; }
}

/* ── block: text (house) ─────────────────────── */
/* ── {% text %}: a band of prose that dresses itself ─────────────────────
 *
 * The whole point of this block is that the author never thinks about layout,
 * so the layout lives here and nowhere else. Three decisions:
 *
 *   1. WIDTH. The inner column is the site's own reading measure — the same
 *      `--reading-width` an ordinary article page narrows to — centred in the
 *      content column. `--wide` lets a page opt up to the full content width.
 *   2. BREATHING ROOM. A band's worth of space above and below, in the site's
 *      own steps, so the block sits between two designed bands as an equal
 *      and not as a gap-filler. On an ordinary reading page the same values
 *      simply read as generous paragraph spacing.
 *   3. NOTHING ELSE. Type, colour, link dress all inherit — the site's core
 *      prose rules already know how to set an h2 and a paragraph. Repeating
 *      them here is how a block drifts off-brand.
 */
.b-text {
  padding-block: var(--space-l);
}
.b-text__inner {
  max-width: var(--reading-width, 46rem);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.b-text--wide .b-text__inner {
  max-width: var(--content-width);
}
/* The band's first words start at its top edge, not a heading-margin below
   it; same at the bottom. The band owns the spacing. */
.b-text__inner > :first-child { margin-top: 0; }
.b-text__inner > :last-child { margin-bottom: 0; }

/* ── block: timeline (house) ─────────────────────── */
/* Timeline — the date is the anchor, so it gets its own column and the eye can
   run down it. An <ol>: the order is the content, not a presentation choice.

   The rule and the markers are drawn with borders and a pseudo-element, so they
   cost no markup and disappear cleanly when styles are off — where this becomes
   a plain numbered list of "1874 The schoolhouse is built", which is correct.

   HOOKS FOR PER-SITE STYLING (Eric, 2026-08-05). No entrance animation lives
   here; it is a per-site decision. What lives here is everything needed to add
   one from a site stylesheet without touching the platform:

     .timeline-item[data-i]      1-based position — stagger a delay off it
     .timeline-item[data-total]  how many there are
     .timeline-when / -what / -media   the parts, separately addressable
     .timeline-vertical / -horizontal  which way it runs

   e.g.  .timeline-item { opacity: 0; animation: rise .5s forwards; }
         .timeline-item { animation-delay: calc(var(--i, 0) * .08s); }
   with  .timeline-item { --i: attr(data-i number); }  where supported, or a
   handful of nth-child rules where it is not. Wrap any of it in
   `@media (prefers-reduced-motion: no-preference)`. */
.timeline { margin-block: var(--space-block); }
.timeline-label {
  margin: 0 0 .75rem; font-weight: 700; font-size: .8rem;
  text-transform: uppercase; letter-spacing: .05em;
}
.timeline-list { list-style: none; margin: 0; padding: 0; }
.timeline-when { font-weight: 700; font-variant-numeric: tabular-nums; }
.timeline-what { min-width: 0; }
.timeline-media {
  width: 100%; height: auto; border-radius: 3px; display: block;
}

/* ── down the page ─────────────────────────────────────────────────── */
.timeline-vertical .timeline-list {
  padding: 0 0 0 1.15rem;
  border-left: 2px solid var(--color-rule, #ddd);
}
.timeline-vertical .timeline-item {
  position: relative;
  display: grid;
  grid-template-columns: minmax(4.5rem, max-content) minmax(0, 1fr);
  gap: 0 1rem;
  padding: 0 0 .9rem;
}
.timeline-vertical .timeline-item:last-child { padding-bottom: 0; }
.timeline-vertical .timeline-item::before {
  content: ""; position: absolute; left: -1.5rem; top: .5rem;
  width: .5rem; height: .5rem; border-radius: 50%;
  background: var(--color-accent);
}
/* The picture sits under the words, in the words' column. */
.timeline-vertical .timeline-media {
  grid-column: 2; max-width: 16rem; margin-top: .5rem;
}

/* Two columns is a hostile shape for a phone once the date is four characters
   and the event is a sentence — stack, keeping the date as the heading of its
   own entry. */
@media (max-width: 32rem) {
  .timeline-vertical .timeline-item { grid-template-columns: minmax(0, 1fr); }
  .timeline-vertical .timeline-what { margin-top: .1rem; }
  .timeline-vertical .timeline-media { grid-column: 1; }
}

/* ── along the page ────────────────────────────────────────────────── */
/* A scrolling row, not a carousel: everything is present and reachable, it
   just does not all fit at once. The list itself takes focus so it can be
   scrolled with the keyboard, which a plain overflow container cannot. */
.timeline-horizontal .timeline-list {
  display: flex;
  gap: 1.25rem;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding: 1.4rem 0 1rem;
  border-top: 2px solid var(--color-rule, #ddd);
  overscroll-behavior-x: contain;
}
.timeline-horizontal .timeline-item {
  position: relative;
  flex: 0 0 14rem;
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  gap: .3rem;
}
.timeline-horizontal .timeline-item::before {
  content: ""; position: absolute; left: 0; top: -1.65rem;
  width: .5rem; height: .5rem; border-radius: 50%;
  background: var(--color-accent);
}
.timeline-horizontal .timeline-media { max-height: 9rem; object-fit: cover; }
.timeline-horizontal .timeline-list:focus-visible {
  outline: 3px solid var(--color-accent); outline-offset: 3px;
}

/* On a phone a horizontal timeline is a row of cards you can only see one of.
   It becomes the vertical one, which is the shape that works there. */
@media (max-width: 40rem) {
  .timeline-horizontal .timeline-list {
    display: block; overflow-x: visible; border-top: 0;
    padding: 0 0 0 1.15rem; border-left: 2px solid var(--color-rule, #ddd);
  }
  .timeline-horizontal .timeline-item {
    flex: none; padding-bottom: .9rem;
  }
  .timeline-horizontal .timeline-item::before { left: -1.5rem; top: .5rem; }
  .timeline-horizontal .timeline-media { max-width: 16rem; max-height: none; }
}


/* ─────────────────────────────────────────────────────────────────────
   Content patterns — ARRANGEMENT ONLY. Injected here at build time from
   platform/patterns/<name>/pattern.css.

   AFTER the blocks and not before, deliberately. A pattern is applied to
   hand-written HTML that may sit inside or beside a block, and when the two
   ever touch the same property the pattern is the more specific intent —
   somebody wrote that markup for this spot on purpose.

   Why patterns exist beside blocks: platform/patterns/_loader.js
   ───────────────────────────────────────────────────────────────────── */
/* ── pattern: callout (house) ───────────────────── */
/* Callout — an aside the eye can skip, not a shout.

   Ported from the callout BLOCK on 2026-08-08, class names rewritten to the
   pattern contract (p-callout / p-callout__label).

   FIXED 2026-08-10. The port carried the block's rules across unchanged, and
   three of them were only ever legal because a block's stylesheet is not held
   to the pattern contract. `python tools/test_pattern_options.py` had been red
   ever since — a failing test in the repository that nobody read. What was
   wrong, and it was wrong on 19 real pages:

     * a colour written here (the rgba fallback), so a callout stayed the same
       grey whatever the site's design said;
     * `--color-tint`, which this site has never had — a var() pointing at a
       token that does not exist resolves to nothing, so the rule it is in
       silently does not apply and the fallback was doing all the work;
     * a text size of its own for the label;
     * nothing at all changing at 46rem, so the padding on a phone was the
       padding on a desktop.

   The tint is now the same expression the two-column pattern uses, so a
   callout follows the site's primary colour the way every other surface does. */
.p-callout {
  margin-block: var(--space-block);
  padding: .9rem 1.1rem;
  border-left: 4px solid var(--color-accent);
  background: color-mix(in srgb, var(--color-primary) 6%, transparent);
  border-radius: 0 3px 3px 0;
}
.p-callout__label {
  margin: 0 0 .35rem;
  font-weight: 700;
  font-size: var(--caption-size);
  text-transform: uppercase;
  letter-spacing: .05em;
}
.p-callout > :last-child { margin-bottom: 0; }
.p-callout--plain { background: none; border-radius: 0; padding-left: 1rem; }

/* A callout is one column at every width, so there is no stacking to do — what
   the narrow screen needs is its space back. An inset panel costs the same
   2rem of a 375px screen that it costs of a 1400px one, and there are 19 of
   these down one archive record. */
@media (max-width: 46rem) {
  .p-callout { padding: .75rem .85rem; }
  .p-callout--plain { padding-left: .85rem; }
}

/* ── pattern: columns (house) ───────────────────── */
/* Columns — pick a structure, then fill it.

   Elementor's "choose your structure" move, which is the one thing from a page
   builder worth copying: you are not describing a layout in words, you are
   pointing at the shape you want. Agreed 2026-08-08, built 2026-08-10.

   IT REPLACES `two-column`, which is retired. Two across is now one choice in
   here rather than a pattern of its own — Eric: "the two columns is now a moot
   point, because you can just pick two columns out of the structure picker."

   EVERY STRUCTURE IS A COLUMN TEMPLATE AND NOTHING ELSE. The cells flow into
   it in the order they are written, so source order is reading order at every
   width and there is no modifier anywhere below that reverses anything. That is
   the same line `media-text` holds and for the same reason: a CSS reversal
   gives a screen reader, a phone and a printout an order nobody chose.

   Grid rather than flex throughout: cells line up against each other without
   either one having to know how wide the other is, and the stack at 46rem is
   one property rather than a direction switch. */
.p-columns {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.25rem 2rem;
  align-items: start;
  margin-block: var(--space-block);
}

/* ── the structures ─────────────────────────────────────────────────
   Two across is the default and has no class of its own, so the commonest
   shape costs nothing to write and an instance with no structure modifier is
   never ambiguous. */
.p-columns--three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.p-columns--four  { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.p-columns--wide-narrow { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
.p-columns--narrow-wide { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); }

/* One across the top, two beneath it. The full-width cell is named by POSITION
   because that is what the structure means — the first thing you wrote is the
   band across the top. */
.p-columns--one-over-two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.p-columns--one-over-two > .p-columns__cell:first-child { grid-column: 1 / -1; }

/* Two side by side, then one across the bottom. */
.p-columns--two-over-one { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.p-columns--two-over-one > .p-columns__cell:nth-child(3) { grid-column: 1 / -1; }

/* Two full-width bands, stacked. Eric's own addition, and it is not the same as
   writing two paragraphs: the two bands are ONE thing, so they move together,
   gain space together and can be given a surface together. */
.p-columns--rows { grid-template-columns: minmax(0, 1fr); }

/* ONE COLUMN — the shape that is not a split (Cam, 2026-09-01: "you should be
   able to have one column ... this is essentially your custom element, your
   custom block"). One full-width cell you fill with elements and then move
   around the page as one thing.

   Written as its own rule even though the declaration is identical to
   `--rows`, and deliberately not folded in with it. The two are the same GRID
   and different CHOICES: `--rows` holds two cells that belong together, this
   holds one. The picker draws its diagram from the declared cell count and
   the applier composes that many cells, so collapsing them into one selector
   would leave the structure the author actually picked unnameable.

   `--single` rather than `--one`, because `p-columns--one` is a prefix of
   `p-columns--one-over-two`. Nothing here matches classes by prefix today —
   CSS matches whole tokens and the applier compares exactly — but a name that
   is a prefix of another name is a grep away from a wrong answer later. */
.p-columns--single { grid-template-columns: minmax(0, 1fr); }

/* ── one inside another (2026-09-01) ────────────────────────────────
   Cam: "we also should be able to add a column within a column. Only once,
   though ... if I wanted to, for example, have two buttons beside each other."

   `margin-block: var(--space-block)` is BAND spacing — the gap between one
   arrangement and the next thing on the page. Inside a cell it is not a band,
   it is one item in a short stack, so it takes the stack's own rhythm from the
   cell instead. Without this a nested pair of buttons sat a full band away
   from the heading above it and looked like a mistake.

   Depth is capped at one everywhere else — both scanners stop descending and
   the applier refuses to compose a third — so there is no rule here for a
   level that cannot exist. */
.p-columns__cell > .p-columns { margin-block: 0; }
.p-columns__cell > .p-columns + * { margin-top: 1rem; }
.p-columns__cell > * + .p-columns { margin-top: 1rem; }

/* ── spacing and alignment ──────────────────────────────────────── */
.p-columns--middle { align-items: center; }
.p-columns--tight { gap: .75rem 1.25rem; }
.p-columns--roomy { gap: 2rem 3rem; }

/* min-width: 0, or a long word inside a grid cell refuses to wrap and pushes
   its column wider than its share. */
.p-columns__cell { min-width: 0; }
.p-columns__cell > :first-child { margin-top: 0; }
.p-columns__cell > :last-child { margin-bottom: 0; }

/* Surfaces come from the house tokens, never a colour written here — a pattern
   that hard-codes one is how a page quietly stops matching its own site. */
.p-columns__cell--tint {
  background: color-mix(in srgb, var(--color-primary) 6%, transparent);
  border-radius: var(--radius);
  padding: var(--gutter);
}
.p-columns__cell--bordered {
  border: var(--card-border);
  border-radius: var(--radius);
  padding: var(--gutter);
}

/* Four across is unreadable long before a phone. It halves first, then stacks
   with everything else. */
@media (max-width: 62rem) {
  .p-columns--four { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* THE ONE RULE NO STRUCTURE MAY OPT OUT OF. Every shape above becomes a single
   column below 46rem, in the order it was written — about half a museum's
   visitors are on a phone, and two columns of running text there is one word
   per line. The full-width cells are reset too, or `grid-column: 1 / -1`
   survives into a one-column grid and means nothing. */
@media (max-width: 46rem) {
  .p-columns,
  .p-columns--three,
  .p-columns--four,
  .p-columns--wide-narrow,
  .p-columns--narrow-wide,
  .p-columns--one-over-two,
  .p-columns--two-over-one {
    grid-template-columns: minmax(0, 1fr);
  }
  .p-columns > .p-columns__cell { grid-column: auto; }
}

/* ── pattern: cta (house) ───────────────────── */
/* Call to action — one thing to do, said once.

   Ported from the cta BLOCK on 2026-08-08, classes rewritten to the pattern
   contract. A LINK styled as a button, never a <button>: it navigates, so
   middle-click, right-click and open-in-new-tab must all keep working, and a
   screen reader should call it a link because that is what it is.

   FIXED 2026-08-10, with callout and quote. The port carried three colours
   written here rather than read from the site: a tint and a border that named
   `--color-tint` and `--color-rule`, WHICH THIS PLATFORM HAS NEVER HAD, so the
   var() resolved to nothing and the hard-coded fallback did all the work on
   every site — a panel that stayed the same grey whatever the museum's design
   said. The third was a literal white for the button's own text, which is now
   `--button-color`: the site already has a setting for the ink that sits on a
   button, and hard-coding white here was the reason a site with a pale primary
   colour got white-on-pale and nothing said so. There was also nothing
   happening at 46rem. */
.p-cta {
  margin-block: var(--space-block);
  padding: 1.25rem 1.4rem;
  background: color-mix(in srgb, var(--color-primary) 6%, transparent);
  border: var(--card-border);
  border-radius: 4px;
}
.p-cta > :first-child { margin-top: 0; }
.p-cta__action { margin: .9rem 0 0; }
.p-cta > .p-cta__action:first-child { margin-top: 0; }
.p-cta__button {
  display: inline-block;
  background: var(--color-primary);
  color: var(--button-color);
  text-decoration: none;
  font-weight: 600;
  padding: .7rem 1.4rem;
  border-radius: 3px;
  /* Comfortably past the 44px minimum target on touch. */
  min-height: 2.75rem;
  line-height: 1.35;
}
/* The site-wide rule turns links the accent colour on hover; on a filled
   button that would recolour the TEXT against its own background. */
a.p-cta__button:hover { color: var(--button-color); background: var(--color-accent); }
.p-cta__button:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 3px; }
.p-cta--plain { background: none; border: 0; padding: 0; }
.p-cta--centred { text-align: center; }

/* On a phone the panel's inset costs the same 2.8rem it costs on a desktop, out
   of a third of the width — and the button goes full width, because a call to
   action that needs a careful thumb is not one. */
@media (max-width: 46rem) {
  .p-cta { padding: 1rem; }
  .p-cta__button { display: block; text-align: center; }
}

/* ── the large one: the band a page ends on (Cam, 2026-08-31) ────────────
   "A full width CTA, big." The small one is a panel inside the reading
   column; this is the whole width of the page, and it is the last thing
   said. Same markup, same link-not-button rule, one extra class.

   Full-bleed the way the rest of the platform does it — a box-shadow spread
   to 100vmax with a matching clip-path — rather than 100vw, which is wider
   than the page on any browser drawing a classic scrollbar and puts a
   horizontal scrollbar on every page carrying one. */
.p-cta--large {
  background: var(--color-primary);
  border: 0;
  border-radius: 0;
  padding: clamp(2.5rem, 6vw, 4.5rem) 0;
  box-shadow: 0 0 0 100vmax var(--color-primary);
  clip-path: inset(0 -100vmax);
  text-align: center;
}
.p-cta--large p { color: var(--color-bg); }
.p-cta--large :is(h2, h3) {
  color: var(--color-bg);
  font-size: clamp(1.6rem, 3.4vw, 2.4rem);
  margin: 0 0 .6rem;
}
.p-cta--large .p-cta__lede {
  max-width: 34em; margin-inline: auto;
  color: color-mix(in srgb, var(--color-bg) 78%, var(--color-primary));
}
/* The accent is the ground's opposite here: a primary-coloured button on a
   primary ground is invisible, so the large band's button takes the accent
   and its ink is the same --button-color the small one uses. */
.p-cta--large .p-cta__button {
  background: var(--color-accent);
  color: var(--color-primary);
  font-size: 1rem;
  padding: .85rem 2rem;
}
.p-cta--large a.p-cta__button:hover {
  background: var(--color-bg);
  color: var(--color-primary);
}
.p-cta--large .p-cta__button:focus-visible {
  outline: 3px solid var(--color-bg); outline-offset: 3px;
}
@media (max-width: 46rem) {
  .p-cta--large { padding: 2.25rem 0; }
  .p-cta--large .p-cta__button { display: inline-block; }
}

/* ── pattern: deadline (site) ───────────────────── */
/* The five-day deadline — the home page's one moment of pure confidence.

   Written 2026-09-08. The page below the hero had become three rows of equal
   boxes; this is the asymmetric, one-very-large-thing section that breaks that
   rhythm. Five marks for five business days, drawn rather than described.

   Colour comes from the site's tokens. The marks are the accent used as a
   FILL, which is the only way this site is allowed to use rose at size. */

.p-deadline { background: #DCEAE0; box-shadow: 0 0 0 100vmax #DCEAE0; clip-path: inset(0 -100vmax); }
.p-deadline__inner {
  max-width: var(--content-width); margin: 0 auto; padding: clamp(3rem, 6vw, 5rem) var(--gutter);
  display: grid; gap: clamp(2rem, 5vw, 4.5rem); align-items: center;
}
@media (min-width: 60rem) {
  .p-deadline__inner { grid-template-columns: minmax(0, .82fr) minmax(0, 1.18fr); }
}

/* The marks. Four uprights and a fifth struck across them — a tally of five,
   which is how anybody actually counts days on a desk pad. */
.p-deadline__count { display: inline-flex; align-items: flex-end; gap: clamp(.6rem, 1.6vw, 1rem); position: relative; }
.p-deadline__mark { width: clamp(.55rem, 1.5vw, .95rem); height: clamp(6rem, 13vw, 9.5rem); background: var(--color-accent); border-radius: 2px; }
.p-deadline__mark--strike {
  position: absolute; left: -6%; top: 50%; width: 112%; height: clamp(.55rem, 1.5vw, .95rem);
  transform: translateY(-50%) rotate(-14deg); transform-origin: center; border-radius: 2px;
}
.p-deadline__days {
  margin: 1.4rem 0 0; font-family: var(--font-body); font-size: .78rem; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase; color: var(--color-primary);
}

.p-deadline__eyebrow {
  margin: 0 0 1.1rem; font-family: var(--font-body); font-size: .74rem; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase; color: var(--link-hover-color);
}
.p-deadline__heading {
  margin: 0; font-family: var(--font-heading); font-weight: 700;
  font-size: clamp(2rem, 4.2vw, 3.1rem); line-height: 1.04; letter-spacing: -.03em;
  color: var(--color-primary); text-wrap: balance;
}
.p-deadline__body { margin: 1.3rem 0 0; font-family: var(--font-body); font-size: 1.05rem; line-height: 1.62; color: #23342C; max-width: 46em; }
.p-deadline__actions { display: flex; flex-wrap: wrap; align-items: center; gap: .8rem 1.6rem; margin: 1.9rem 0 0; }
.p-deadline__actions a:not([class]) {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: 1rem 1.6rem; border-radius: var(--button-radius);
  font-family: var(--font-body); font-weight: 700; font-size: .98rem; text-decoration: none;
  background: var(--color-primary); color: #fff; border: 1px solid var(--color-primary);
}
.p-deadline__actions a:not([class]) + a:not([class]) {
  background: transparent; border-color: transparent; color: var(--color-primary);
  padding-left: 0; padding-right: 0;
}
.p-deadline__actions a:not([class]) + a:not([class])::after { content: "\2192"; display: inline-block; }
.p-deadline__actions a:not([class]) + a:not([class]):hover { text-decoration: underline; text-underline-offset: .16em; }

.p-deadline--pine { background: var(--color-primary); box-shadow: 0 0 0 100vmax var(--color-primary); }
.p-deadline--pine .p-deadline__heading { color: #fff; }
.p-deadline--pine .p-deadline__body { color: #C3DACB; }
.p-deadline--pine .p-deadline__eyebrow, .p-deadline--pine .p-deadline__days { color: #93C7AE; }

/* ── pattern: district-map (site) ───────────────────── */
/* The twelve districts — CAMRO's own geography, drawn once.

   Written 2026-09-07 for the Field Guide edition. It exists because the site
   talks about districts on nine pages and had never shown one, and because a
   hero needs something to look at that is the SUBJECT rather than a stock
   photograph of a filing cabinet.

   Colour comes from the site's tokens. The picked-out district is filled with
   --color-accent (rose) and its numeral is set in ink, never white: white on
   rose is 3.3:1 and fails, ink on rose is 4.9:1. */

.p-district-map { margin: 0; }
.p-district-map__figure { width: 100%; max-width: 26rem; display: block; margin-inline: auto; overflow: visible; }

.p-district-map__cells polygon {
  fill: rgba(255,255,255,.07);
  stroke: rgba(147,199,174,.5);
  stroke-width: 1.5;
}
.p-district-map__cells text {
  fill: #93C7AE;
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: 600;
  text-anchor: middle;
}
.p-district-map__cells .is-on polygon { fill: var(--color-accent); stroke: var(--color-accent); }
.p-district-map__cells .is-on text { fill: #12231C; font-weight: 700; }

.p-district-map__pin circle { fill: #12231C; stroke: none; }
.p-district-map__pin .p-district-map__ring {
  fill: none; stroke: rgba(18,35,28,.5); stroke-width: 2;
  animation: p-district-map-ping 2.6s ease-out infinite;
}
@keyframes p-district-map-ping {
  0%   { r: 9px;  opacity: .9; }
  70%  { r: 28px; opacity: 0; }
  100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .p-district-map__pin .p-district-map__ring { animation: none; opacity: .5; }
}

.p-district-map__caption {
  display: flex; align-items: center; justify-content: center; gap: .6rem;
  margin: 1.1rem 0 0;
  font-family: var(--font-body); font-size: .92rem; font-weight: 600; line-height: 1.4;
  color: #A9C9B7;
}
.p-district-map__dot { width: .6rem; height: .6rem; border-radius: 50%; background: var(--color-accent); flex: none; }

/* On a light ground the same drawing, inverted. The picked-out district has to
   be restated here: these selectors carry the same specificity as the ones
   above, so on equal weight the later rule wins and the highlight would be
   quietly repainted mint — the map would still draw, and would simply stop
   saying the one thing it exists to say. */
.p-district-map--light .p-district-map__cells polygon { fill: #DCEAE0; stroke: #C9DCD0; }
.p-district-map--light .p-district-map__cells text { fill: #4E6459; }
.p-district-map--light .p-district-map__cells .is-on polygon { fill: var(--color-accent); stroke: var(--color-accent); }
.p-district-map--light .p-district-map__cells .is-on text { fill: #12231C; }
.p-district-map--light .p-district-map__caption { color: #4E6459; }

/* ── pattern: doors (site) ───────────────────── */
/* Two doors — the audience split, made into the page.

   Ported from CAMRO's Essentials `cards` block on 2026-08-25; reset
   2026-09-07, then dressed as Field Guide signposts the same day: a white
   card on the chalk ground, one mint hairline, the site's card radius, and a
   mint chip carrying the door's number. Nothing here lifts on hover: a panel
   is not a link, only its links are. The number chip is decoration and is
   hidden from a screen reader by the page, not by this file. */

.p-doors { padding: clamp(2rem, 4vw, 3rem) 0 clamp(3rem, 6.5vw, 5.25rem); }
.p-doors__inner { max-width: var(--content-width); margin: 0 auto;
  padding: 0 var(--gutter); }
.p-doors__grid { display: grid; gap: 1.5rem; }
@media (min-width: 48rem) { .p-doors__grid { grid-template-columns: repeat(2, minmax(0,1fr)); } }
.p-doors--three .p-doors__grid { grid-template-columns: 1fr; }
@media (min-width: 60rem) { .p-doors--three .p-doors__grid { grid-template-columns: repeat(3, minmax(0,1fr)); } }

.p-doors__panel { background: #fff; border: 1px solid #C9DCD0; border-radius: var(--card-radius);
  padding: 1.8rem 1.7rem 1.6rem; display: flex; flex-direction: column; gap: .85rem; }
.p-doors__heading { margin: 0; color: var(--color-primary);
  font-family: var(--font-heading); font-size: 1.35rem; line-height: 1.2; letter-spacing: -.012em; }
.p-doors__body { margin: 0; color: #4E6459; font-family: var(--font-body); font-size: .98rem; line-height: 1.6; }
.p-doors__links { list-style: none; margin: auto 0 0; padding: .6rem 0 0;
  display: flex; flex-direction: column; gap: .5rem; }
.p-doors__links a { color: var(--color-primary); font-family: var(--font-body); font-weight: 600;
  font-size: .95rem; text-decoration: none; }
.p-doors__links a::after { content: "\2192"; margin-left: .4rem; display: inline-block; transition: transform .15s ease; }
.p-doors__links a:hover { color: var(--link-hover-color); text-decoration: underline; text-underline-offset: .16em; }
.p-doors__links a:hover::after { transform: translateX(.2rem); }

/* The number chip: mint, the card radius halved, the pine numeral. */
.p-doors__n {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.6rem; height: 2.6rem; margin: 0 0 .3rem;
  border-radius: calc(var(--card-radius) * .75);
  background: #DCEAE0; color: var(--color-primary);
  font-family: var(--font-body); font-weight: 700; font-size: 1rem;
}

/* ── --rows: the doors as three ruled rows rather than three equal boxes ──
   2026-09-08. On the home page the doors were one of three consecutive card
   rows and read as furniture. As rows they get the width of the page, the
   numeral becomes a real typographic mark rather than a chip, and the section
   stops rhyming with the two collection grids under it. Asymmetric on purpose:
   a narrow numeral column against a wide one. */
.p-doors--rows .p-doors__grid { display: block; }
.p-doors--rows .p-doors__panel {
  background: none; border: 0; border-top: 1px solid #C9DCD0; border-radius: 0;
  padding: clamp(1.6rem, 3.2vw, 2.4rem) 0;
  display: grid; gap: .4rem clamp(1.5rem, 4vw, 3.5rem); align-items: start;
}
.p-doors--rows .p-doors__panel:last-child { border-bottom: 1px solid #C9DCD0; }
@media (min-width: 52rem) {
  .p-doors--rows .p-doors__panel { grid-template-columns: 6rem minmax(0, 1fr) minmax(0, 15rem); }
  .p-doors--rows .p-doors__n { grid-row: 1 / span 3; }
  .p-doors--rows .p-doors__links { grid-column: 3; grid-row: 1 / span 3; margin: .2rem 0 0; }
}
.p-doors--rows .p-doors__n {
  /* the base rule is an inline-flex chip that centres its own content; as a
     grid item that blockifies and the numeral drifts to the middle of the
     row on a phone. It is a typographic mark here, not a chip. */
  display: block; width: auto; height: auto; margin: 0; background: none; border-radius: 0;
  font-family: var(--font-heading); font-weight: 700;
  font-size: clamp(2.6rem, 5vw, 4rem); line-height: .9; letter-spacing: -.04em;
  color: var(--color-accent);
}
.p-doors--rows .p-doors__heading { font-size: clamp(1.5rem, 2.6vw, 2rem); letter-spacing: -.025em; }
.p-doors--rows .p-doors__body { max-width: 38em; }

/* ── pattern: hero (site) ───────────────────── */
/* Hero band — the opening of a page.

   Ported from CAMRO's Essentials `hero` block on 2026-08-25; reset on
   2026-09-07 for the second edition of the Minute Book. The opening is no
   longer a navy slab: the home page opens on paper, straight under the
   masthead's double rule, with the headline in the display serif at its
   largest size and the three figures set as a ruled register beside it —
   numerals in the serif, labels in the sans, hairlines between, no box.

   2026-09-07, third pass (Field Guide): --color-primary is PINE and
   --color-accent is ROSE. The home page opens on a full-bleed pine band with
   the district map beside it (--pine); a section front opens on mint
   (--mist); --plain keeps the paper opening. The one rule the colours here
   must not break: ROSE IS A FILL. As text on a light ground it is 3.1:1 and
   fails, so anything rose that carries words uses --link-hover-color
   (#B01D42), and on the pine ground it lightens to #FA8CA3. */

.p-hero {
  padding: clamp(3.25rem, 7vw, 5.5rem) 0 clamp(2.75rem, 5.5vw, 4.5rem);
}

.p-hero__inner {
  max-width: var(--content-width);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

.p-hero__eyebrow {
  font-family: var(--font-body);
  font-size: .74rem;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  margin: 0 0 1.25rem;
  color: var(--link-hover-color);
}

.p-hero__heading {
  margin: 0;
  font-family: var(--font-heading);
  font-size: clamp(2.7rem, 5.2vw, 4.6rem);
  line-height: 1.02;
  letter-spacing: -.03em;
  max-width: 12em;
  text-wrap: balance;
  color: var(--color-primary);
}

.p-hero__lede {
  font-family: var(--font-body);
  font-size: 1.18rem;
  line-height: 1.6;
  max-width: 34em;
  margin: 1.35rem 0 0;
  color: var(--abstract-color);
}

.p-hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .75rem 1.75rem;
  margin: 2rem 0 0;
}

/* Grounds. Plain is the home page: paper, nothing behind it. Mist and navy
   stay available to a page that wants a band. */
.p-hero--plain { background: none; }
.p-hero--mist { background: #DCEAE0; box-shadow: 0 0 0 100vmax #DCEAE0; clip-path: inset(0 -100vmax); }
/* On mint, quiet text is the ink or pine — never a mid-tone green. A mid green
   on mint clears AA on the numbers (5.13:1) and still reads badly, because the
   two share a hue and the eye separates hue before it separates lightness. The
   muted tone is for the chalk ground only. */
.p-hero--mist .p-hero__lede { color: #23342C; }
.p-hero--navy { background: var(--color-primary); color: #fff;
  box-shadow: 0 0 0 100vmax var(--color-primary); clip-path: inset(0 -100vmax); }
.p-hero--navy .p-hero__heading { color: #fff; }
.p-hero--navy .p-hero__eyebrow { color: #FA8CA3; }
.p-hero--navy .p-hero__lede { color: #C3DACB; }

/* Actions. A link styled as a button, never a <button>: it navigates, so
   middle-click and open-in-new-tab have to keep working.

   :not([class]) throughout: a BARE link here draws the hero's own default
   treatment, and a link the inline editor has given a skin class (.btn and
   friends) is that skin's business entirely. The FIRST bare link is the
   section's one button; a second is a link with an arrow — the house rule
   is one button per section, the rest inline. */
.p-hero__actions a:not([class]) {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .85rem 1.45rem;
  border-radius: var(--button-radius);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: .98rem;
  letter-spacing: .01em;
  text-decoration: none;
  background: var(--color-primary);
  color: #fff;
  border: 1px solid var(--color-primary);
  transition: background-color .15s ease, border-color .15s ease;
}
.p-hero__actions a:not([class]):hover { background: #0F6349; border-color: #0F6349; filter: none; }
.p-hero__actions a:not([class]) + a:not([class]) {
  background: transparent; border-color: transparent; color: var(--color-primary);
  padding-left: 0; padding-right: 0;
}
.p-hero__actions a:not([class]) + a:not([class])::after {
  content: "\2192"; display: inline-block; transition: transform .15s ease;
}
.p-hero__actions a:not([class]) + a:not([class]):hover { background: transparent; text-decoration: underline;
  text-underline-offset: .16em; }
.p-hero__actions a:not([class]) + a:not([class]):hover::after { transform: translateX(.2rem); }
.p-hero--navy .p-hero__actions a:not([class]) { background: #fff; color: var(--color-primary); border-color: #fff; }
.p-hero--navy .p-hero__actions a:not([class]) + a:not([class]) { background: transparent; color: #fff; border-color: transparent; }
.p-hero__actions a { text-decoration: none; }

/* The figures beside the headline on a wide screen, beneath it on a phone. */
.p-hero__grid { display: grid; gap: 2.5rem; align-items: end; }
@media (min-width: 60rem) {
  .p-hero__grid { grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); gap: clamp(3rem, 6vw, 6rem); }
}

/* A ruled register, not a box: a navy rule at its head, hairlines between
   the rows, the numeral in the serif and the label in the sans. The number
   and label are each their own <p> so the editor can offer each its own
   words; margin:0 keeps them on the row. */
.p-hero__stats { display: flex; flex-direction: column; gap: 0; margin: 0; padding: 0; list-style: none;
  background: none; border-radius: 0; border-top: 2px solid var(--color-primary); }
.p-hero__stat { display: flex; align-items: baseline; gap: 1.1rem; padding: 1.05rem 0 1rem;
  border-bottom: 1px solid #D3E4D9; background: none; }
.p-hero__stat-n { font-family: var(--font-heading); font-size: clamp(2.3rem, 3.4vw, 2.9rem); font-weight: 700;
  line-height: 1; letter-spacing: -.02em; color: var(--color-primary); min-width: 3.1em; margin: 0;
  font-variant-numeric: tabular-nums; font-variation-settings: "opsz" 96; }
.p-hero__stat-l { font-family: var(--font-body); font-size: .9rem; line-height: 1.4; color: var(--abstract-color); margin: 0; }
.p-hero--navy .p-hero__stats { border-top-color: rgba(255,255,255,.7); }
.p-hero--navy .p-hero__stat { border-bottom-color: rgba(255,255,255,.18); }
.p-hero--navy .p-hero__stat-n { color: #fff; }
.p-hero--navy .p-hero__stat-l { color: #A9C9B7; }

/* ── --pine: the home page's opening ──────────────────────────────────────
   A full-bleed pine band with a soft foot, the headline in white with its
   second sentence in rose, the figures on one rule beneath, and the district
   map beside it. The primary action is the accent FILLED, with ink on it —
   white-on-rose is 3.3:1 and would fail; ink-on-rose is 4.9:1. */
.p-hero--pine {
  background: var(--color-primary);
  color: #EAF3EC;
  box-shadow: 0 0 0 100vmax var(--color-primary);
  clip-path: inset(0 -100vmax round 0 0 var(--radius) var(--radius));
  padding: clamp(2.6rem, 5.5vw, 4.4rem) 0 clamp(2.4rem, 4.5vw, 3.4rem);
}
.p-hero--pine .p-hero__heading { color: #fff; max-width: 15em; }
.p-hero--pine .p-hero__heading em,
.p-hero--pine .p-hero__heading strong { font-style: normal; font-weight: inherit; color: #FA8CA3; }
.p-hero--pine .p-hero__eyebrow { color: #93C7AE; }
.p-hero--pine .p-hero__lede { color: #C3DACB; max-width: 28em; }
.p-hero--pine .p-hero__actions a:not([class]) {
  background: var(--color-accent); border-color: var(--color-accent); color: #12231C;
}
.p-hero--pine .p-hero__actions a:not([class]):hover { background: #FA8CA3; border-color: #FA8CA3; }
.p-hero--pine .p-hero__actions a:not([class]) + a:not([class]) {
  background: transparent; border-color: transparent; color: #fff;
}
/* The figures under the opening: one hairline, no box, four across. */
.p-hero--pine .p-hero__stats {
  border-top: 1px solid rgba(255,255,255,.2);
  display: flex; flex-direction: row; flex-wrap: wrap; gap: 1.5rem clamp(1.5rem, 5vw, 4rem);
  margin-top: clamp(2.2rem, 4.5vw, 3.2rem); padding-top: 1.9rem;
}
.p-hero--pine .p-hero__stat {
  display: block; border-bottom: 0; padding: 0; flex: 0 1 auto;
}
.p-hero--pine .p-hero__stat-n { color: #fff; min-width: 0; font-size: clamp(2.2rem, 3.1vw, 3rem); }
.p-hero--pine .p-hero__stat-l { color: #A9C9B7; margin-top: .4rem; }
@media (min-width: 60rem) {
  .p-hero--pine .p-hero__grid { grid-template-columns: minmax(0, 1.1fr) minmax(0, .9fr); align-items: center; }
}

/* ── pattern: media-text (house) ───────────────────── */
/* A picture beside its words.

   Centred against each other by default, because that is what reads as
   deliberate when a photograph is taller or shorter than its paragraph.

   NO REVERSE MODIFIER, deliberately. To put the picture on the right, write it
   second. Reversing visually while leaving the source order alone hands a
   screen reader and a phone a different order from the one on screen, and this
   platform's rule is that those are never allowed to disagree. The editor's
   "swap sides" button therefore rewrites the SOURCE — see editor.swap_parts —
   which is why there is nothing here for it to hook into. */
.p-media-text {
  display: grid;
  grid-template-columns: minmax(0, var(--mt-a, 1fr)) minmax(0, var(--mt-b, 1fr));
  gap: 1.5rem 2rem;
  align-items: center;
  margin-block: var(--space-block);
}

/* The split is named for the COLUMNS as written — first, then second — not for
   "picture" and "words". Which of the two is the picture depends on the order
   they are written in, and that order is a per-instance choice; a class called
   `--wide-media` would name the wrong column the moment the sides were
   swapped, and would then be a lie nobody could see. */
.p-media-text--split-30-70 { --mt-a: 3fr; --mt-b: 7fr; }
.p-media-text--split-40-60 { --mt-a: 4fr; --mt-b: 6fr; }
.p-media-text--split-60-40 { --mt-a: 6fr; --mt-b: 4fr; }
.p-media-text--split-70-30 { --mt-a: 7fr; --mt-b: 3fr; }

/* The first two splits under their old names. Written before the split scale
   existed and possibly sitting in a page somewhere, so they keep working; they
   are off the options list and out of the vocabulary the AI is given, so
   nothing new will be written with them. */
.p-media-text--wide-text  { --mt-a: 4fr; --mt-b: 6fr; }
.p-media-text--wide-media { --mt-a: 6fr; --mt-b: 4fr; }

.p-media-text--top   { align-items: start; }
.p-media-text--tight { gap: 1rem 1.25rem; }
.p-media-text--roomy { gap: 2rem 3rem; }

/* A <figure> carries its own margin in every browser's default sheet, which
   would push the picture out of alignment with the words beside it. */
.p-media-text__media { min-width: 0; margin: 0; }
.p-media-text__media img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius);
}
.p-media-text__media figcaption {
  font-size: var(--caption-size);
  line-height: 1.4;
  margin-top: .4rem;
  opacity: .85;
}

.p-media-text__text { min-width: 0; }
.p-media-text__text > :first-child { margin-top: 0; }
.p-media-text__text > :last-child  { margin-bottom: 0; }

@media (max-width: 46rem) {
  .p-media-text { --mt-a: 1fr; --mt-b: 1fr; grid-template-columns: minmax(0, 1fr); }
}

/* ── pattern: minute-book (site) ───────────────────── */
/* --- bands ------------------------------------------------------------- */
.band{padding:clamp(2.5rem,5vw,4rem) 0}
/* THE GROUND SPREADS, THE BOX DOES NOT MOVE (2026-09-01).
   A band's colour has to reach both edges of the screen while its words stay
   in the page column. This used to be done in design.custom_css by pulling
   the element out to the viewport and pushing its content back
   (`margin-inline: calc(50% - 50vw)` with a matching padding) — two
   declarations that undo each other, and which have to agree about the
   viewport width to do it. They cannot: `vw` counts the scrollbar and the
   viewport does not, so on a classic-scrollbar machine every band came out
   one scrollbar too wide and had 15px thrown away by a clip.
   Painting instead of moving has none of that. The element keeps its place
   in the column, the shadow spreads the ground outward, and the clip trims
   it flush at top and bottom so one band cannot wash into the next. Same
   idiom as `.p-hero--navy` and `.page-band` in the chrome and as
   `.p-cta--large`; the engine's `.blk[data-bg]` was converted to it first. */
.band-mist{background:var(--mist);
  box-shadow:0 0 0 100vmax var(--mist);clip-path:inset(0 -100vmax)}
.band-navy{background:var(--accent);color:#fff;
  box-shadow:0 0 0 100vmax var(--accent);clip-path:inset(0 -100vmax)}
.band-navy h1,.band-navy h2,.band-navy h3{color:#fff}
.band-navy .lede{color:var(--onDark)}
.band-head{display:flex;justify-content:space-between;align-items:flex-end;gap:1.5rem;flex-wrap:wrap;margin-bottom:1.75rem}
.eyebrow{font-family:'Public Sans', 'Segoe UI', system-ui, sans-serif;font-size:.76rem;letter-spacing:.14em;text-transform:uppercase;color:var(--accentSoft);margin-bottom:1rem}
.lede{font-size:1.15rem;color:var(--muted);max-width:38em}
.band-navy .eyebrow{color:var(--accentSoft)}
h1+.lede,h2+.lede{margin-top:.85rem}
.hero h1{margin-bottom:0}

/* --- hero -------------------------------------------------------------- */
.hero .wrap{display:grid;gap:clamp(1.75rem,4vw,2.75rem)}
.hero-split .wrap{grid-template-columns:minmax(0,1.4fr) minmax(0,1fr);align-items:center}
@media(max-width:820px){.hero-split .wrap{grid-template-columns:1fr}}
.hero h1{max-width:16em}
.stats{display:flex;flex-direction:column;gap:1px;background:rgba(255,255,255,.22);border-radius:8px;overflow:hidden}
.stat{background:var(--accent);padding:1.15rem 1.4rem;display:flex;align-items:baseline;gap:1rem}
.stat-n{font-family:Spectral, Georgia, 'Times New Roman', serif;font-size:2.35rem;line-height:1;color:#fff;min-width:2.6em}
.stat-l{font-size:.92rem;color:var(--onDark);line-height:1.4}

/* --- buttons ----------------------------------------------------------- */
.actions{display:flex;flex-wrap:wrap;gap:.75rem;align-items:center;margin:1.5rem 0 0}
/* A bare link in an actions row draws as a ghost button, so the editor can
   round-trip its words without a class riding on the link. */
.actions>a:not([class]){display:inline-block;background:transparent;color:var(--accent);padding:.85rem 1.6rem;border-radius:5px;font-weight:600;font-size:1rem;border:1px solid var(--accent)}
.actions>a:not([class]):hover{filter:brightness(1.12);text-decoration:none}
.btn{display:inline-block;background:var(--accent);color:#fff;padding:.85rem 1.6rem;border-radius:5px;font-weight:600;font-size:1rem;border:1px solid var(--accent)}
.btn:hover{filter:brightness(1.12);text-decoration:none}
.btn-ghost{background:transparent;color:var(--accent)}
.btn-invert{background:#fff;color:var(--accent);border-color:#fff}
.btn-ghost-invert{background:transparent;color:#fff;border-color:var(--accentSoft)}
/* Variants the inline editor can set. Both are ENUMS on the record, not
   free-form CSS — a client can pick a shape the design already has and
   cannot invent one. */
.btn-wide{display:block;width:100%;text-align:center}
/* A button drawn as a plain link. Still a button in the record; the design
   decides it should not look like one here. */
.btn-bare{background:transparent;border-color:transparent;color:var(--accent);padding-left:0;padding-right:0}
.btn-bare:hover{filter:none;text-decoration:underline;text-underline-offset:4px}
.btn-bare-invert{color:#fff}

/* ---------------------------------------------------------------------------
   LAYOUT VARIANTS the inline editor can set.
   Every one of these is driven by an ENUM on the record. There is no rule here
   that a client can reach with a number or a colour — they choose between
   arrangements the design already has. Brand-owned properties (band colour,
   accent, card and button treatment, type scale) are deliberately NOT in this
   set; see the BRAND list in platform/records/fields.ts.
   ------------------------------------------------------------------------ */

/* Vertical room around a section. */
.sp-tight{padding-top:clamp(1.25rem,2.5vw,2rem);padding-bottom:clamp(1.25rem,2.5vw,2rem)}
.sp-roomy{padding-top:clamp(4rem,8vw,6.5rem);padding-bottom:clamp(4rem,8vw,6.5rem)}

/* Alignment of a section's own text. Left, centre or right — the three a
   client actually asks for, and the whole of what "move it over" means here. */
.al-center{text-align:center}
.al-center .actions,.al-center .band-head{justify-content:center}
.al-center .lede{margin-left:auto;margin-right:auto}
.al-right{text-align:right}
.al-right .actions,.al-right .band-head{justify-content:flex-end}
.al-right .lede{margin-left:auto}
.actions-center{justify-content:center}
.actions-right{justify-content:flex-end}

/* How wide the text measure runs before it wraps. */
.measure-narrow{max-width:34rem}
.measure-normal{max-width:52rem}
.measure-wide{max-width:none}
.al-center .measure-narrow,.al-center .measure-normal{margin-left:auto;margin-right:auto}

/* Hero presence and headline placement. */
.hero-center .hero-body{text-align:center;margin-left:auto;margin-right:auto}
.hero-center .actions{justify-content:center}
.hero-right .hero-body{text-align:right;margin-left:auto}
.hero-right .actions{justify-content:flex-end}
.hero-compact{padding-top:clamp(1.75rem,3.5vw,2.75rem);padding-bottom:clamp(1.75rem,3.5vw,2.75rem)}
.hero-tall{padding-top:clamp(4.5rem,9vw,7.5rem);padding-bottom:clamp(4.5rem,9vw,7.5rem)}
.h-large h1{font-size:clamp(2.6rem,6vw,4.25rem)}
/* Stats beneath the headline rather than beside it: one row, not a column. */
.stats-below{display:grid;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));gap:1rem}
@media(max-width:820px){.stats-below{grid-template-columns:1fr}}

/* A single-column grid, for a cards or steps row a client has narrowed. */
.cols-1{grid-template-columns:minmax(0,1fr)}

/* Split: which side leads, and how the row divides. */
.split-even{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
.split-media{grid-template-columns:minmax(0,7fr) minmax(0,5fr)}
.split-reverse{direction:rtl}
.split-reverse>*{direction:ltr}
@media(max-width:820px){.split-even,.split-media{grid-template-columns:1fr}.split-reverse{direction:ltr}}

/* Where a figure sits inside its column. */
.fig-center{margin-left:auto;margin-right:auto}
.fig-right{margin-left:auto}

/* Events as a stacked list rather than a grid of cards. */
.ev-list .card{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:1rem}
@media(max-width:700px){.ev-list .card{grid-template-columns:1fr}}

/* Declared form questions. Half-width fields pair up on a wide screen and
   stack on a phone.
   These build ON the base form rules further down rather than replacing them:
   form.cantata label is already display:grid, which is what stacks a label
   above its control. An earlier version set .fld{display:block} and won on
   specificity, so every label collapsed and the textarea floated above its
   own caption. Only the column span and the checkbox row are new here. */
form.cantata{grid-template-columns:repeat(2,minmax(0,1fr));max-width:44rem}
form.cantata>label,form.cantata>p,form.cantata .fld-full{grid-column:1/-1}
form.cantata .fld-half{grid-column:span 1}
@media(max-width:600px){
  form.cantata{grid-template-columns:1fr}
  form.cantata .fld-half{grid-column:1/-1}
}
/* A tickbox reads across, not down, so this one label opts out of the grid. */
form.cantata label.fld-check{display:flex;align-items:flex-start;gap:.6rem;text-transform:none;letter-spacing:normal;font-weight:400;font-size:1rem}
form.cantata label.fld-check input{margin-top:.25rem;width:auto;flex:0 0 auto}
form.cantata .hint{display:block;font-size:.85rem;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:normal;margin:.1rem 0 .2rem}
form.cantata select{font:inherit;font-size:1rem;padding:.6rem .75rem;border:1px solid var(--rule);border-radius:var(--radius);background:var(--paper);color:var(--ink);text-transform:none;letter-spacing:normal;font-weight:400}

/* A call to action with the button beneath the text instead of beside it. */
.cta-stacked .wrap{display:block}
.cta-stacked .btn{margin-top:1.25rem}
.cta-stacked.al-center .btn{margin-left:auto;margin-right:auto;display:table}

/* --- grids and cards --------------------------------------------------- */
.grid{display:grid;gap:1.35rem}
.cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}
.cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}
.cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}
@media(max-width:900px){.cols-3,.cols-4{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:640px){.cols-2,.cols-3,.cols-4{grid-template-columns:1fr}}
.card{background:var(--paper);border:1px solid var(--rule);border-radius:var(--radius);padding:1.6rem 1.75rem;display:flex;flex-direction:column;gap:.75rem}
.card h3{color:var(--accent)}
.card h3 a{color:inherit}
.card-accent{border-top:3px solid var(--accent)}
.card-gold{border-top:3px solid var(--gold)}
.card p{margin:0}
.linklist{list-style:none;margin:.35rem 0 0;padding:0;display:flex;flex-direction:column;gap:.55rem;font-size:1rem}
.tagrow{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;margin:0}
.pill{background:var(--accent);color:#fff;font-family:'Public Sans', 'Segoe UI', system-ui, sans-serif;font-size:.68rem;letter-spacing:.08em;text-transform:uppercase;padding:.25rem .6rem;border-radius:var(--radius);font-weight:600}
.meta{color:var(--muted);font-family:'Public Sans', 'Segoe UI', system-ui, sans-serif;font-size:.92rem;margin:0}

/* --- split / panel ------------------------------------------------------ */
.split{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:clamp(1.75rem,4vw,3rem);align-items:start}
@media(max-width:820px){.split{grid-template-columns:1fr}}
.panel{background:var(--accent);color:#fff;border-radius:10px;padding:clamp(1.75rem,4vw,2.9rem)}
.panel h2,.panel h3{color:#fff}
.panel p{color:var(--onDark)}
.panel a{color:#fff;text-decoration:underline;text-underline-offset:4px}
.badge{display:inline-flex;align-items:center;gap:.5rem;border:1px solid var(--gold);color:var(--gold);border-radius:999px;padding:.35rem .85rem;font-size:.8rem;font-weight:600;margin:0 0 1rem}
.badge .i{width:14px;height:14px}
/* Optional figure in a split. The width is an inline style because it is a
   per-record CLAMPED number (160-960) validated by the schema, not a value
   a client typed into a stylesheet. It never exceeds its column. */
.split-fig{max-width:100%;height:auto;border-radius:8px;margin:1.35rem 0 0;display:block}
/* The width comes from the img's width ATTRIBUTE, not an inline style:
   style-src is 'self' with no 'unsafe-inline', so a style attribute would
   simply be dropped and the number would silently do nothing. */
.checks{list-style:none;margin:1.4rem 0 0;padding:0;display:flex;flex-direction:column;gap:.75rem}
.checks li{display:flex;align-items:flex-start;gap:.7rem}
.checks .i{width:18px;height:18px;flex:none;color:var(--gold);margin-top:.2rem}

/* --- steps and questions ------------------------------------------------ */
.steps .step{display:flex;flex-direction:column;gap:.6rem}
.step-n{font-family:Spectral, Georgia, 'Times New Roman', serif;font-size:2rem;color:var(--accentSoft);line-height:1}
.qgrid{gap:1rem}
.q{border-left:2px solid var(--accentSoft);padding:.2rem 0 .2rem 1rem}
.q-area{display:block;font-size:.7rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:.25rem}
.q p{font-size:1rem;margin:0}

/* --- cta ---------------------------------------------------------------- */
.cta .wrap{display:flex;align-items:center;justify-content:space-between;gap:1.75rem;flex-wrap:wrap}
.cta h2{margin-bottom:.35rem}

/* --- prose -------------------------------------------------------------- */
.prose{max-width:44em}
.prose h2{margin:2.2rem 0 .7rem}
.prose h2:first-child{margin-top:0}
.prose h3{margin:1.7rem 0 .5rem}
.prose ul,.prose ol{padding-left:1.3rem}
.prose li{margin:0 0 .45rem}
.prose blockquote{margin:1.6rem 0;padding:1rem 1.3rem;background:var(--mist);border-left:3px solid var(--gold);border-radius:0 var(--radius) var(--radius) 0}
.prose blockquote p{margin:0}
.prose table{width:100%;border-collapse:collapse;margin:1.4rem 0;font-size:.97rem}
.prose th,.prose td{text-align:left;padding:.6rem .75rem;border-bottom:1px solid var(--rule);vertical-align:top}
.prose th{font-family:'Public Sans', 'Segoe UI', system-ui, sans-serif;font-weight:600;font-size:.9rem;letter-spacing:.04em}
.tablewrap{overflow-x:auto}

/* --- directory ----------------------------------------------------------- */
.dir{display:grid;grid-template-columns:270px minmax(0,1fr);gap:clamp(1.5rem,3vw,2.5rem);align-items:start}
@media(max-width:820px){.dir{grid-template-columns:1fr}}
.dir-filters{border:1px solid var(--rule);border-radius:var(--radius);padding:1.35rem;display:flex;flex-direction:column;gap:1.5rem;position:sticky;top:1rem}
@media(max-width:820px){.dir-filters{position:static}}
.dir-filters fieldset{border:0;margin:0;padding:0;display:flex;flex-direction:column;gap:.6rem}
.dir-filters legend{font-size:.72rem;font-weight:700;letter-spacing:.07em;text-transform:uppercase;padding:0;margin-bottom:.3rem}
.field{display:flex;flex-direction:column;gap:.4rem}
.field span{font-size:.72rem;font-weight:700;letter-spacing:.07em;text-transform:uppercase}
.field input{font:inherit;font-size:1rem;padding:.55rem .7rem;border:1px solid var(--rule);border-radius:var(--radius);background:var(--paper);color:var(--ink);width:100%}
.check{display:flex;align-items:center;gap:.6rem;font-size:.97rem;color:var(--muted);cursor:pointer}
.check input{width:15px;height:15px;accent-color:var(--accent);flex:none}
.check span:nth-of-type(1){flex:1;color:var(--ink)}
.count{font-size:.85rem;color:var(--muted)}
.dir-results{display:flex;flex-direction:column;gap:.9rem}
.dir-count{color:var(--muted);font-size:.97rem;margin:0}
#dirlist{display:flex;flex-direction:column;gap:.75rem}
.card.row{padding:1.1rem 1.35rem;gap:.3rem}
.card.row h3{font-size:1.16rem}
.dir-empty{color:var(--muted)}

/* --- event detail --------------------------------------------------------- */
.ev{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:clamp(1.75rem,4vw,3rem);align-items:start}
@media(max-width:900px){.ev{grid-template-columns:1fr}}
.ev-side{display:flex;flex-direction:column;gap:1.1rem;position:sticky;top:1.5rem}
@media(max-width:900px){.ev-side{position:static}}
.ev-facts{border:1px solid var(--rule);border-top:3px solid var(--accent);border-radius:7px;padding:1.5rem;display:flex;flex-direction:column;gap:1.1rem}
.fact{display:flex;flex-direction:column;gap:.15rem}
.fact-l{font-size:.7rem;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--muted)}
.fact-v{font-size:1rem;line-height:1.45}
.ev-facts .btn{text-align:center}
.ev-note{font-size:.85rem;color:var(--muted);text-align:center;margin:0}
.ev-box{border:1px solid var(--rule);border-radius:7px;padding:1.25rem 1.5rem;display:flex;flex-direction:column;gap:.6rem}
.ev-box .fact-l{margin-bottom:.2rem}
.crumbs{font-size:.85rem;color:var(--muted);margin:0 0 1rem}


/* The pattern contract: every placed instance of this example carries the
   root class, so "where is every instance" is a selector. The vocabulary
   classes beside it do the drawing. */
.p-minute-book{display:block}

/* ── pattern: quote (house) ───────────────────── */
/* Quote — a pulled-out quotation with its attribution.

   Ported from the quote BLOCK on 2026-08-08, classes rewritten to the pattern
   contract. The big quotation mark stays a CSS pseudo-element on purpose: it is
   decoration, so it must not be in the text where it would be copied with the
   words and read out by a screen reader.

   FIXED 2026-08-10, with callout and cta — the same fault in all three, carried
   over from stylesheets that were written as BLOCKS and never held to the
   pattern contract. Here it was type: 1.15rem for the words and 3.2rem for the
   mark, decided inside one arrangement. Both now come from `--pullquote-size`,
   a real design setting, and the mark is DERIVED from it so it grows when the
   quotation does — nobody wants to set a quotation mark's size. The breakpoint
   also moved from 34rem to the house 46rem, so this folds where everything
   else on the site folds. */
.p-quote {
  margin-block: var(--space-block); margin-inline: 0;
  padding-left: 2.25rem;
  position: relative;
}
.p-quote::before {
  content: "\201C";
  position: absolute;
  left: 0; top: -.35rem;
  font-family: var(--font-heading);
  font-size: calc(var(--pullquote-size) * 2.8); line-height: 1;
  color: var(--color-accent);
  opacity: .55;
}
.p-quote blockquote {
  margin: 0;
  font-size: var(--pullquote-size); line-height: 1.55;
  font-family: var(--font-heading);
  color: var(--color-primary);
}
.p-quote blockquote > :first-child { margin-top: 0; }
.p-quote blockquote > :last-child { margin-bottom: 0; }
.p-quote__cite {
  margin-top: .5rem;
  font-size: var(--caption-size);
  opacity: .85;
}
.p-quote__cite::before { content: "\2014\00a0"; }   /* em dash + nbsp */
.p-quote--plain { padding-left: 1.2rem; border-left: 3px solid var(--color-accent); }
.p-quote--plain::before { content: none; }

@media (max-width: 46rem) {
  .p-quote { padding-left: 1.6rem; }
  .p-quote::before { font-size: calc(var(--pullquote-size) * 2.1); }
}

/* ── pattern: register-shapes (site) ───────────────────── */
/* ── Direction C additions ────────────────────────────────────────────
   The Minute Book vocabulary the inner pages never got: ruled data
   tables, definition rows, a documentary index. Built on the shipped
   Direction C tokens — warm paper, navy, gold rules, Spectral body,
   Public Sans labels, square corners. Nothing here invents a colour. */

/* Spreads its ground rather than moving its box — see the note beside
   `.band-mist` in minute-book/pattern.css for why.

   THE BORDER IS GONE, deliberately. It was a full-width hairline only
   because the element itself was dragged to the viewport; painted instead,
   the element sits in the column and a border would stop 56px short of each
   edge and end in mid-air.

   A spread shadow is a solid rectangle. Offsetting translates it; enlarging
   grows it; neither peels a line off its edge, so no combination of shadows
   ON THE ELEMENT can produce a hairline at its boundary — three
   constructions tried and rendered, all dead. A pseudo-element carrying its
   own spread and its own clip CAN: its clip is measured against its own 1px
   box, so it keeps the horizontal spread and cuts the vertical one to a
   line. Verified. Declined here anyway — it costs `position: relative` and
   an extra box on every instance, which is a new mechanism for a 1px
   decorative line, and the mist-to-paper boundary already does the
   separating.

   The load-bearing words are ON THE ELEMENT. An earlier version of this
   comment said the shadow simply could not carry a rule, generalising from
   three failed attempts to an impossibility — a different claim, with a
   different evidence requirement, and the fourth construction was one render
   away the whole time. It was caught by the session that found the
   counter-example (2026-09-01). */
.c-hero { background: var(--mist);
  box-shadow: 0 0 0 100vmax var(--mist); clip-path: inset(0 -100vmax);
  padding: clamp(2.25rem,4vw,3.25rem) 0; }
.c-hero .lbl { display:block; margin-bottom:.7rem; }
.c-hero h1 { margin:0; color:var(--accent); font-size:clamp(2rem,4vw,2.9rem); line-height:1.1; }
.c-hero p { margin:.9rem 0 0; max-width:42em; color:var(--muted); font-size:1.1rem; }

.lbl { font-family:'Public Sans','Segoe UI',system-ui,sans-serif; font-size:10.5px;
  letter-spacing:.19em; text-transform:uppercase; color:var(--muted); }

/* A section, announced the way a minute book announces one. */
.c-sec { padding: clamp(2rem,3.5vw,3rem) 0; border-top:1px solid var(--rule); }
.c-sec:first-of-type { border-top:0; }
.c-sec > h2 { margin:0 0 1.25rem; color:var(--accent); font-size:1.5rem;
  padding-bottom:.5rem; border-bottom:2px solid var(--gold); display:inline-block; }
.c-sec p { max-width:40em; }

/* THE ruled table. The site is mostly tables; C treats that as the point. */
.c-table { width:100%; border-collapse:collapse; margin:0; font-size:1rem; }
.c-table caption { text-align:left; font-family:'Public Sans',system-ui,sans-serif;
  font-size:10.5px; letter-spacing:.19em; text-transform:uppercase;
  color:var(--muted); padding-bottom:.6rem; }
.c-table th { text-align:left; font-family:'Public Sans',system-ui,sans-serif;
  font-size:11px; letter-spacing:.14em; text-transform:uppercase; font-weight:600;
  color:var(--muted); padding:.55rem .9rem .55rem 0; border-bottom:2px solid var(--accent); }
.c-table td { padding:.8rem .9rem .8rem 0; border-bottom:1px solid var(--rule);
  vertical-align:baseline; }
.c-table tr:last-child td { border-bottom:2px solid var(--accent); }
.c-table td:first-child { font-weight:600; color:var(--ink); }
.c-table .num { font-variant-numeric:tabular-nums; white-space:nowrap; }
.c-table .qt { color:var(--muted); }

/* A fact list: label left, answer right, ruled between. */
.c-facts { border-top:2px solid var(--accent); margin:0; }
.c-facts div { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:1.5rem;
  padding:.75rem 0; border-bottom:1px solid var(--rule); align-items:baseline; }
.c-facts dt { font-family:'Public Sans',system-ui,sans-serif; font-size:.86rem;
  letter-spacing:.04em; color:var(--muted); margin:0; }
.c-facts dd { margin:0; font-weight:600; font-variant-numeric:tabular-nums; }

/* A named thing and its definition — awards, credentials, limitations. */
.c-defs { margin:0; border-top:2px solid var(--accent); }
.c-defs > div { padding:1.1rem 0; border-bottom:1px solid var(--rule); }
.c-defs dt { margin:0 0 .35rem; font-weight:600; color:var(--accent); font-size:1.12rem; }
.c-defs dd { margin:0; color:var(--ink); max-width:44em; }

/* A run of links presented as an index, not a paragraph of slashes. */
.c-index { list-style:none; margin:0; padding:0; border-top:1px solid var(--rule);
  display:grid; grid-template-columns:repeat(auto-fit,minmax(15rem,1fr)); }
.c-index li { border-bottom:1px solid var(--rule); }
.c-index a { display:block; padding:.8rem 0; font-weight:600;
  font-family:'Public Sans',system-ui,sans-serif; font-size:.98rem; }

/* An aside the minute book would set apart: a gold rule and warm ground. */
.c-note { background:var(--mist); border-left:0; border-top:2px solid var(--gold);
  padding:1.4rem 1.6rem; margin:1.5rem 0 0; }
.c-note h3 { margin:0 0 .5rem; color:var(--accent); font-size:1.1rem; }
.c-note p { margin:0; max-width:44em; }

/* A year-and-name roll, for past recipients. */
.c-roll { list-style:none; margin:0; padding:0; border-top:1px solid var(--rule); }
.c-roll li { display:grid; grid-template-columns:5.5rem 1fr; gap:1.25rem;
  padding:.6rem 0; border-bottom:1px solid var(--rule); align-items:baseline; }
.c-roll b { font-variant-numeric:tabular-nums; color:var(--muted);
  font-family:'Public Sans',system-ui,sans-serif; font-size:.9rem; font-weight:600; }


/* The announced section. Root rule so the spacing system can see the gap
   around it (check_contract flagged the omission, 2026-08-27). */
.c-sec { margin-block: var(--space-block); }

/* The pattern contract: the root class names every placed instance. */
.p-register-shapes{display:block}


/* ─────────────────────────────────────────────────────────────────────
   Custom templates — a site's own page layouts, record layouts and loops.
   Injected here at build time from sites/<site>/templates/<name>/template.css
   (and the company library's sites/_shared/templates tier).

   AFTER the patterns, for the same reason patterns sit after blocks: a
   template's rules dress markup written for one deliberate shape, so where
   the two ever touch a property the template is the more specific intent.

   Why templates exist beside patterns: platform/templates/_loader.js
   ───────────────────────────────────────────────────────────────────── */


/* Per-picture settings — caption, and the enlarged view.
   The width/height/fit/focal-point of an individual picture are written as an
   inline style by the build (see the image-settings rule), because they are
   per-use decisions, not house style. */
.fig { margin-block: var(--space-block); margin-inline: 0; }
.fig img { display: block; border-radius: 3px; }
.fig figcaption {
  font-size: var(--caption-size); line-height: 1.4; margin-top: .4rem; opacity: .85;
}
/* Where the caption sits under its picture. Left is the default and needs no
   class — it is what a caption does if nobody says otherwise, and it is the
   only one that stays readable when the caption runs to two or three lines. */
.fig.cap-center figcaption { text-align: center; }
.fig.cap-right figcaption  { text-align: right; }
/* Captioning a picture builds a <figure> round it. Inside an arrangement that
   already provides one — media-text's picture half is a <figure> — that nests.
   Legal HTML and the right meaning (the caption belongs to the picture, not to
   the column), but it brings a standalone figure's outside margin in with it,
   which pushes the picture out of line with the words beside it. */
figure .fig { margin: 0; }
a.lightbox { display: inline-block; cursor: zoom-in; }
a.lightbox:hover img { opacity: .93; }

/* A picture set to sit beside the text (`align=left|right` on that one
   picture). The default is still a hard block — see the standing rule above.
   The width lives on the figure, not on the picture inside it, so a width
   given as a percentage is a percentage of the column and not of itself. */
.fig.align-left, .fig.align-right {
  width: var(--fig-w, 40%); max-width: 100%; margin-top: .35rem;
}
.fig.align-left { float: left; margin-right: 1.6rem; }
.fig.align-right { float: right; margin-left: 1.6rem; }
.fig.align-left img, .fig.align-right img { width: 100%; }
/* A float that outlives its own text lands in the next section, beside words
   it has nothing to do with. Headings stop it, and the body clears whatever
   is left — neither does anything on a page with no floated picture on it. */
.page-body h2, .page-body h3 { clear: both; }
.page-body::after { content: ""; display: block; clear: both; }
/* Text wrapping round a picture is always wrong on a phone: it leaves a column
   of words two or three to the line. Below this width the picture goes back to
   being a block, which is what it would have been anyway. */
@media (max-width: 40rem) {
  .fig.align-left, .fig.align-right {
    float: none; width: auto; margin-left: 0; margin-right: 0;
  }
}

/* The enlarged view. A native <dialog>, so Escape, focus trapping and inertness
   of the page behind come from the browser rather than from code that has to be
   maintained. */
.lb {
  border: 0; padding: 0; background: transparent; max-width: 100vw; max-height: 100vh;
  width: 100%; height: 100%;
}
.lb::backdrop { background: rgba(12, 12, 16, .88); }
.lb-fig {
  margin: 0; height: 100%; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: .75rem; padding: 3.5rem 1rem 2rem;
}
.lb-fig img {
  max-width: 100%; max-height: 100%; min-height: 0; object-fit: contain;
  border-radius: 3px; background: #000;
}
.lb-fig figcaption {
  color: #f2f2f4; font-size: .95rem; line-height: 1.45; text-align: center;
  max-width: 48rem; flex: none;
}
.lb-x, .lb-nav {
  position: absolute; background: rgba(255, 255, 255, .12); color: #fff; border: 0;
  cursor: pointer; border-radius: 50%; line-height: 1;
}
.lb-x { top: .75rem; right: .9rem; width: 2.4rem; height: 2.4rem; font-size: 1.6rem; }
.lb-nav { top: 50%; transform: translateY(-50%); width: 3rem; height: 3rem; font-size: 2rem; }
.lb-prev { left: .6rem; }
.lb-next { right: .6rem; }
.lb-x:hover, .lb-nav:hover { background: rgba(255, 255, 255, .26); }
.lb-x:focus-visible, .lb-nav:focus-visible { outline: 3px solid #fff; outline-offset: 2px; }
@media (max-width: 34rem) { .lb-nav { width: 2.4rem; height: 2.4rem; font-size: 1.5rem; } }

/* ─────────────────────────────────────────────────────────────────────
   SPACING — the gap between one part of a page and the next.

   Two halves. The DEFAULT is --space-block, read by every block, pattern and
   standalone picture instead of each one carrying its own margin, so a site's
   whole rhythm is one setting in /__design/. The OVERRIDE is the pair of
   classes below, written by the Space control on the page toolbar.

   ALWAYS A PAIR — u-space-t-<step> and u-space-b-<step> — even when the two
   sides match and a person thinks of it as one choice. One family of classes
   means linked and unlinked differ only in what gets written, never in how the
   CSS resolves, so there is no shorthand-versus-longhand fight to lose.

   THE DOUBLED CLASS IS DELIBERATE. `.callout { margin-block: var(--space-block) }`
   is one class of specificity and so is `.u-space-t-xl`, which would leave the
   winner decided by source order — and source order here is a generated
   injection point in a generated stylesheet, exactly the kind of thing that
   holds until somebody moves a marker. Doubling makes it win on merit.

   Last in the file, AFTER the @blocks and @patterns markers and after .fig, for
   the same reason.

   NEIGHBOURS SHARE A GAP: this is ordinary block flow, so two adjacent margins
   collapse and the bigger of the two wins. Setting `none` below something does
   not close the gap on its own if the thing underneath asks for space above it.
   Said out loud in the Space popover, because it is the one part of this that
   surprises people. Making .page-body a flex column would make every value
   exact and additive — and would change the rhythm of every paragraph and
   heading on every page already built, which is not a trade worth making for a
   rule one sentence explains. (2026-08-06)
   ───────────────────────────────────────────────────────────────────── */
/* One dial for a whole page (`space: tight|normal|roomy` in its frontmatter).
   It moves the DEFAULT, so a chunk that was given its own step keeps it — a
   page-wide setting that overrode the per-chunk ones would undo the finer
   decision every time it was touched. `normal` is written out rather than
   left implicit so the page can be put back without deleting the line. */
.page-body.space-tight  { --space-block: var(--space-xs); }
.page-body.space-normal { --space-block: var(--space-s); }
.page-body.space-roomy  { --space-block: var(--space-m); }

.u-space-t-none.u-space-t-none { margin-block-start: 0; }
.u-space-t-xs.u-space-t-xs     { margin-block-start: var(--space-xs); }
.u-space-t-s.u-space-t-s       { margin-block-start: var(--space-s); }
.u-space-t-m.u-space-t-m       { margin-block-start: var(--space-m); }
.u-space-t-l.u-space-t-l       { margin-block-start: var(--space-l); }
.u-space-t-xl.u-space-t-xl     { margin-block-start: var(--space-xl); }
.u-space-b-none.u-space-b-none { margin-block-end: 0; }
.u-space-b-xs.u-space-b-xs     { margin-block-end: var(--space-xs); }
.u-space-b-s.u-space-b-s       { margin-block-end: var(--space-s); }
.u-space-b-m.u-space-b-m       { margin-block-end: var(--space-m); }
.u-space-b-l.u-space-b-l       { margin-block-end: var(--space-l); }
.u-space-b-xl.u-space-b-xl     { margin-block-end: var(--space-xl); }

/* ─────────────────────────────────────────────────────────────────────
   HUGGING — closing the join between one band and the next.

   Cam, 2026-08-31: "there's some situations where it kinda needs to hug the
   bottom spot, or hug the top ... I hesitate to do, like, pixels. But maybe
   there can be some buttons that say remove bottom padding, remove top
   padding ... it would obviously have, like, a little bit of padding there,
   like, ten pixels or something like that still."

   WHY THIS IS NOT `u-space-t-none`. The spacing scale above sets ONE side of
   ONE element, and .page-body is ordinary block flow: adjacent margins
   collapse and the bigger of the two wins. A band asking for the smallest gap
   above itself therefore moves by nothing at all while the band above it asks
   for the normal one — which is every band, by default. That is the caveat the
   Space popover already has to explain ("to close a gap completely, set None
   on both sides of it"), and it is exactly the case this control is FOR. So a
   hug is a statement about the JOIN, and it reaches the neighbour to make it
   true: one class on one band, and the gap actually closes.

   `:has(+ …)` rather than editing the neighbour's source. The alternative was
   sending two statements per click — this band's top and the band above's
   bottom — which would mutate a band nobody clicked on, fail whenever the
   neighbour had moved on, and leave a stale setting behind the moment either
   band was re-ordered. The join is a rendering fact, so it is expressed in
   rendering.

   MARGIN WAS ONLY HALF THE GAP — the half most bands do not use. Cam,
   2026-08-31, testing a text block: "you click hug the bottom, and it just
   adds padding to the bottom ... obviously that's the opposite of what it
   should be doing." He was right, and the reason is that a band's breathing
   room is its own PADDING, not a margin: `.b-text` is `padding-block:
   var(--space-l)`, `.p-hero` and `.p-doors` the same shape. The first version
   of these rules set margins to --space-xs, so on a band whose margin was
   already zero a hug could only ever ADD twelve pixels — to this band and,
   through `:has(+ …)`, to a neighbour that had none either.

   SO A HUG SETS BOTH, AND ONLY EVER DOWNWARD. The hugged side's own padding
   comes down to --space-xs — "only having ten pixels left between the text and
   the bottom of the block" — and the margin on that side goes to zero, as does
   the neighbour's facing margin. Nothing here can enlarge a gap: the only
   value that rises is the padding of a band that had none, and that band was
   carrying a margin at least as large, which this same rule removes.

   NOT ZERO PADDING, because "it would obviously have, like, a little bit of
   padding there, like, ten pixels or something like that still" — the hug is
   the smallest step on the scale, not the absence of one. `none` is untouched
   and still means a zero margin on one side, so no page already built moves.

   THE NEIGHBOUR IS ONLY EVER RELAXED, never re-padded. Reaching across the
   join to zero a margin closes a gap the neighbour did not design; reaching
   across to collapse its padding would redraw a band nobody clicked on — a
   hero's inset is its design, not slack to be taken up.

   DOUBLED SELECTORS, for the same reason as the spacing utilities, and LAST in
   the file so a hug beats a chosen size on the neighbour — a person who asked
   this band to hug meant it.
   ───────────────────────────────────────────────────────────────────── */
.u-hug-t.u-hug-t {
  margin-block-start: 0;
  padding-block-start: var(--space-xs);
}
.u-hug-b.u-hug-b {
  margin-block-end: 0;
  padding-block-end: var(--space-xs);
}
*:has(+ .u-hug-t.u-hug-t) { margin-block-end: 0; }
.u-hug-b.u-hug-b + *      { margin-block-start: 0; }

/* A DRESSED BLOCK HAS TWO PADDINGS, and the hug has to reach both. `bg=` wraps
   the block in `.blk[data-bg]`, which brings its own inset — so the hug class,
   which lands on the outermost element, was closing the wrapper's padding while
   the band inside kept its own `--space-l` and the whole thing still read as
   un-hugged. The facing edge of the outermost child gives its padding up too,
   so what stays is the wrapper's ten pixels and nothing else. Only the facing
   edge, and only the outer child: a hug says where this band's content sits
   against this band's edge, and says nothing about the spacing within it. */
.blk.u-hug-t.u-hug-t > :first-child { padding-block-start: 0; }
.blk.u-hug-b.u-hug-b > :last-child  { padding-block-end: 0; }

/* ── ON PAPER THE BAND'S MARGIN ESCAPES, AND A HUG TRAPPED IT (2026-09-01) ─
   Measured on a fixture of stacked dressed blocks, ink of one block to ink of
   the next: "Hug the bottom" on a paper block took the join from 29.9px to
   41.9px. It made the gap BIGGER — the exact fault this whole mechanism was
   written to fix, still shipping on one ground.

   WHY. `.blk[data-bg="paper"]` carries no padding of its own, so a block that
   takes its breathing room as `margin-block` on its own band — gallery,
   timeline, slider, tabs — reaches the join by that margin COLLAPSING out
   through the padding-less wrapper. The hug then adds `padding-block-end:
   var(--space-xs)` to the wrapper, and padding is exactly what stops a child
   margin collapsing through. So the band's 29.9px stopped escaping, stayed
   inside, and the hug's twelve pixels were added underneath it.

   Every other ground was fine, and for the reason that explains the whole
   family: they carry `--blk-pad` already, so their children's margins never
   escaped and the hug has always simply brought that padding down. Measured
   unchanged here: mist and sand 106.7 → 80.3, navy 38.4 → 12.

   THE BAND GIVES UP THAT MARGIN ONLY WHILE THE BLOCK IS HUGGING, and only on
   paper, and only on the facing edge. The resting page does not move: the
   whole matrix measured byte-identical with this rule present and absent. */
.blk[data-bg="paper"].u-hug-b.u-hug-b > :last-child { margin-block-end: 0; }
/* …AND PAST THE HEADER, which is the common case rather than the odd one.
   A dressed block's first child is usually its <header>, not the band, so
   `> :first-child` lands on an element that has no padding to remove and the
   band behind it keeps its full gap — a hug that measured correctly on hug
   BOTTOM (where :last-child really is the band) and did nothing at all on hug
   TOP. Every example in the block picker ships with a header, so the case
   that did not work was the one a client would hit first. */
.blk.u-hug-t.u-hug-t > .blk-head + * { padding-block-start: 0; }

/* A COLOURED GROUND OWNS ITS VERTICAL INSET.
   `.blk[data-bg]` sets the gap for the whole dressed block, and a child that
   carries its own `padding-block` (the text block does — `var(--space-l)`)
   adds a second one inside the first: 40px of ground above the words and 40px
   below, on top of the wrapper's. On `paper` the wrapper has NO padding, so
   the child's rhythm is the only spacing there is and is exactly right —
   which is why this is scoped away from it rather than applied to all four.

   HORIZONTAL inset is deliberately NOT touched here. Only the text block
   insets itself, by `var(--gutter)`, and centring its reading measure inside
   the content column is the house look — a site that wants its blocks flush
   to one edge is making a house-style decision and says so in its own
   stylesheet, as CAMRO's register does. */
.blk[data-bg]:not([data-bg="paper"]) > * { padding-block: 0; }

/* ...EXCEPT DIRECTLY UNDER THE HEADER, and that includes paper.
   The note above is right about a dressed block STANDING ALONE: on paper the
   child's own band padding is the only rhythm there is. It is wrong the
   moment there is a header, because then the child's top padding stacks
   underneath the header's own margin and the heading floats a whole band
   above the words it names — 24px of margin plus 80px of band on a text
   block, which is what Cam saw first (2026-08-31): "heading for this section
   and 'write words here' are, like, really far apart."
   Every example the block picker ships carries a header, so this was the
   first thing a client met. Same shape as the hug rule above, and for the
   same reason: the header sets that gap, so the child must not set it too. */
.blk[data-bg] > .blk-head + * { padding-block-start: 0; }

/* ── A HUG ON PAPER, UNDER A HEADER, HAS NOTHING LEFT TO TAKE ─────────────
   Found 2026-09-01 in a row of the [data-bg] acceptance fixture that I had
   not thought to build — my own matrix had paper, but not paper against a
   neighbour, and the gap only shows in the join.

   Every other ground reaches its ten pixels by coming DOWN: `.blk[data-bg]`
   carries `--blk-pad` and the hug takes it to `--space-xs`. Paper carries
   none, and with a header the band's top padding is already zero from the
   rule directly above. So on that one join the hug's ten pixels are pure
   ADDITION — 0px before, 12px after — which is the very fault this whole
   mechanism was written to fix, surviving on the one ground nobody tested.

   ONE JOIN OF FOUR, which is why this is `:has(> .blk-head)` and not a ban on
   paper. Measured, wrapper + band totals: paper with no header hugs correctly
   at both ends (79.9 → 12.0), and "Hug the bottom" works on every paper block
   (79.9 → 12.0). Only the headered top join is broken. The first version of
   this fix zeroed paper's padding on both edges — "never worse", but it
   quietly turned three working joins into flush zero. One rule, one join.

   WHY NOT REACH FURTHER. What actually sets that gap is the band padding of
   the block ABOVE. A hug could close it by repainting the neighbour, and that
   is the line this design does not cross: the neighbour is relaxed, never
   re-padded, or a hug redraws a band nobody clicked on.

   `:has()` IS ALREADY LOAD-BEARING HERE — the neighbour rule above uses it,
   and has since the hug shipped. So a browser without it sees TODAY's
   behaviour coherently (this no-op ignored AND that neighbour rule already
   inert), not a half-applied state. No new requirement is added by this line.

   The editor also stops offering the button on such a block; this rule is
   what keeps a `hug="top"` written by hand from making a page worse. */
.blk[data-bg="paper"].u-hug-t.u-hug-t:has(> .blk-head) { padding-block-start: 0; }
