/* IngotForge — default public theme.
   Palettes are switched by data-theme on <html>; the server renders the
   default_theme setting and an inline head script applies the visitor's
   stored preference before first paint.
   The --tb-* topbar variables are emitted by the layout from site settings:
   they are brand colors chosen by the owner and stay identical in both
   themes. Only page surfaces follow the theme.

   The accent doubles as link text, and an arbitrary brand color cannot clear
   WCAG AA on both backgrounds. So the layout also emits --accent-ink-dark and
   --accent-ink-light (App\Core\Color::readableOn, hue preserved) plus
   --accent-on (black or white, for text sitting on an accent fill).
   Rule of thumb: --accent-ink for accent TEXT, --tb-accent for fills. */

:root,
[data-theme="dark"] {
  --bg: #14171f;
  --surface: #1c212d;
  --ink: #e6e8f0;
  --muted: #9aa3b2;
  --line: #2a3040;
  --code-bg: #0f131b;
  --code-ink: #e6e8f0;
  --shadow: rgba(0, 0, 0, .45);
  --accent-ink: var(--accent-ink-dark, #e8a33d);
}

[data-theme="light"] {
  --bg: #fbfbfd;
  --surface: #ffffff;
  --ink: #23283a;
  --muted: #667085;
  --line: #e6e8f0;
  --code-bg: #23283a;
  --code-ink: #e6e8f0;
  --shadow: rgba(15, 20, 35, .12);
  --accent-ink: var(--accent-ink-light, #996212);
}

* { box-sizing: border-box; margin: 0; }

/* ---- Layout widths ----
   Two widths, one job each.

   --layout-max is the SHELL: topbar, footer, wide sections and the inner
   column of a full-bleed band. It takes 80% of the viewport but never drops
   below 1080px, so a large monitor is actually used while a laptop keeps the
   width the site was designed at. Below ~1350px the floor wins, and below
   ~1100px the element's own width:100% wins — which is why no media query is
   needed for phones.

   --reading-max is the narrow token, and stays narrow on purpose: a section
   set to "narrow" is asking for a comfortable measure, not for 80% of a
   27-inch screen. */
:root {
  --layout-max: max(80vw, 1080px);
  --reading-max: 620px;
}
html { font-size: var(--base-size, 16px); scroll-behavior: smooth; }
body {
  font-family: var(--font-body, system-ui, -apple-system, "Segoe UI", sans-serif);
  background: var(--bg);
  color: var(--ink);
  line-height: var(--line-height, 1.65);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  /* Full-bleed sections are sized in vw, which counts the scrollbar and would
     otherwise add a horizontal one. `clip` rather than `hidden`: hidden would
     make this a scroll container and break the sticky topbar. */
  overflow-x: clip;
}

/* Base link colour. Low specificity on purpose: .topbar .menu a, .lang-dropdown li a,
   .page .body a and .property-card (color: inherit) all override it. Without this,
   links outside those scopes — e.g. module templates — fall back to UA blue. */
a { color: var(--accent-ink); }

/* ---- Preview banner ---- */
.preview-banner {
  background: repeating-linear-gradient(45deg, #b45309, #b45309 12px, #92400e 12px, #92400e 24px);
  color: #fff; text-align: center; font-weight: 700; font-size: .85rem;
  padding: .45rem .8rem; letter-spacing: .04em;
}

/* ---- Topbar ----
   Position is a setting: sticky (the default, and what the site always did),
   fixed, or static. Only `fixed` leaves the flow, so only `fixed` needs the
   body padded — otherwise the first section would start underneath the bar. */
.topbar {
  position: sticky; top: 0; z-index: 100;
  background: var(--tb-bg, #1f2430);
  color: var(--tb-fg, #f5f6f8);
  box-shadow: 0 1px 0 rgba(0,0,0,.12), 0 4px 16px var(--shadow);
}
/* `relative` rather than `static`: it renders identically (no offsets, still in
   the flow) but it is a containing block, which the drop-down menu panel below
   needs to hang off the bar instead of off the page. */
.topbar.tb-static { position: relative; }
.topbar.tb-fixed { position: fixed; top: 0; left: 0; right: 0; }
body.has-fixed-topbar { padding-top: var(--tb-total, var(--tb-h, 56px)); }
/* Anchors must not land under the bar when it covers the top of the page. */
html:has(.topbar.tb-fixed), html:has(.topbar.tb-sticky) { scroll-padding-top: var(--tb-total, var(--tb-h, 56px)); }

.topbar-inner {
  max-width: var(--layout-max); margin: 0 auto;
  /* Height is the setting; padding only decides the sides. min-height rather
     than height so a wrapped menu on a narrow screen can still grow. */
  min-height: var(--tb-h, 56px);
  padding: 0 var(--tb-pad, 1.2rem);
  display: flex; align-items: center; gap: 1.4rem;
  transition: min-height .18s ease;
}
/* Shrink on scroll, when the owner asked for it. The class is added by site.js
   and means nothing on its own. */
.topbar.is-scrolled .topbar-inner { min-height: var(--tb-h-min, 44px); }
@media (prefers-reduced-motion: reduce) { .topbar-inner { transition: none; } }
.topbar .brand { color: var(--tb-fg, #f5f6f8); text-decoration: none; font-weight: 700; font-size: 1.05rem; display: flex; align-items: center; }
.topbar .brand img { max-height: 34px; display: block; }
.topbar .menu { display: flex; gap: .2rem; flex: 1; flex-wrap: wrap; }
/* With no brand the menu IS the bar's left edge, so it has to sit ON the
   page's grid rather than one link-padding inside it — the brand's text
   started at the padding edge and the menu's first label would otherwise
   start 10px further in, misaligned against the h1 below it. Pulled back by
   exactly the link padding, and only while the menu is a row: the collapsed
   panel is absolutely positioned with its own padding and resets this. */
.topbar-inner:not(:has(.brand)) > .menu { margin-left: -.65rem; }
.topbar .menu a {
  color: var(--tb-fg, #f5f6f8); opacity: .82; text-decoration: none; font-size: .93rem;
  padding: .35rem .65rem; border-radius: 7px;
  display: inline-flex; align-items: center; gap: .4rem;
}
/* em, so an icon follows the menu's type rather than needing its own size the
   next time the theme's base size moves. */
.topbar .menu .menu-icon { width: 1.1em; height: 1.1em; flex-shrink: 0; }
.topbar .menu a:hover { opacity: 1; background: rgba(255,255,255,.08); }
.topbar-right { display: flex; align-items: center; gap: .7rem; margin-left: auto; }

/* ---- Header layouts (v0.17.0) ----
   `inline` is the bar as it always was and carries no class. The two others
   give the menu a row of its own under the logo: `centered` puts the logo in
   the middle with the menu centred below, `stacked` keeps the logo on the left.
   A grid only on a wide screen — under the breakpoint the menu is behind the
   hamburger (or, with JavaScript off, wraps), and one row is what fits.

   --tb-total is the bar's whole height. Everything that makes room for the bar
   (a fixed bar's body padding, anchor scroll padding, a full-screen section)
   reads it, so a two-row bar is never mistaken for a one-row one. */
:root { --tb-total: var(--tb-h, 56px); --tb-row2: 2.9rem; }
@media (min-width: 861px) {
  :root:has(.topbar.tb-layout-centered),
  :root:has(.topbar.tb-layout-stacked) { --tb-total: calc(var(--tb-h, 56px) + var(--tb-row2)); }
  .topbar:is(.tb-layout-centered, .tb-layout-stacked) .topbar-inner {
    display: grid; column-gap: 1.4rem; row-gap: 0; min-height: 0;
    grid-template-rows: var(--tb-h, 56px) minmax(var(--tb-row2), auto);
    transition: grid-template-rows .18s ease;
  }
  .topbar:is(.tb-layout-centered, .tb-layout-stacked).is-scrolled .topbar-inner {
    grid-template-rows: var(--tb-h-min, 44px) minmax(var(--tb-row2), auto);
  }
  .topbar.tb-layout-centered .topbar-inner { grid-template-columns: 1fr auto 1fr; grid-template-areas: ". brand right" "menu menu menu"; }
  .topbar.tb-layout-stacked .topbar-inner { grid-template-columns: auto 1fr; grid-template-areas: "brand right" "menu menu"; }
  .topbar:is(.tb-layout-centered, .tb-layout-stacked) .brand { grid-area: brand; }
  .topbar.tb-layout-centered .brand { justify-self: center; }
  .topbar:is(.tb-layout-centered, .tb-layout-stacked) .menu { grid-area: menu; align-self: center; padding-bottom: .35rem; }
  /* Centred under the logo; stacked keeps the first label on the page's own
     column, the same pull-back a brandless inline bar uses. */
  .topbar.tb-layout-centered .menu { justify-content: center; margin-left: 0; }
  .topbar.tb-layout-stacked .menu { margin-left: -.65rem; }
  .topbar:is(.tb-layout-centered, .tb-layout-stacked) .topbar-right { grid-area: right; justify-self: end; }
}
@media (prefers-reduced-motion: reduce) { .topbar-inner { transition: none !important; } }

/* ---- See-through bar over an opening hero (v0.17.0) ----
   Opt-in (`topbar_transparent`), and only on a page whose FIRST section is
   full-screen: anywhere else there is nothing for the bar to sit over, so it
   is the ordinary bar. Decided here rather than in PHP so the builder's live
   preview follows a section being made full-screen or moved.

   It is not actually transparent, and that is the point: a photo has no colour
   to derive an ink from, so the bar keeps a dark scrim at the overlay
   doctrine's alpha (#f5f6f8 over .6 of the scrim clears 4.5:1 even over a pure
   white photo) and fixes its own ink. Past the hero site.js adds
   .is-past-hero and the owner's own colours come back; with JavaScript off it
   simply stays on the scrim, which is still readable. */
body:has(> .topbar.tb-transparent):has(> .page > article > .body > .sec-height-screen:first-child) > .topbar,
body:has(> .topbar.tb-transparent):has(> .page > article > .body > .sec-outline:first-child > .sec-height-screen) > .topbar {
  position: fixed; top: 0; left: 0; right: 0;
}
body:has(> .topbar.tb-transparent):has(> .page > article > .body > .sec-height-screen:first-child) > .topbar.tb-static,
body:has(> .topbar.tb-transparent):has(> .page > article > .body > .sec-outline:first-child > .sec-height-screen) > .topbar.tb-static {
  position: absolute;
}
body:has(> .topbar.tb-transparent):has(> .page > article > .body > .sec-height-screen:first-child) > .topbar:not(.is-past-hero),
body:has(> .topbar.tb-transparent):has(> .page > article > .body > .sec-outline:first-child > .sec-height-screen) > .topbar:not(.is-past-hero) {
  --tb-fg: #f5f6f8; --tb-bg: #1f2430;
  background: rgba(14, 16, 22, .6); box-shadow: none;
  transition: background-color .2s ease;
}
body.has-fixed-topbar:has(> .topbar.tb-transparent):has(> .page > article > .body > .sec-height-screen:first-child),
body.has-fixed-topbar:has(> .topbar.tb-transparent):has(> .page > article > .body > .sec-outline:first-child > .sec-height-screen) { padding-top: 0; }
body:has(> .topbar.tb-transparent) > .page:has(> article > .body > .sec-height-screen:first-child),
body:has(> .topbar.tb-transparent) > .page:has(> article > .body > .sec-outline:first-child > .sec-height-screen) { padding-top: 0; }
/* The hero carries its own heading, so the page title steps out of the way.
   Kept for screen readers. */
body:has(> .topbar.tb-transparent) > .page > article:has(> .body > .sec-height-screen:first-child) > h1,
body:has(> .topbar.tb-transparent) > .page > article:has(> .body > .sec-outline:first-child > .sec-height-screen) > h1 {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; margin: 0;
}
/* The hero reaches the top of the window and keeps its words below the bar. */
body:has(> .topbar.tb-transparent) > .page > article > .body > .sec-height-screen:first-child,
body:has(> .topbar.tb-transparent) > .page > article > .body > .sec-outline:first-child > .sec-height-screen {
  min-height: 100svh; padding-top: calc(var(--tb-total, var(--tb-h, 56px)) + 1.5rem);
}

/* ---- Dropdown menu items ----
   One level, and the panel is revealed by CSS rather than by un-hiding an
   attribute.

   Two modes, and only ever one at a time. With JavaScript off, :hover and
   :focus-within open the panel, so pointer and keyboard both reach it — the
   server ships a menu that already works, the same shape as the hamburger.
   site.js then adds .has-submenu-js, which switches those OFF and leaves the
   click toggle as the only opener.

   The switch is not a preference, it is a correctness fix: with both live,
   Escape removed .is-open while the pointer was still over the caret, :hover
   immediately re-opened the panel, and the key appeared to do nothing. It also
   makes the two dropdowns on this bar behave alike — the language switcher
   beside them has always been click-only. */
/* ONE control. The parent of a dropdown is always a single button carrying
   the label, the icon and the arrow — see the note in topbar.php for why the
   link-plus-caret pair it replaced was wrong. Nothing here has to reconcile
   two children or care which side the icon is on. */
.topbar .menu .menu-item { position: relative; display: flex; align-items: center; border-radius: 7px; }
.topbar .menu .menu-item:hover,
.topbar .menu .menu-item.is-open { background: rgba(255,255,255,.08); }

.topbar .menu .menu-caret {
  display: inline-flex; align-items: center; gap: .4rem;
  background: none; border: 0; cursor: pointer;
  color: var(--tb-fg, #f5f6f8); opacity: .82; font: inherit; font-size: .93rem;
  padding: .35rem .65rem;
}
.topbar .menu .menu-caret:focus-visible { outline: 2px solid var(--tb-accent, #e8a33d); outline-offset: 2px; }
.topbar .menu .menu-caret-art { width: .85em; height: .85em; flex-shrink: 0; transition: transform .15s ease; }
.topbar .menu .menu-item.is-open .menu-caret-art { transform: rotate(180deg); }

.topbar .menu .menu-sub {
  display: none;
  position: absolute; top: 100%; left: 0; z-index: 210;
  min-width: 12rem; margin: 0; padding: .35rem; list-style: none;
  background: var(--tb-bg, #1f2430);
  border: 1px solid rgba(255,255,255,.14); border-radius: 10px;
  box-shadow: 0 14px 34px var(--shadow);
}
.topbar:not(.has-submenu-js) .menu .menu-item:hover > .menu-sub,
.topbar:not(.has-submenu-js) .menu .menu-item:focus-within > .menu-sub,
.topbar .menu .menu-item.is-open > .menu-sub { display: block; }
.topbar .menu .menu-sub a { display: flex; width: 100%; padding: .45rem .6rem; font-size: .9rem; opacity: .9; }
.topbar .menu .menu-sub a:hover { opacity: 1; }
/* The parent's own page, which the button above no longer navigates to. Set
   apart by a rule so it reads as "this section" rather than as a child that
   happens to share the heading's name. */
.topbar .menu .menu-sub .menu-sub-self {
  font-weight: 600; opacity: 1;
  border-bottom: 1px solid rgba(255,255,255,.14);
  margin-bottom: .25rem; padding-bottom: .5rem;
}
/* A panel opens from its button's left edge, rightwards, whatever the item's
   position in the menu. Only when there is not room for it before the edge of
   the window does site.js add .menu-sub-end, measured at the moment it opens,
   and the panel hangs off the button's right edge instead. Guessing from the
   position (the last item always flipped) opened menus backwards on every bar
   whose last entry was a dropdown, with plenty of room to its right. */
.topbar .menu .menu-item > .menu-sub.menu-sub-end { left: auto; right: 0; }

/* ---- Narrow-screen menu ----
   Everything here is scoped to .has-menu-toggle, which site.js adds. Without
   JavaScript the class is absent, the menu never collapses, and it wraps onto
   a second line exactly as it did before — the toggle ships `hidden` for the
   same reason. Open/closed is .is-menu-open on the HEADER rather than `hidden`
   on the nav, so widening the window past the breakpoint reveals the menu
   again by itself. */
/* Scoped to .topbar deliberately: the button also carries .tb-btn, whose
   `display: inline-flex` is declared FURTHER DOWN this file at the same
   specificity — a bare `.menu-toggle` here loses the tie and the hamburger
   shows at every width. */
.topbar .menu-toggle { display: none; }
.topbar .menu-toggle .icon-close { display: none; }
@media (max-width: 860px) {
  .topbar.has-menu-toggle .menu-toggle { display: inline-flex; }
  .topbar.has-menu-toggle.is-menu-open .menu-toggle .icon-bars { display: none; }
  .topbar.has-menu-toggle.is-menu-open .menu-toggle .icon-close { display: block; }

  .topbar.has-menu-toggle .menu {
    display: none; margin-left: 0;
    position: absolute; left: 0; right: 0; top: 100%; z-index: 200;
    flex-direction: column; flex-wrap: nowrap; gap: .1rem;
    padding: .5rem var(--tb-pad, 1.2rem) .8rem;
    background: var(--tb-bg, #1f2430);
    border-top: 1px solid rgba(255,255,255,.12);
    box-shadow: 0 12px 24px var(--shadow);
    /* A long menu on a short phone scrolls inside the panel rather than off
       the bottom of it. */
    max-height: calc(100vh - var(--tb-h, 56px)); overflow-y: auto;
  }
  .topbar.has-menu-toggle.is-menu-open .menu { display: flex; }
  .topbar.has-menu-toggle .menu a { padding: .6rem .5rem; font-size: 1rem; opacity: .95; }

  /* Inside the panel a dropdown stacks instead of floating: an absolutely
     positioned card over a column of links has nothing to sit beside, and on a
     phone it would cover the items under it. The parent's button spans the
     row, so the whole width is the tap target and the arrow sits at the far
     edge where a thumb expects it. */
  .topbar.has-menu-toggle .menu .menu-item { display: block; width: 100%; }
  .topbar.has-menu-toggle .menu .menu-caret { width: 100%; padding: .6rem .5rem; font-size: 1rem; }
  .topbar.has-menu-toggle .menu .menu-caret .menu-caret-art { margin-left: auto; }
  .topbar.has-menu-toggle .menu .menu-sub {
    position: static; min-width: 0; margin: 0 0 .2rem .8rem; padding: 0;
    background: none; border: 0; border-left: 1px solid rgba(255,255,255,.16);
    border-radius: 0; box-shadow: none;
  }
  /* Nothing here has to switch hover off: .has-menu-toggle only exists when
     site.js ran, and site.js also sets .has-submenu-js, which took the hover
     and focus rules out of play. The click toggle is the only opener at this
     width, which is what a touch screen needs. */
  .topbar.has-menu-toggle .menu .menu-item.is-open > .menu-sub { display: block; }
}

/* ---- Topbar icon buttons (theme toggle, language dropdown trigger) ---- */
/* One square box for both, so the language trigger and the theme toggle are
   the same size. min-width rather than width so a long code (fr-ca) grows the
   button instead of overflowing it. */
.tb-btn {
  display: inline-flex; align-items: center; justify-content: center;
  height: 34px; min-width: 34px; padding: 0 .35rem;
  background: transparent; border: 1px solid rgba(255,255,255,.18); border-radius: 8px;
  color: var(--tb-fg, #f5f6f8); font: inherit; font-size: .82rem; font-weight: 600;
  cursor: pointer; line-height: 1;
}
.tb-btn:hover { background: rgba(255,255,255,.1); }
.tb-btn:focus-visible { outline: 2px solid var(--tb-accent, #e8a33d); outline-offset: 2px; }
.tb-btn svg { width: 16px; height: 16px; display: block; fill: currentColor; }
/* Only one of the two icons shows, depending on the active theme. Selectors
   are scoped to .tb-btn so they outrank the `.tb-btn svg` display above —
   without that the sun stayed visible and the button was wider in light. */
.tb-btn .icon-sun { display: none; }
[data-theme="dark"] .tb-btn .icon-sun { display: block; }
[data-theme="dark"] .tb-btn .icon-moon { display: none; }

/* ---- Language dropdown ---- */
.lang-dropdown { position: relative; }
.lang-dropdown ul {
  position: absolute; right: 0; top: calc(100% + .4rem); z-index: 200;
  list-style: none; margin: 0; padding: .3rem;
  min-width: 160px; max-height: 60vh; overflow-y: auto;
  background: var(--surface); border: 1px solid var(--line); border-radius: 9px;
  box-shadow: 0 10px 30px var(--shadow);
}
.lang-dropdown ul[hidden] { display: none; }
.lang-dropdown li a {
  display: block; padding: .45rem .6rem; border-radius: 6px;
  color: var(--ink); text-decoration: none; font-size: .88rem; white-space: nowrap;
}
.lang-dropdown li a:hover { background: var(--bg); text-decoration: none; }
.lang-dropdown li a[aria-current="true"] { font-weight: 700; color: var(--accent-ink); }
.lang-dropdown li a .code { font-weight: 700; margin-right: .4rem; }
@media (max-width: 640px) {
  .lang-dropdown ul { min-width: 190px; }
  .lang-dropdown li a { padding: .6rem; }
}

/* ---- Back to top ----
   Hidden until there is something to go back up to; site.js adds .is-on past
   one viewport of scrolling. Accent fill with the derived ink on it, never a
   hardcoded white — the accent doctrine applies here as everywhere. */
.to-top {
  position: fixed; right: 1.2rem; bottom: 1.2rem; z-index: 90;
  width: 2.8rem; height: 2.8rem; display: grid; place-items: center;
  border: 0; border-radius: 999px; cursor: pointer;
  background: var(--tb-accent, #e8a33d); color: var(--accent-on, #14171f);
  box-shadow: 0 8px 22px var(--shadow);
  opacity: 0; visibility: hidden; transform: translateY(.5rem);
  transition: opacity .18s ease, transform .18s ease, visibility .18s;
}
.to-top.is-on { opacity: 1; visibility: visible; transform: none; }
.to-top svg { width: 1.2rem; height: 1.2rem; fill: currentColor; display: block; }
.to-top:hover { filter: brightness(1.06); }
.to-top:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .to-top { transition: none; } }

/* ---- Content ---- */
.page {
  max-width: var(--layout-max); width: 100%; margin: 0 auto; flex: 1;
  padding: calc(2.5rem * var(--rhythm, 1)) 1.2rem calc(3rem * var(--rhythm, 1));
}
.page h1 {
  font-family: var(--font-heading, inherit);
  font-size: 2rem; line-height: 1.25; margin-bottom: calc(1.2rem * var(--rhythm, 1));
}
.page .body h2 { font-family: var(--font-heading, inherit); font-size: 1.4rem; margin: calc(1.6rem * var(--rhythm, 1)) 0 .6rem; }
.page .body h3 { font-family: var(--font-heading, inherit); font-size: 1.15rem; margin: calc(1.3rem * var(--rhythm, 1)) 0 .5rem; }
.page .body p { margin: calc(.8rem * var(--rhythm, 1)) 0; }
.page .body ul, .page .body ol { margin: .8rem 0 .8rem 1.4rem; }
.page .body a { color: var(--accent-ink); }
.page .body img { max-width: 100%; height: auto; border-radius: var(--radius, 8px); }
.page .body table { border-collapse: collapse; margin: 1rem 0; width: 100%; }
.page .body table td, .page .body table th { border: 1px solid var(--line); padding: .5rem .7rem; text-align: left; }
.page .body blockquote { border-left: 3px solid var(--tb-accent, #e8a33d); padding: .2rem 0 .2rem 1rem; color: var(--muted); margin: 1rem 0; }
.page .body pre { background: var(--code-bg); color: var(--code-ink); padding: 1rem; border-radius: 8px; overflow-x: auto; font-size: .88rem; }

.notfound { text-align: center; padding: 3rem 0; }
.notfound h1 { margin-bottom: .8rem; }
.notfound .button {
  display: inline-block; margin-top: 1rem; background: var(--tb-accent, #e8a33d); color: var(--accent-on, #fff);
  padding: .55rem 1.2rem; border-radius: 8px; text-decoration: none; font-weight: 600;
}

/* ---- Footer ----
   Built in Settings > Footer: a column grid of typed elements over a bottom
   bar. The layout token from the stored envelope becomes a class here — the
   renderer never emits inline grid CSS.

   Colours: data-bg="theme" uses the page palette, so links keep --accent-ink,
   which is already derived per theme. The other three modes sit on a surface
   the owner chose, so the layout emits --footer-bg / --footer-fg /
   --footer-ink derived from it with App\Core\Color. Nothing here hardcodes
   #fff or #000 on an accent or custom surface — that is the recurring failure
   mode this derivation exists to prevent. */
.site-footer {
  margin-top: auto;
  background: var(--footer-bg, var(--bg));
  color: var(--footer-fg, var(--ink));
  border-top: 1px solid var(--line);
  font-size: .9rem;
  /* The topbar casts a shadow down onto the page; the footer casts the mirror
     of it upward, so the two bands bracket the content the same way. A shadow
     paints outside its own box, and the footer comes LAST in the flow, so it
     needs a stacking context of its own or the section above simply paints
     over it. */
  position: relative; z-index: 1;
  box-shadow: 0 -1px 0 rgba(0,0,0,.12), 0 -4px 16px var(--shadow);
}
.site-footer:not([data-bg="theme"]) { border-top: 0; }
.footer-inner { max-width: var(--layout-max); margin: 0 auto; padding: 2.2rem 1.2rem 1.4rem; }
.site-footer a { color: var(--footer-ink, var(--accent-ink)); text-decoration: underline; text-underline-offset: .18em; }
.site-footer a:hover { text-decoration-thickness: 2px; }
.site-footer a:focus-visible { outline: 2px solid var(--footer-ink, var(--accent-ink)); outline-offset: 3px; border-radius: 3px; }

.footer-cols { display: grid; gap: 1.6rem 2rem; padding-bottom: 1.6rem; }
.footer-cols-1 { grid-template-columns: 1fr; }
.footer-cols-1-1 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.footer-cols-1-1-1 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.footer-cols-1-1-1-1 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.footer-cols-2-1 { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
.footer-cols-1-2 { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); }
.footer-cols-2-1-1 { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr); }
.footer-cols-1-1-2 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 2fr); }
.footer-col { display: flex; flex-direction: column; gap: 1.4rem; min-width: 0; }

/* A long unbroken URL in a label must not push the page sideways. */
.footer-col, .footer-bottom { overflow-wrap: anywhere; }

.fel { min-width: 0; }
.fel-heading { font-size: .82rem; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; margin: 0 0 .6rem; }
.fel-text-body p { margin: 0 0 .5rem; }
.fel-text-body p:last-child { margin-bottom: 0; }
.fel-text-body.align-center { text-align: center; }
.fel-text-body.align-right { text-align: right; }

.fel-link-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .35rem; }
.fel-link-list a { text-decoration: none; }
.fel-link-list a:hover { text-decoration: underline; }

.fel-contact-body { font-style: normal; display: flex; flex-direction: column; gap: .4rem; }
.fel-contact-body .fel-org { font-weight: 700; }
.fel-contact-body .fel-label { display: block; font-size: .8rem; opacity: .85; }
.fel-contact-body a { text-decoration: none; }
.fel-contact-body a:hover { text-decoration: underline; }

.fel-social-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: .55rem; }
.fel-social-list a {
  display: inline-flex; align-items: center; gap: .45rem; text-decoration: none;
  color: var(--footer-fg, var(--ink));
  border: 1px solid currentColor; border-radius: 8px; padding: .38em .55em; line-height: 1;
}
/* Sized in em so the icon-only and icon-with-name styles share one rule. */
.fel-social-list svg { width: 1.15em; height: 1.15em; fill: currentColor; display: block; }
.fel-social-list.style-icon a { padding: .45em; }
.fel-social-list a:hover { color: var(--footer-ink, var(--accent-ink)); }

.fel-logo-img { display: block; height: auto; width: 100%; }
.fel-tagline { margin: .6rem 0 0; font-size: .88rem; }
.fel-html-body > *:first-child { margin-top: 0; }
.fel-html-body img { max-width: 100%; height: auto; }

.footer-bottom {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .6rem 1.2rem;
  padding-top: 1.1rem; font-size: .84rem;
  /* --footer-rule is emitted alongside --footer-fg so the divider is a soft
     tint of the derived foreground rather than a hard line. */
  border-top: 1px solid var(--footer-rule, var(--line));
}
.footer-inner > .footer-bottom:only-child { border-top: 0; padding-top: 0; text-align: center; justify-content: center; }
.footer-copy { margin: 0; }
.footer-legal { display: flex; flex-wrap: wrap; gap: .3rem 1rem; }
.footer-legal a { text-decoration: none; }
.footer-legal a:hover { text-decoration: underline; }

@media (max-width: 720px) {
  .footer-cols { grid-template-columns: 1fr !important; gap: 1.6rem; }
  .footer-inner { padding: 1.8rem 1.1rem 1.2rem; }
  .footer-bottom { justify-content: flex-start; }
}

/* ---- Blocks (page builder output) ---- */
.blk-heading { font-family: var(--font-heading, inherit); margin: calc(1.6rem * var(--rhythm, 1)) 0 .6rem; line-height: 1.25; }
h2.blk-heading { font-size: 1.4rem; }
h3.blk-heading { font-size: 1.15rem; }
h4.blk-heading { font-size: 1rem; }
.blk-richtext > *:first-child { margin-top: 0; }
/* Prose keeps a readable measure inside the wide shell. The layout is 80% of
   the viewport so grids, galleries and property cards can use the screen —
   but a paragraph 1500px wide is unreadable, and "use the space" was never a
   request to widen body copy. Left-anchored rather than centred, so an
   author's alignment token still decides where the column sits; centred
   sections re-centre it below. ch, so the measure follows the theme's font
   size instead of being a second number to keep in sync. */
.blk-richtext, .page .body { max-width: 78ch; }
/* ...but a block-built page's `.body` is the SECTION CONTAINER, and clamping
   that is a different thing entirely: `.sec-width-wide` / `.sec-width-full`
   are centred on their parent (`left: 50%; translateX(-50%)`), so a 78ch
   parent hung them hundreds of pixels off the LEFT of the page, and every
   other section was stuck at reading width inside an 80vw shell. Prose keeps
   its measure per rich-text block, which is where it belongs. The legacy
   `body_html` path has no blocks and so keeps the cap above. */
.page .body-sections { max-width: none; }
.sec-align-center .blk-richtext { margin-inline: auto; }
.sec-align-right .blk-richtext { margin-left: auto; }
/* A narrow section is already at a reading width — no second clamp. */
.sec-width-narrow .blk-richtext { max-width: none; }

/* ---- Video ----
   aspect-ratio rather than the padding-top trick: the box is the shape of the
   video, so the section below it never jumps once the player loads. 16/9 for
   every provider — a vertical Short letterboxes inside its own player, which is
   what the provider's own page does too. */
.blk-video { margin: 1.4rem 0; aspect-ratio: 16 / 9; border-radius: var(--radius, 8px); overflow: hidden; background: #000; }
.blk-video iframe { width: 100%; height: 100%; border: 0; display: block; }
/* A library file keeps its own proportions. */
.blk-video.is-file { aspect-ratio: auto; }
.blk-video video { width: 100%; max-height: 80vh; display: block; }

/* ---- Countdown ---- */
.blk-countdown { margin: 1.4rem 0; }
.blk-countdown.align-center { text-align: center; }
.blk-countdown.align-right { text-align: right; }
.countdown-heading { font-weight: 600; margin-bottom: .6rem; }
.countdown-units { display: flex; gap: .8rem; flex-wrap: wrap; }
.blk-countdown.align-center .countdown-units { justify-content: center; }
.blk-countdown.align-right .countdown-units { justify-content: flex-end; }
.countdown-units[hidden], .countdown-done[hidden], .countdown-date[hidden] { display: none; }
.countdown-unit {
  min-width: 4.5rem; padding: .6rem .4rem; border-radius: var(--radius, 8px);
  background: var(--surface); border: 1px solid var(--line); text-align: center;
}
.countdown-value {
  display: block; font-size: 1.8rem; font-weight: 800; line-height: 1.1;
  /* Digits change every second; without tabular figures the boxes twitch. */
  font-variant-numeric: tabular-nums;
}
.countdown-label { display: block; font-size: .72rem; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
.countdown-done { font-size: 1.2rem; font-weight: 700; color: var(--accent-ink); }
.countdown-date { color: var(--muted); font-size: .9rem; margin-top: .4rem; }

.blk-image { margin: 1.2rem 0; }
.blk-image img { max-width: 100%; height: auto; border-radius: var(--radius, 8px); display: block; }
.blk-image figcaption { color: var(--muted); font-size: .84rem; margin-top: .4rem; }
.blk-image.align-center { text-align: center; }
.blk-image.align-center img, .blk-image.align-center figcaption { margin-inline: auto; }
.blk-image.align-right { text-align: right; }
.blk-image.align-right img { margin-left: auto; }
.blk-image.align-full img { width: 100%; }

.blk-gallery { display: grid; gap: .6rem; margin: 1.2rem 0; }
.blk-gallery.cols-2 { grid-template-columns: repeat(2, 1fr); }
.blk-gallery.cols-3 { grid-template-columns: repeat(3, 1fr); }
.blk-gallery.cols-4 { grid-template-columns: repeat(4, 1fr); }
.blk-gallery img { width: 100%; aspect-ratio: 4/3; object-fit: cover; border-radius: var(--radius, 8px); display: block; }

.blk-columns { display: grid; gap: 1.4rem; margin: 1.2rem 0; }
.blk-columns.cols-2 { grid-template-columns: repeat(2, 1fr); }
.blk-columns.cols-3 { grid-template-columns: repeat(3, 1fr); }
.blk-column > *:first-child { margin-top: 0; }

.blk-button { margin: 1.2rem 0; }
.blk-button.align-center { text-align: center; }
.blk-button.align-right { text-align: right; }
.blk-button a {
  display: inline-block; padding: .55rem 1.3rem; border-radius: var(--radius, 8px);
  font-weight: 600; text-decoration: none;
}
/* `a.btn-*` rather than a descendant selector: `.page .body a` (0,2,1) would
   otherwise outrank `.blk-button .btn-primary` (0,2,0) and paint the label in
   accent ink ON an accent fill — an invisible button. Adding the element to
   the selector takes it to (0,2,1) and the later rule wins.
   Found by the theme preview: the sample page renders a primary button, which
   is exactly the case no existing page happened to exercise. */
.blk-button a.btn-primary { background: var(--tb-accent, #e8a33d); color: var(--accent-on, #fff); }
.blk-button a.btn-secondary { background: transparent; color: var(--ink); border: 1px solid var(--line); }

.blk-divider { border: 0; border-top: 1px solid var(--line); }
.blk-divider.size-s { margin: .8rem 0; }
.blk-divider.size-m { margin: 1.6rem 0; }
.blk-divider.size-l { margin: 2.6rem 0; }
.blk-space.size-s { height: .8rem; }
.blk-space.size-m { height: 1.8rem; }
.blk-space.size-l { height: 3rem; }

.blk-embed { margin: 1.2rem 0; }
.blk-embed iframe, .blk-embed video { max-width: 100%; border-radius: 8px; }

.blk-group > *:first-child { margin-top: 0; }
.blk-group > *:last-child { margin-bottom: 0; }

/* ---- Section presentation (the builder's layout tokens) ----
   Blocks::layoutClasses() emits this wrapper ONLY when a section departs from
   the default tokens, so a page authored before the builder still renders with
   no wrapper at all.

   Backgrounds are theme ROLES, never author-picked hex. surface/alt stay on
   the palette pair, which already clears AA in both themes; dark is one fixed
   pair; accent uses --accent-on, derived by App\Core\Color for exactly this.
   Link ink follows the band it sits on rather than the page behind it —
   --accent-ink is derived against the PAGE background and would be the wrong
   colour on an accent or dark band. */
.sec { --sec-scale: 1; }
.sec > *:first-child { margin-top: 0; }
.sec > *:last-child { margin-bottom: 0; }

.sec-space-s { padding-block: 1rem; }
.sec-space-m { padding-block: 2rem; }
.sec-space-l { padding-block: 3.2rem; }
.sec-space-xl { padding-block: 4.8rem; }

/* Widths relative to .page's column. left/transform rather than negative
   margins so nothing depends on the parent's padding.

   `wide` and `full` are measured from the VIEWPORT, not from --layout-max.
   They used to be --layout-max wide, which is the shell's BORDER box — the
   topbar, .page and the footer each put 1.2rem of padding inside it, so a wide
   section's content landed exactly one gutter outside the grid the brand, the
   h1 and the footer all align to. 19.2px is too small to read as a decision
   and too large to miss: it reads as the section overflowing to the left. A
   section can be aligned to that grid or wider than it, never both — so `wide`
   and `full` are now unmistakably wider, and `normal` is the aligned one. */
.sec-width-narrow { max-width: var(--reading-max); margin-inline: auto; }
.sec-width-wide,
.sec-width-full { position: relative; left: 50%; transform: translateX(-50%); }
.sec-width-wide { width: calc(100vw - var(--sbw, 0px) - 2.4rem); }
.sec-width-full { width: calc(100vw - var(--sbw, 0px)); }
/* A BAND is not the same thing as a bleed. Once a wide or full section has a
   background, the thing being widened is the colour, and the words inside it
   still have to line up with the words above it — so a band keeps the page's
   own column and only its background reaches out. With no background the
   section IS the content — a photo, a grid, a gallery — and nothing is capped.
   Applied to `wide` as well as `full`, or a full band would end up with a
   NARROWER column than a wide one. `- 2.4rem` because --layout-max is the
   border box: this is the same column .page's own padding leaves. */
.sec-width-wide.sec-bg-surface,
.sec-width-wide.sec-bg-alt,
.sec-width-wide.sec-bg-dark,
.sec-width-wide.sec-bg-accent,
.sec-width-wide.sec-bg-secondary,
.sec-width-wide.sec-bg-custom,
.sec-width-full.sec-bg-surface,
.sec-width-full.sec-bg-alt,
.sec-width-full.sec-bg-dark,
.sec-width-full.sec-bg-accent,
.sec-width-full.sec-bg-secondary,
.sec-width-full.sec-bg-custom { padding-inline: 1.2rem; }
.sec-width-wide.sec-bg-surface > *,
.sec-width-wide.sec-bg-alt > *,
.sec-width-wide.sec-bg-dark > *,
.sec-width-wide.sec-bg-accent > *,
.sec-width-wide.sec-bg-secondary > *,
.sec-width-wide.sec-bg-custom > *,
.sec-width-full.sec-bg-surface > *,
.sec-width-full.sec-bg-alt > *,
.sec-width-full.sec-bg-dark > *,
.sec-width-full.sec-bg-accent > *,
.sec-width-full.sec-bg-secondary > *,
.sec-width-full.sec-bg-custom > * { max-width: calc(var(--layout-max) - 2.4rem); margin-inline: auto; }

.sec-size-s { --sec-scale: .82; }
.sec-size-l { --sec-scale: 1.35; }
.sec-size-xl { --sec-scale: 1.75; }
.sec h2.blk-heading { font-size: calc(1.4rem * var(--sec-scale)); }
.sec h3.blk-heading { font-size: calc(1.15rem * var(--sec-scale)); }
.sec h4.blk-heading { font-size: calc(1rem * var(--sec-scale)); }

.sec-align-left { text-align: left; }
.sec-align-center { text-align: center; }
.sec-align-center .blk-image img { margin-inline: auto; }

.sec-bg-surface,
.sec-bg-alt,
.sec-bg-dark,
.sec-bg-accent,
.sec-bg-secondary,
.sec-bg-custom { padding-inline: 1.2rem; border-radius: var(--radius, 8px); }
.sec-width-full.sec-bg-surface,
.sec-width-full.sec-bg-alt,
.sec-width-full.sec-bg-dark,
.sec-width-full.sec-bg-accent,
.sec-width-full.sec-bg-secondary,
.sec-width-full.sec-bg-custom { border-radius: 0; }
/* A band with a background but no space chosen would have its text touching
   the edge; give it the smallest step rather than nothing. */
.sec-bg-surface:not([class*="sec-space-"]),
.sec-bg-alt:not([class*="sec-space-"]),
.sec-bg-dark:not([class*="sec-space-"]),
.sec-bg-accent:not([class*="sec-space-"]),
.sec-bg-secondary:not([class*="sec-space-"]),
.sec-bg-custom:not([class*="sec-space-"]) { padding-block: 1.4rem; }

.sec-bg-surface { background: var(--surface); }
/* The subtle band tints with the brand kit's neutral when one is chosen and
   with the page's own ink otherwise — which is what it always did. */
.sec-bg-alt { background: color-mix(in srgb, var(--neutral, var(--ink)) 6%, var(--bg)); }
/* The dark band is the brand kit's dark (default: the pair this rule always
   hardcoded). Its ink is derived by App\Core\Theme, never assumed white. */
.sec-bg-dark { background: var(--dark-bg, #1f2430); color: var(--dark-fg, #f5f6f8); }
.sec-bg-dark .blk-heading { color: var(--dark-fg, #f5f6f8); }
.sec-bg-dark a { color: var(--sec-dark-ink, #e8a33d); }
.sec-bg-dark .blk-button a.btn-secondary { color: var(--dark-fg, #f5f6f8); border-color: color-mix(in srgb, currentColor 28%, transparent); }
/* The second brand colour, a fill with its derived ink. Links take the ink
   and an underline rather than an accent, because no accent is promised to
   read on an owner's second colour. */
.sec-bg-secondary { background: var(--brand-2, #2f6f8f); color: var(--brand-2-on, #ffffff); }
.sec-bg-secondary .blk-heading,
.sec-bg-secondary a { color: var(--brand-2-on, #ffffff); }
.sec-bg-secondary a:not([class*="btn-"]) { text-decoration: underline; }
.sec-bg-secondary .blk-button a.btn-secondary { color: var(--brand-2-on, #ffffff); border-color: currentColor; }
.sec-bg-accent { background: var(--tb-accent, #e8a33d); color: var(--accent-on, #14171f); }
.sec-bg-accent .blk-heading { color: var(--accent-on, #14171f); }
.sec-bg-accent a { color: var(--accent-on, #14171f); }
.sec-bg-accent .blk-button a.btn-primary { background: var(--accent-on, #14171f); color: var(--tb-accent, #e8a33d); }
.sec-bg-accent .blk-button a.btn-secondary { color: var(--accent-on, #14171f); border-color: currentColor; }

/* Full-screen sections: at least as tall as the window (minus the topbar the
   visitor still sees), content centred in it. svh, not vh, so a phone's
   collapsing address bar cannot push the bottom of a hero off the screen. */
.sec-height-screen {
  min-height: calc(100svh - var(--tb-total, var(--tb-h, 56px)));
  display: flex; flex-direction: column; justify-content: center;
}
/* A rule inside a full-screen, centred section is an accent under the
   headline, not a page divider: short and centred. Scoped to the new height
   token so no section written before it changes. */
.sec-height-screen.sec-align-center .blk-divider { width: min(5rem, 40%); margin-inline: auto; }
/* On a coloured band or over an image the page's own --line is drawn for a
   different surface (near-invisible on dark, white on an accent). The rule
   takes the band's ink instead, softened. */
.sec:is(.sec-bg-dark, .sec-bg-accent, .sec-bg-secondary, .sec-bg-custom, .sec-overlay-dark, .sec-overlay-light) .blk-divider {
  border-top-color: currentColor; opacity: .45;
}
/* Overlays sit between a background image and the words. Each FIXES its own
   ink, because a photo has no single colour to derive one from; the alphas are
   chosen so that ink clears 4.5:1 over a pure white or pure black image (dark:
   #f5f6f8 on at worst rgb(110,111,114), 5.1:1; light: #14171f on at worst
   rgb(188,188,190), 9.5:1). `.sec.` doubles the specificity on purpose: it has
   to beat the scoped `.sec-b_…` rule that carries a custom colour's derived
   ink, which is printed after this file. Links take the band's ink and an
   underline rather than an accent, for the same reason. */
.sec.sec-overlay-dark,
.sec.sec-overlay-light { position: relative; isolation: isolate; }
.sec.sec-overlay-dark::before,
.sec.sec-overlay-light::before {
  content: ""; position: absolute; inset: 0; z-index: -1; border-radius: inherit;
}
.sec.sec-overlay-dark { color: #f5f6f8; }
.sec.sec-overlay-dark::before { background: rgba(14, 16, 22, .6); }
.sec.sec-overlay-light { color: #14171f; }
.sec.sec-overlay-light::before { background: rgba(251, 251, 253, .75); }
.sec.sec-overlay-dark .blk-heading,
.sec.sec-overlay-light .blk-heading { color: inherit; }
.page .body .sec.sec-overlay-dark a:not([class*="btn-"]),
.page .body .sec.sec-overlay-light a:not([class*="btn-"]) { color: inherit; text-decoration: underline; }
.sec.sec-overlay-dark .blk-button a.btn-secondary { color: #f5f6f8; border-color: rgba(255, 255, 255, .5); }
.sec.sec-overlay-light .blk-button a.btn-secondary { color: #14171f; border-color: rgba(20, 23, 31, .4); }

/* .page .body a wins on specificity over the plain `a` rules above — and
   would win over the BUTTON rules too, which is why buttons are excluded the
   way the overlay rule above excludes them. Without it a primary button on a
   dark band printed its label in the band's link ink on an accent fill, and
   on an accent band in --accent-on on a --accent-on fill: invisible. */
.page .body .sec-bg-dark a:not([class*="btn-"]) { color: var(--sec-dark-ink, #e8a33d); }
.page .body .sec-bg-accent a:not([class*="btn-"]) { color: var(--accent-on, #14171f); }
.page .body .sec-bg-secondary a:not([class*="btn-"]) { color: var(--brand-2-on, #ffffff); }

/* ---- Button and card styles (v0.17.0) ----
   Theme tokens every section inherits, as a class on <body> that is absent
   for the default — so `filled` and `bordered` are simply the rules above.

   Buttons change only on the page's own surfaces. A coloured band, an image
   overlay or a custom background has already fixed its own button colours
   against ITS surface, and an outline drawn in page ink on an accent band is
   exactly the unreadable button the accent doctrine exists to prevent.
   - outline: no fill, the accent as link INK (derived against the page, so it
     clears AA on it) for both the label and the border.
   - soft: a light wash of the accent behind body ink. The ink is the page's
     own, and a 20% wash cannot pull it under AA on either theme. */
body.btn-style-outline .blk-button a.btn-primary:not(:is(.sec-bg-dark, .sec-bg-accent, .sec-bg-secondary, .sec-bg-custom, .sec-overlay-dark, .sec-overlay-light) *),
body.btn-style-outline .form-btn:not(:is(.sec-bg-dark, .sec-bg-accent, .sec-bg-secondary, .sec-bg-custom, .sec-overlay-dark, .sec-overlay-light) *) {
  background: transparent; color: var(--accent-ink); border: 2px solid currentColor;
}
body.btn-style-soft .blk-button a.btn-primary:not(:is(.sec-bg-dark, .sec-bg-accent, .sec-bg-secondary, .sec-bg-custom, .sec-overlay-dark, .sec-overlay-light) *),
body.btn-style-soft .form-btn:not(:is(.sec-bg-dark, .sec-bg-accent, .sec-bg-secondary, .sec-bg-custom, .sec-overlay-dark, .sec-overlay-light) *) {
  background: color-mix(in srgb, var(--tb-accent, #e8a33d) 20%, var(--bg)); color: var(--ink); border-color: transparent;
}
body.btn-style-soft .blk-button a.btn-secondary:not(:is(.sec-bg-dark, .sec-bg-accent, .sec-bg-secondary, .sec-bg-custom, .sec-overlay-dark, .sec-overlay-light) *) {
  border-color: transparent; background: color-mix(in srgb, var(--neutral, var(--ink)) 7%, var(--bg));
}

/* Cards: the boxed things the site draws — countdown units and the theme
   preview's cards here; a module's own cards (listings, a post's contents,
   an answer) carry the same two rules in that module's stylesheet.
   - flat: no border, a faint tint so the card still reads as a card on a page
     the same colour as its surface.
   - raised: no border, a soft shadow instead. */
body.card-style-flat :is(.theme-card, .countdown-unit) {
  border-color: transparent; background: color-mix(in srgb, var(--neutral, var(--ink)) 5%, var(--bg));
}
body.card-style-raised :is(.theme-card, .countdown-unit) {
  border-color: transparent; box-shadow: 0 6px 22px var(--shadow);
}

@media (max-width: 640px) {
  .blk-columns.cols-2, .blk-columns.cols-3 { grid-template-columns: 1fr; }
  .blk-gallery.cols-3, .blk-gallery.cols-4 { grid-template-columns: repeat(2, 1fr); }
  .sec-space-l { padding-block: 2.2rem; }
  .sec-space-xl { padding-block: 2.8rem; }
  .sec-size-xl { --sec-scale: 1.35; }
}

/* ---- Theme preview (admin only) ----
   Reached only through /admin/settings/theme/preview, which the router gates
   on settings.manage. It lives in the public stylesheet on purpose: the point
   of the preview is that it is styled by exactly what visitors get. */
.theme-card {
  border: 1px solid var(--line); border-radius: var(--radius, 8px);
  padding: .8rem .9rem; background: var(--surface);
}
.theme-sample .blk-button a + a { margin-left: .5rem; }

/* ---- Member accounts (v0.12.0) ----
   Only rendered while member accounts are switched on. Fills use the accent
   doctrine: --tb-accent behind, --accent-on on top, --accent-ink for links. */
a.tb-btn { text-decoration: none; }
.tb-account.is-signed-in { background: var(--tb-accent, #e8a33d); border-color: transparent; color: var(--accent-on, #14171f); }
.tb-account.is-signed-in:hover { background: var(--tb-accent, #e8a33d); filter: brightness(1.08); }
.tb-initial { font-size: .85rem; font-weight: 700; }

.acct { display: flex; justify-content: center; padding: 2.5rem 0 3.5rem; }
.acct-card {
  width: 100%; max-width: 30rem; padding: 2rem 2rem 1.6rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius, 8px);
  box-shadow: 0 10px 30px var(--shadow);
}
.acct-card h1 { margin: 0 0 1rem; font-size: 1.6rem; }
.acct-card h2 { margin: 1.8rem 0 .4rem; font-size: 1.1rem; }
.acct-intro { color: var(--muted); margin: 0 0 1.2rem; }
.acct-role {
  display: inline-block; margin-left: .35rem; padding: .05rem .5rem; border-radius: 999px;
  border: 1px solid var(--line); color: var(--ink); font-size: .8rem;
}
.acct-form { display: flex; flex-direction: column; gap: .35rem; }
.acct-form label { font-weight: 600; font-size: .9rem; margin-top: .6rem; }
.acct-form input[type="text"], .acct-form input[type="email"], .acct-form input[type="password"] {
  font: inherit; color: var(--ink); background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--radius, 8px); padding: .6rem .7rem;
}
.acct-form input:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 1px; }
.acct-hint { color: var(--muted); font-size: .82rem; }
.acct-error { color: #d93a3a; font-size: .85rem; }
[data-theme="dark"] .acct-error { color: #ff8a8a; }
.acct-btn {
  margin-top: 1rem; align-self: flex-start; cursor: pointer;
  font: inherit; font-weight: 600; padding: .6rem 1.3rem; border-radius: var(--radius, 8px);
  background: var(--tb-accent, #e8a33d); color: var(--accent-on, #14171f); border: 1px solid transparent;
}
.acct-btn.secondary { background: transparent; color: var(--ink); border-color: var(--line); }
.acct-btn.danger { background: transparent; color: var(--ink); border-color: #d93a3a; }
.acct-btn:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
.acct-links { display: flex; flex-wrap: wrap; gap: .5rem 1.4rem; margin: 1.3rem 0 0; font-size: .92rem; }
.acct-links a, .acct-card p a { color: var(--accent-ink); }
.acct-flash { margin: 0 0 1rem; padding: .6rem .8rem; border-radius: var(--radius, 8px); border: 1px solid var(--line); }
.acct-flash-success { border-color: #2f9e5b; }
.acct-flash-error { border-color: #d93a3a; }
.acct-signout { margin-top: 1.6rem; padding-top: 1.2rem; border-top: 1px solid var(--line); }
.acct-signout .acct-btn { margin-top: 0; }
.acct-danger { margin-top: 1.4rem; }
.acct-danger summary { cursor: pointer; color: var(--muted); }
/* The spam trap: off-screen rather than display:none, which some bots skip. */
.acct-trap { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }
@media (max-width: 520px) {
  .acct { padding: 1.2rem 0 2rem; }
  .acct-card { padding: 1.4rem 1.1rem; }
}

/* ---- Forms and search (v0.13.0) ----
   Same field shape as the account forms, and the same accent doctrine:
   --tb-accent behind the button, --accent-on on it, --accent-ink for text. */
.blk-form { margin: 1.4rem 0; max-width: 40rem; }
.sec-align-center .blk-form { margin-inline: auto; }
.form-body { display: flex; flex-direction: column; gap: .9rem; }
.form-field { display: flex; flex-direction: column; gap: .3rem; }
.form-field > label { font-weight: 600; font-size: .92rem; }
.form-field input[type="text"], .form-field input[type="email"], .form-field input[type="tel"],
.form-field textarea, .form-field select {
  font: inherit; color: var(--ink); background: var(--bg); width: 100%;
  border: 1px solid var(--line); border-radius: var(--radius, 8px); padding: .6rem .7rem;
}
.form-field textarea { resize: vertical; min-height: 8rem; }
.form-field :focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 1px; }
.form-field [aria-invalid="true"] { border-color: #d93a3a; }
.form-check { display: flex; gap: .55rem; align-items: flex-start; font-size: .95rem; }
.form-check input { margin-top: .25rem; width: 1.05rem; height: 1.05rem; accent-color: var(--tb-accent, #e8a33d); }
.form-req { color: var(--muted); }
.form-error { color: #d93a3a; font-size: .85rem; margin: 0; }
[data-theme="dark"] .form-error { color: #ff8a8a; }
.form-sent { padding: 1rem 1.1rem; border: 1px solid #2f9e5b; border-radius: var(--radius, 8px); margin: 0; white-space: pre-line; }
.form-trap { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }
.form-btn {
  align-self: flex-start; cursor: pointer; font: inherit; font-weight: 600;
  padding: .65rem 1.4rem; border-radius: var(--radius, 8px); border: 1px solid transparent;
  background: var(--tb-accent, #e8a33d); color: var(--accent-on, #14171f);
}
.form-btn:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }

.search-page { max-width: 46rem; margin: 0 auto; padding: 1rem 0 3rem; }
.search-form { display: flex; gap: .6rem; margin: 1rem 0 1.4rem; }
.search-form input {
  flex: 1; min-width: 0; font: inherit; color: var(--ink); background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--radius, 8px); padding: .6rem .8rem;
}
.search-form input:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 1px; }
.search-form .button {
  cursor: pointer; font: inherit; font-weight: 600; padding: .6rem 1.2rem; border-radius: var(--radius, 8px);
  background: var(--tb-accent, #e8a33d); color: var(--accent-on, #14171f); border: 1px solid transparent;
}
.search-note { color: var(--muted); }
.search-results { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 1.3rem; }
.search-kind { display: block; font-size: .75rem; letter-spacing: .05em; text-transform: uppercase; color: var(--muted); }
.search-title { font-size: 1.15rem; font-weight: 600; color: var(--accent-ink); }
.search-snippet { margin: .25rem 0 0; color: var(--ink); line-height: 1.55; }
/* The browser's yellow is unreadable on the dark theme; a tint of the accent
   behind the page's own ink keeps the word at body contrast in both. */
.search-snippet mark { background: color-mix(in srgb, var(--tb-accent, #e8a33d) 30%, transparent); color: inherit; border-radius: 3px; padding: 0 .1em; }
.pagination { display: flex; gap: .35rem; margin-top: 1.6rem; flex-wrap: wrap; }
.pagination a, .pagination .current {
  padding: .3rem .7rem; border: 1px solid var(--line); border-radius: var(--radius, 8px); color: var(--ink); text-decoration: none;
}
.pagination .current { background: var(--tb-accent, #e8a33d); color: var(--accent-on, #14171f); border-color: transparent; }
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* ---- Holding pages (app/Views/public/holding.php) ----
   Maintenance and coming soon: one screen, no topbar or footer. The surface
   is a theme role, a colour (its ink printed beside it by the view), or a
   photo under a fixed scrim; every ink is derived or fixed for 4.5:1, never
   picked. Links take the surface's own ink with an underline, because no
   accent is promised to read on an owner's colour. */
body.holding {
  --hold-bg: var(--bg); --hold-fg: var(--ink); --hold-mark: var(--tb-accent, #e8a33d);
  background: var(--hold-bg); color: var(--hold-fg);
  min-height: 100svh; isolation: isolate;
}
.holding.hold-on-dark { --hold-bg: var(--dark-bg, #1f2430); --hold-fg: var(--dark-fg, #f5f6f8); --hold-mark: var(--sec-dark-ink, #e8a33d); }
.holding.hold-on-accent { --hold-bg: var(--tb-accent, #e8a33d); --hold-fg: var(--accent-on, #14171f); --hold-mark: currentColor; }
.holding.hold-on-color { --hold-mark: currentColor; }
/* Holding::PHOTO_SCRIM and Holding::PHOTO_INK: black at .6 over a pure white
   photo is rgb(102,102,102), on which #f5f6f8 holds 5.4:1. */
.holding.hold-on-photo { --hold-bg: #111318; --hold-fg: #f5f6f8; --hold-mark: currentColor; }
.hold-backdrop { position: fixed; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: -2; }
.holding.hold-on-photo::before { content: ""; position: fixed; inset: 0; background: rgba(0, 0, 0, .6); z-index: -1; }

.hold { flex: 1; width: 100%; min-height: 100svh; display: grid; }
.hold-body {
  display: flex; flex-direction: column; gap: calc(1rem * var(--rhythm, 1));
  padding: clamp(2rem, 7vw, 5rem) clamp(1.2rem, 6vw, 5rem);
}
.hold a { color: var(--hold-fg); text-decoration: underline; text-underline-offset: .18em; }
.hold-brand { margin-bottom: calc(1rem * var(--rhythm, 1)); }
.hold-logo { display: block; max-width: min(220px, 60vw); max-height: 96px; width: auto; height: auto; }
.hold-name { font-family: var(--font-heading, inherit); font-weight: 700; font-size: 1.15rem; letter-spacing: .02em; }
.hold-title {
  font-family: var(--font-heading, inherit); font-weight: 700; line-height: 1.12;
  font-size: clamp(2rem, 5.5vw, 3.6rem); color: inherit; overflow-wrap: anywhere;
}
.hold-rule { width: min(5rem, 40%); border-top: 3px solid var(--hold-mark); }
.hold-message { font-size: 1.12rem; max-width: 38rem; }
.hold-message p + p { margin-top: .8rem; }
.hold-contact { display: flex; flex-wrap: wrap; gap: .8rem 1.2rem; align-items: center; margin-top: .4rem; }
.hold-social { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: .5rem; }
.hold-social a {
  display: inline-grid; place-items: center; width: 2.6rem; height: 2.6rem;
  border: 1px solid color-mix(in srgb, currentColor 45%, transparent); border-radius: var(--radius, 8px);
}
.hold-social svg { width: 1.2rem; height: 1.2rem; fill: currentColor; }
.hold-langs { display: flex; flex-wrap: wrap; gap: .4rem 1rem; margin-top: auto; padding-top: 1.6rem; font-size: .92rem; }
.hold-langs a[aria-current] { font-weight: 700; text-decoration: none; }
/* The countdown keeps its block markup; on a holding surface its boxes are
   outlines in the surface's ink rather than the page's cards. */
.holding .blk-countdown { margin: .4rem 0; }
.holding .countdown-unit { background: transparent; border-color: color-mix(in srgb, currentColor 40%, transparent); }
.holding .countdown-label,
.holding .countdown-date,
.holding .countdown-done { color: inherit; }
@media (max-width: 480px) {
  .holding .countdown-units { gap: .4rem; }
  .holding .countdown-unit { min-width: 0; flex: 1 1 0; }
  .holding .countdown-value { font-size: 1.4rem; }
}

/* Centred: the stack in the middle of the screen. */
.hold-layout-centered .hold-body { align-items: center; justify-content: center; text-align: center; max-width: 46rem; margin: 0 auto; width: 100%; }
.hold-layout-centered .hold-langs { justify-content: center; margin-top: 1.2rem; }
.hold-layout-centered .hold-contact { justify-content: center; }
/* Full photo: the words sit low and to the left, over the picture. */
.hold-layout-photo .hold-body { justify-content: flex-end; max-width: 54rem; }
.hold-layout-photo .hold-title { font-size: clamp(2.4rem, 7vw, 4.6rem); }
.hold-layout-photo .hold-langs { margin-top: 1.2rem; }
/* Minimal: small and quiet, set to the left. */
.hold-layout-minimal .hold-body { justify-content: center; max-width: 36rem; }
.hold-layout-minimal .hold-title { font-size: clamp(1.6rem, 3.6vw, 2.3rem); }
.hold-layout-minimal .hold-rule { display: none; }
.hold-layout-minimal .hold-message { font-size: 1rem; }
.hold-layout-minimal .hold-langs { margin-top: 1.2rem; }
.hold-layout-photo .hold-rule { display: none; }
/* Split: the words on the surface, the photo beside them. */
.hold-layout-split .hold { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.hold-layout-split .hold-body { justify-content: center; }
.hold-side { position: relative; min-height: 100%; }
.hold-side img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
@media (max-width: 760px) {
  .hold-layout-split .hold { grid-template-columns: 1fr; grid-template-rows: 38svh auto; }
  .hold-layout-split .hold-side { order: -1; }
}
