/* ═══════════════════════════════════════════════════════════════
   Shared CMS site stylesheet
   ═══════════════════════════════════════════════════════════════ */

/* Skin (colour + type tokens) lives in the shared theme.css — edit that
   one file to reskin the site AND the CMS admin at once. Must be the first
   statement so its :root tokens land before any rule below uses them. */
@import url('theme.css');

/* Structural presentation that's allowed to diverge per site (currently
   just nav layout — see theme-layout.css's own header for the full
   reasoning) lives in theme-layout.css, added 4 Aug 2026. Site.css itself
   must stay byte-identical across every site — this is the deliberate
   escape hatch for the one thing that's proven not to be. */
@import url('theme-layout.css');

/* ── STRUCTURAL TOKENS ──────────────────────────────────────── */
/* Layout-only tokens stay here; everything visual (colours, fonts,
   the --blue-1..4 ramp) now comes from theme.css. */
:root {
  --radius:    10px;
  --max-w:     860px;
  --nav-h:     56px;
}

/* ── RESET ──────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
/* text-size-adjust added 3 Aug 2026 as part of the w3w icon oversized-on-
   iPhone fix — see icons/w3w.svg and .article-body .w3w-icon comment below
   for the full root-cause writeup. Stops mobile Safari's automatic font-
   boosting from inflating any em-sized element (not just body text) on
   narrow columns; harmless everywhere else since it only disables an
   auto-adjustment, not actual sizing. */

body {
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--text);
  line-height: 1.6;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

img { max-width: 100%; display: block; }
a { color: var(--signal); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ── FOOTER ─────────────────────────────────────────────────── */
#site-footer {
  margin-top: auto;
  border-top: 1px solid var(--border);
  padding: 2rem 1.5rem;
  text-align: center;
  font-size: 0.75rem;
  color: var(--muted);
  font-family: var(--font-mono);
}
#site-footer a { color: var(--muted); }
#site-footer a:hover { color: var(--signal); }
.footer-sites { margin-top: 0.5rem; font-size: 0.68rem; opacity: 0.8; }
.footer-sites a { color: var(--muted); }
.footer-sites a:hover { color: var(--signal); }

/* ── PAGE WRAPPER ───────────────────────────────────────────── */
.page-wrap {
  max-width: var(--max-w);
  margin: 0 auto;
  padding: 3rem 1.5rem;
  flex: 1;
}

/* ── PAGE HEADER (hero area for inner pages) ────────────────── */
.page-header {
  margin-bottom: 2.5rem;
  padding-bottom: 1.5rem;
  border-bottom: 1px solid var(--border);
}
.page-header .eyebrow {
  font-family: var(--font-mono);
  font-size: 0.65rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.6rem;
}
.page-header h1 {
  font-family: var(--font-display);
  font-size: clamp(1.7rem, 4vw, 2.4rem);
  font-weight: 600;
  line-height: 1.12;
  letter-spacing: -0.02em;
  color: var(--text);
  margin-bottom: 0.5rem;
}
.page-header .standfirst {
  font-size: 1.05rem;
  color: var(--muted);
  font-weight: 300;
  max-width: 600px;
}

/* ── POST CARDS (index listing) ─────────────────────────────── */
.post-grid {
  display: grid;
  gap: 1.25rem;
}

.post-card {
  background: var(--card);
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  padding: 1.5rem;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.5rem 1rem;
  align-items: start;
  transition: border-color 0.2s, transform 0.2s;
  text-decoration: none;
  color: inherit;
}
.post-card:hover {
  border-color: var(--signal);
  transform: translateY(-2px);
  text-decoration: none;
}
/* Title turns blue when hovering anywhere on the card (matches the hero). */
.post-card:hover h2 { color: var(--signal); }

.post-card h2 {
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: -0.015em;
  color: var(--text);
  /* Match the photo card: 2-line title cap for equal heights. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
.post-card p {
  font-size: 0.85rem;
  color: var(--muted);
  grid-column: 1 / -1;
  margin-top: 0.25rem;
  /* Match the photo card: 3-line meta cap, ellipsis is the "more" cue. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
}
/* Read-more label (2 Aug 2026) — replaces the old arrow-only cue (→), which
   Richard found floated below the excerpt with a visible gap rather than
   reading as a continuation of the card's text; also now built for the
   photo card variant, which never had one. One shared rule so both stay in
   sync (same reasoning as .product-card/.download-card sharing rules).
   .post-card (the no-photo grid card) needs an explicit grid-column, since
   it isn't otherwise part of that grid's row flow — without it the label
   can land in a stray column next to the title instead of its own row
   directly under the excerpt. .post-card--photo doesn't need the same
   fix: .post-card__body is already a flex column with one shared gap
   (see below), so a new child there sits tight against the excerpt with
   no extra rule required. */
.post-card-more {
  color: var(--signal);
  font-size: 0.8rem;
  font-weight: 600;
}
.post-card .post-card-more {
  grid-column: 1 / -1;
}

/* Category and date both removed outright from every card, 2 Aug 2026 —
   neither shows on a card at all any more (category was a bordered badge,
   then plain text, then nothing; date briefly had a per-block toggle,
   then was dropped everywhere, blog.html included). Full history of the
   back-and-forth is in PROJECT-NOTES.md rather than repeated here, since
   there's no live rule left to hang the comment on — .post-category,
   .post-card__category, .post-card-meta, .post-card__meta, .post-card-date
   and .hp-hero-meta are all gone. The category value itself still lives in
   data-category on the card element for the blog.html filter (js/site.js);
   both category and date still show on the article page's own header
   (buildPostHtml()'s .eyebrow line, untouched by any of this). */

/* ── ARTICLE BODY ───────────────────────────────────────────── */
.article-body {
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.75;
  color: var(--text);
}
/* Found 25 July 2026 via the test post: on fixed-left/fixed-right posts,
   the text column and photo column both start at the same grid-row top,
   but h2/h3's own top margin (2.5rem/2rem) pushed the *visible* heading
   text down below that line while the photo (no margin) sat flush —
   e.g. a post opening with an H2 misaligned by 40px against the photo.
   Zeroing whichever element actually leads the body fixes it for any
   post regardless of what that first element is (heading, paragraph,
   Amazon card, etc), without changing spacing for the same element type
   when it isn't first. */
.article-body > *:first-child { margin-top: 0; }
.article-body h2 {
  font-family: var(--font-display);
  font-size: 1.2rem;
  font-weight: 500;
  letter-spacing: -0.01em;
  margin: 2.5rem 0 0.75rem;
  color: var(--text);
}
.article-body h3 {
  font-family: var(--font-display);
  font-size: 0.98rem;
  font-weight: 500;
  margin: 2rem 0 0.5rem;
  color: var(--text);
}
.article-body p { margin-bottom: 1.25rem; }
.article-body ul,
.article-body ol {
  margin: 0 0 1.25rem 1.5rem;
}
.article-body li { margin-bottom: 0.4rem; }
.article-body blockquote {
  border-left: 3px solid var(--signal);
  margin: 1.5rem 0;
  padding: 0.5rem 0 0.5rem 1.25rem;
  color: var(--muted);
  font-style: italic;
}
.article-body img {
  border-radius: var(--radius);
  margin: 1.5rem 0;
  border: 1px solid var(--border);
}
.article-body a { color: var(--signal); }
/* Pre-existing bug, surfaced 25 July 2026 by the Amazon card's button:
   `.article-body a` (class+element) beats plain `.btn` (one class) on
   specificity, so any <a class="btn"> inside a post body silently lost
   its white text to this rule — invisible white-on-blue became
   blue-on-blue. Affects the existing 🔗 Button shortcut too, not just
   the new Amazon one. Fixed by out-specifying it here. */
.article-body a.btn { color: #fff; }
/* Found 28 July 2026: a standalone CTA button (the 🔗 Button toolbar
   shortcut, its own line) lands in a plain <p> via autoParagraphs(), and
   .article-body p only carries margin-bottom — no margin-top anywhere —
   so it sat exactly as close to the text above it as any two ordinary
   paragraphs, with none of the "this is its own callout" breathing room
   the Amazon/Download/Map boxes get (all margin: 2rem 0). Richard was
   adding blank spacer paragraphs by hand to work around it. Scoped to a
   button on its own line only, via :only-child — an inline link
   button/CTA sitting mid-sentence (case 'link' in format-toolbar.js) is
   untouched, since it's still meant to sit inside running text. */
.article-body p:has(> a.btn:only-child) { margin: 2rem 0; }

/* ── ALIGNMENT (28 July 2026, toolbar Left/Center/Right/Justify) ─────
   admin/format-toolbar.js's applyAlign() adds align-center/align-right/
   align-justify directly onto whichever tag it decides is the block being
   aligned — a heading, blockquote or paragraph gets the class straight on
   itself (plain text-align); a standalone Button gets it on the <a> even
   though visually it's the <p> autoParagraphs() wraps it in that needs
   centering, so that's targeted via :has() below instead; Amazon/Download
   cards get it on their own outer <div> but need margin, not text-align,
   since they're a fixed-max-width box sitting in a full-width column, not
   full-width text. Left is the default and never gets a class — see
   withAlignClass() in format-toolbar.js. Map embeds, lists, dividers and
   inline photos are deliberately not part of this system (see the comment
   above applyAlign() for why); Justify is restricted client-side to plain
   paragraphs only. */
.article-body h2.align-center,
.article-body h3.align-center,
.article-body blockquote.align-center,
.article-body p.align-center { text-align: center; }
.article-body h2.align-right,
.article-body h3.align-right,
.article-body blockquote.align-right,
.article-body p.align-right { text-align: right; }
.article-body p.align-justify { text-align: justify; }

.article-body .product-card.align-center,
.article-body .download-card.align-center { margin-left: auto; margin-right: auto; }
.article-body .product-card.align-right,
.article-body .download-card.align-right { margin-left: auto; margin-right: 0; }

.article-body p:has(> a.btn.align-center) { text-align: center; }
.article-body p:has(> a.btn.align-right) { text-align: right; }

/* ── AMAZON PRODUCT CARD / DOWNLOAD CARD ───────────────────────
   Amazon card live since 25 July 2026 (🛒 Amazon toolbar button,
   admin/format-toolbar.js). No image (dropped same day — the post's own
   photos already cover that). Background matched to the page (var(--bg),
   not var(--surface)) and width capped so the card reads as a compact,
   deliberate callout rather than a stretched, half-empty box — agreed with
   Richard via a few rounds of visual mockups (see PROJECT-NOTES.md).

   Download card added 26 July 2026 (📥 Download toolbar button) — a PDF
   instruction manual or GPX route file, same title/button/description
   shape as the Amazon card, so it shares every rule below via a combined
   selector rather than a second, easily-drifting copy of the same CSS. */
.product-card,
.download-card {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.1rem 1.3rem;
  margin: 2rem 0;
  max-width: 420px;
  display: flex;
  gap: 1.25rem;
  align-items: center;
}
/* Even spacing between title/blurb/button/disclosure (26 July 2026) — the
   old approach used margin-bottom on h4/p, which only adds space AFTER an
   element, not before. That left the gap above the button (h4's
   margin-bottom, or the blurb p's) looking normal, but nothing added any
   space BEFORE the affiliate-note paragraph that follows the button, so it
   sat squashed underneath it — reported by Richard as uneven/unbalanced.
   Switched to a flex column with one shared `gap`, so every child (title,
   blurb if present, button, disclosure) sits evenly spaced regardless of
   which ones exist, instead of each element carrying its own one-sided
   margin. `align-items: flex-start` keeps the button hugging its own text
   width rather than stretching to the card's full width, which is what a
   flex column would otherwise do to a block-level child by default. */
.product-card-info,
.download-card-info {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.75rem;
}
.product-card-info h4,
.download-card-info h4 {
  font-family: var(--font-body);
  font-size: 0.9rem;
  font-weight: 600;
}
.product-card-info p,
.download-card-info p {
  font-family: var(--font-body);
  font-size: 0.78rem;
  color: var(--muted);
}
.product-card-info .affiliate-note {
  font-size: 0.65rem;
  color: var(--muted);
  font-family: var(--font-mono);
}

/* Map embed (26 July 2026, 🗺️ Map toolbar button) — promoted here from
   journey-to-looe.html's own one-off <style> tag, since the Dynamic Page
   Builder needed the exact same box for its own map pages and repeating it
   as per-page Custom CSS every time wasn't worth it. Responsive 16:9 box
   (3:4 on narrow screens) so a Google My Maps iframe never overflows or
   looks tiny — the iframe itself just fills whatever the container gives
   it. See PROJECT-NOTES.md, "Map embed toolbar button".
   Widened 28 July 2026: Google My Maps reserves its own space for a
   legend/layers panel inside the iframe, so the visible map was cramped
   even at "full width" — pulled out flush to .page-wrap's own edge
   (cancels its 1.5rem side padding, never wider than the page itself, so
   this can't cause a horizontal scrollbar) and given more height (4:3,
   1:1 on mobile, instead of 16:9/3:4) so more of the real map shows
   without needing the embed's own "expand" control. A true edge-to-edge
   viewport bleed was considered and dropped — the standard 100vw trick
   can sit a few px past the true viewport edge on some browsers, not
   worth risking on this design. Toolbar map template also gained an
   explicit "Open in Google Maps" link below the box, see
   admin/format-toolbar.js and PROJECT-NOTES.md. */
.map-container {
  width: calc(100% + 3rem);
  margin: 2rem -1.5rem;
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--border);
  aspect-ratio: 4 / 3;
}
.map-container iframe {
  width: 100%;
  height: 100%;
  border: none;
  display: block;
}
/* When the toolbar's "Open in Google Maps" link follows directly (wrapped
   in its own <p> by autoParagraphs()), .article-body p:has(> a.btn:only-child)
   already gives it margin: 2rem 0 — that collapses with this box's own
   2rem bottom margin rather than stacking, so the gap stays a clean 2rem
   either way, button present or not. No extra rule needed here. */
@media (max-width: 600px) {
  .map-container { width: 100%; margin-left: 0; margin-right: 0; aspect-ratio: 1 / 1; }
}

/* what3words inline reference (3 Aug 2026) — a plain inline link (built by
   the toolbar's new 📍 w3w button, same placeholder-and-hand-edit pattern
   as 🔗 Link/🛒 Amazon/📥 Download/🗺️ Map) carrying the official black w3w
   square icon (icons/w3w.svg, cropped from their brand-kit logo file — see
   PROJECT-NOTES.md) immediately before the ///word.word.word text. Sized
   and nudged to sit on the text baseline rather than floating above it;
   .article-body a already gives the link itself the right colour.
   Two real bugs found live on TC, 3 Aug 2026, both from generic <img>
   rules elsewhere in this file leaking onto this icon:
   1) the global `img { display: block; }` reset (top of this file) forced
      the icon onto its own line, pushing the ///text below it — a plain
      `.w3w-icon { display: inline-block }` alone was tried first and
      wasn't enough to fully fix it, because of bug 2:
   2) `.article-body img` (class+element, higher specificity than a bare
      `.w3w-icon` class) was still winning on every property IT sets —
      `margin: 1.5rem 0` (large vertical gaps, explaining the leftover
      stacked look) plus a border and border-radius meant for real inline
      photos (explaining why the icon itself looked visually "wrong" —
      corners rounded, a stray border around a 1em icon).
   Fixed by raising this rule's own specificity to `.article-body .w3w-icon`
   (two classes beats one class + one element, regardless of file order)
   and explicitly overriding every property `.article-body img` sets, not
   just the ones this icon actually wants changed — so nothing generic can
   leak through again even if that rule changes later. */
.article-body .w3w-icon {
  display: inline-block;
  height: 1em;
  width: 1em;
  margin: 0 0.25em 0 0;
  border: none;
  border-radius: 0;
  vertical-align: -0.15em;
}

.btn {
  display: inline-block;
  background: var(--signal);
  color: #fff;
  font-family: var(--font-body);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  padding: 0.55rem 1.2rem;
  border-radius: 8px;
  text-decoration: none;
  transition: opacity 0.15s;
}
.btn:hover { opacity: 0.85; text-decoration: none; }

/* ── SECTION DIVIDER ────────────────────────────────────────── */
.section-label {
  font-family: var(--font-mono);
  font-size: 0.65rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 1.25rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--border);
}

/* ── PHOTO POST CARD (blog index & homepage) ────────────────── */
.post-card--photo {
  background: var(--card);
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  padding: 1.25rem;
  display: flex;
  gap: 1.25rem;
  align-items: flex-start;
  transition: border-color 0.2s, transform 0.2s;
  text-decoration: none;
  color: inherit;
  position: relative;
}
.post-card--photo:hover {
  border-color: var(--signal);
  transform: translateY(-2px);
  text-decoration: none;
}

.post-card__thumb-link { flex-shrink: 0; text-decoration: none; }

.post-card__thumb {
  width: 160px;
  height: 120px;
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid var(--border);
}

.post-card__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.25s ease;
}

.post-card--photo:hover .post-card__thumb img {
  transform: scale(1.04);
}

.post-card__body {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  min-width: 0;
}

.post-card__title {
  font-family: var(--font-display);
  font-size: 0.98rem;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: -0.015em;
  margin: 0;
  /* Cap to 2 lines so every card is the same height (see .post-card__excerpt). */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

.post-card__title a { color: var(--text); text-decoration: none; }
.post-card__title a:hover { color: var(--signal); }
/* Title turns blue when hovering anywhere on the photo card (matches the hero). */
.post-card--photo:hover .post-card__title a { color: var(--signal); }
/* Stretched link: the title link's overlay covers the whole card, so clicking
   (or hovering) anywhere in the box follows it — the photo card is an <article>
   of separate links, so without this the padding/excerpt/blank areas were dead.
   Matches the hero, which is a single <a>. */
.post-card__title a::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}

.post-card__excerpt {
  font-size: 0.85rem;
  color: var(--muted);
  line-height: 1.5;
  margin: 0;
  /* Fixed card size: clamp the meta text to 3 lines. The truncation adds its
     own "…" — that ellipsis is the "there's more" cue, so no "Read more" label. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
}


/* ── HOMEPAGE WIDGET SHAPES ─────────────────────────────────────
   Front-end styles for the homepage block generator (built 20 Jul 2026 —
   admin/inc/homepage-builder.php). Each block is a .hp-block; the card
   shapes (grid2x2/row3/river/carousel) reuse the existing .post-card /
   .post-card--photo markup inside a shape wrapper, so a card looks the same
   everywhere. Alignment-first: equal columns, one shared gap, equal-height
   cards. All grids collapse to a single column on mobile (see media query). */
.homepage {
  display: flex;
  flex-direction: column;
  gap: 2.75rem;
}

.hp-block { margin: 0; width: 100%; }

/* Block heading — a small mono eyebrow, consistent across every shape. */
.hp-block-title {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 1rem;
}

/* Block header row: title left, "View all →" right. */
.hp-block-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  margin-bottom: 1rem;
}
.hp-block-head .hp-block-title { margin: 0; }
.hp-view-all {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--signal);
  text-decoration: none;
  white-space: nowrap;
}
.hp-view-all:hover { text-decoration: underline; }

/* Shared grid: equal columns, one gap, stretch so cards are equal height. */
.hp-grid {
  display: grid;
  gap: 1.25rem;
  align-items: stretch;
}
.hp-grid--2x2  { grid-template-columns: repeat(2, 1fr); }
.hp-grid--row3 { grid-template-columns: repeat(3, 1fr); }
.hp-grid--river { grid-template-columns: 1fr; }

/* Cards fill their cell and match heights within a row. */
.hp-grid > .post-card,
.hp-grid > .post-card--photo,
.hp-carousel-track > .post-card,
.hp-carousel-track > .post-card--photo { width: 100%; height: 100%; margin: 0; }

/* ── Hero ─────────────────────────────────────────────────────── */
.hp-hero-card {
  display: block;
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  background: var(--card);
  transition: border-color 0.2s, transform 0.2s;
}
.hp-hero-card:hover { border-color: var(--signal); transform: translateY(-2px); text-decoration: none; }
.hp-hero-media { width: 100%; aspect-ratio: 16 / 9; overflow: hidden; }
.hp-hero-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.hp-hero-text { padding: 1.75rem 2rem 2rem; }

/* Desktop: lay the hero out as a full-width side-by-side feature (image left,
   text right, vertically centred) so the card FILLS the whole content width
   and reads as a deliberate hero aligned with the grids below — rather than a
   half-empty box when the top image scrolls past. Stacks (image on top) below
   720px, and a no-photo hero always spans full width as text only. */
@media (min-width: 721px) {
  .hp-hero-card:not(.hp-hero-card--nophoto) {
    display: grid;
    grid-template-columns: 1.4fr 1fr;
    align-items: stretch;
  }
  .hp-hero-card:not(.hp-hero-card--nophoto) .hp-hero-media {
    aspect-ratio: auto;
    height: 100%;
    min-height: 320px;
  }
  .hp-hero-card:not(.hp-hero-card--nophoto) .hp-hero-text { align-self: center; }
}
.hp-hero-title {
  font-family: var(--font-display);
  font-size: clamp(1.4rem, 3.2vw, 2.05rem);
  font-weight: 600;
  line-height: 1.12;
  letter-spacing: -0.025em;
  color: var(--text);
  margin: 0 0 0.6rem;
  /* Cap to 2 lines, same fixed-height treatment as the grid cards
     (see .post-card__title, added 22 July 2026) — 1 Aug 2026. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
.hp-hero-card:hover .hp-hero-title { color: var(--signal); }
/* Cap to 3 lines, ellipsis as the "more" cue — matches .post-card__excerpt.
   No separate "Read more" label, same reasoning as the 22 July grid-card fix
   (1 Aug 2026). */
.hp-hero-excerpt {
  font-size: 1rem;
  color: var(--muted);
  margin: 0 0 0.75rem;
  max-width: 60ch;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
}
/* ── Carousel — vanilla horizontal scroll, prev/next in js/site.js ─ */
.hp-carousel { position: relative; }
.hp-carousel-track {
  display: flex;
  gap: 1.25rem;
  overflow-x: auto;
  scroll-behavior: smooth;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  padding-bottom: 0.5rem;
  scrollbar-width: thin;
}
.hp-carousel-track > .post-card,
.hp-carousel-track > .post-card--photo {
  flex: 0 0 300px;
  max-width: 300px;
  scroll-snap-align: start;
}
/* Photo cards stack vertically in the NARROW shapes (carousel ~300px, row3
   ~270px) — image on top, text below. The side-by-side thumbnail layout only
   has room in the wider shapes (grid2x2, river); in a narrow column it squeezes
   the text to one word per line. */
.hp-carousel-track > .post-card--photo,
.hp-grid--row3 > .post-card--photo {
  flex-direction: column;
  gap: 0.9rem;
  align-items: stretch;
}
.hp-carousel-track > .post-card--photo .post-card__thumb,
.hp-grid--row3 > .post-card--photo .post-card__thumb {
  width: 100%;
  height: 170px;
}
.hp-carousel-nav {
  position: absolute;
  top: calc(50% + 0.75rem);
  transform: translateY(-50%);
  z-index: 2;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 50%;
  border: 1px solid var(--card-border);
  background: var(--card);
  color: var(--signal);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 10px rgba(22, 24, 29, 0.10);
  transition: border-color 0.2s, opacity 0.2s;
}
.hp-carousel-nav:hover { border-color: var(--signal); }
.hp-carousel-nav[disabled] { opacity: 0.35; cursor: default; }
.hp-carousel-prev { left: -0.5rem; }
.hp-carousel-next { right: -0.5rem; }

/* ── Block quote (authored pull-quote, full container width) ──── */
.hp-quote {
  text-align: center;
  padding: 1rem 0;
}
.hp-quote-text {
  margin: 0 auto;
  max-width: 46rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(0.75rem, 1.7vw, 1.125rem);
  line-height: 1.28;
  letter-spacing: -0.02em;
  color: var(--text);
}
/* Big curly quotes hugging the text (accent colour): open before, close after.
   line-height:0 keeps the oversized glyphs from stretching the line box;
   vertical-align drops them to sit roughly centred on the text. */
/* Just the two quote characters from the typeface, larger and in the accent
   colour. No transforms or vertical-align tricks — they sit where the font
   draws them and stay level because they share the text's baseline. */
.hp-quote-text::before,
.hp-quote-text::after {
  color: var(--signal);
  font-size: 1.6em;
  line-height: 0;
}
.hp-quote-text::before { content: "\201C"; }
.hp-quote-text::after  { content: "\201D"; }
.hp-quote-text em { font-style: italic; }
.hp-quote-text strong { font-weight: 700; }
.hp-quote-cite {
  display: block;
  margin: 1.1rem auto 0;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  font-style: normal;
}
.hp-quote-cite::before { content: "— "; }

/* ── Text block (authored free-form content, 23 July 2026) ──────
   Reuses .article-body's typography wholesale (h2/h3/p/ul/ol/blockquote/
   img/a) rather than a second set of rules — same content, same styling,
   whether it's in a post or a text block. Only override: .article-body's
   first heading carries a 2.5rem top margin meant to separate it from
   preceding body text; inside a block that's already sitting in the
   .homepage flex column's own 2.75rem block gap, so it would double up. */
.hp-text > *:first-child { margin-top: 0; }

/* ── Newsletter band ──────────────────────────────────────────── */
.hp-newsletter {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 2.25rem 2rem;
  text-align: center;
}
.hp-newsletter-title {
  font-family: var(--font-display);
  font-size: 1.4rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text);
  margin: 0 0 0.5rem;
}
.hp-newsletter-text { font-size: 0.95rem; color: var(--muted); margin: 0; }
.hp-newsletter-form {
  display: flex;
  gap: 0.6rem;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: 1.25rem;
}
.hp-newsletter-form input[type="email"] {
  font-family: var(--font-body);
  font-size: 0.95rem;
  padding: 0.6rem 0.9rem;
  border: 1px solid var(--card-border);
  border-radius: 8px;
  background: var(--card);
  color: var(--text);
  min-width: 260px;
}
.hp-newsletter-form input[type="email"]:focus {
  outline: none;
  border-color: var(--signal);
  box-shadow: 0 0 0 3px var(--signal-dim);
}
.hp-newsletter-form button {
  font-family: var(--font-body);
  font-size: 0.9rem;
  font-weight: 600;
  padding: 0.6rem 1.4rem;
  border: none;
  border-radius: 8px;
  background: var(--signal);
  color: #fff;
  cursor: pointer;
  transition: opacity 0.2s;
}
.hp-newsletter-form button:hover { opacity: 0.88; }
/* Honeypot — kept in the DOM for bots, invisible + unfocusable for humans. */
.hp-newsletter-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
}
.hp-newsletter-note { margin-top: 0.9rem; font-size: 0.9rem; color: var(--signal); }
.hp-newsletter-note.is-error { color: var(--warn); }

/* ── BLOG CATEGORY FILTER ────────────────────────────────────── */
.blog-filter {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 2rem;
}
.blog-filter__btn {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.4rem 0.9rem;
  border: 1px solid var(--card-border);
  border-radius: 20px;
  background: var(--card);
  color: var(--muted);
  cursor: pointer;
  transition: border-color 0.2s, color 0.2s, background 0.2s;
}
.blog-filter__btn:hover { border-color: var(--signal); color: var(--text); }
.blog-filter__btn.is-active {
  background: var(--signal);
  border-color: var(--signal);
  color: #fff;
}
.post-card.is-hidden,
.post-card--photo.is-hidden { display: none; }

/* ── FIXED-LEFT / FIXED-RIGHT LEAD-PHOTO LAYOUT ─────────────── */
/* .breadcrumb / .post-header / .post-title / .post-meta (the old Photo
   Post article header) retired 24 July 2026 — every post uses the same
   .page-header now (see PROJECT-NOTES.md, "Merge Standard/Photo Post into
   one template"). .post-divider / .post-body / .post-lead-photo below are
   still live — they're the 'fixed-left'/'fixed-right' lead-photo
   placements, two of five options any post can now pick, not a second
   template. 'fixed-right' (added 24 July 2026, later the same day) is a
   plain mirror of 'fixed-left' — same grid, columns reversed, and
   templates.php swaps which element comes first in the markup so the
   narrow photo column lands on the right without any JS/order trickery. */

.post-divider {
  border: none;
  border-top: 1px solid var(--border);
  margin: 0 0 2rem;
}

.post-body {
  display: grid;
  grid-template-columns: 300px 1fr;
  gap: 2.25rem;
  align-items: start;
}

.post-body--right { grid-template-columns: 1fr 300px; }

.post-lead-photo { position: sticky; top: 1.5rem; }

.post-lead-photo img {
  width: 100%;
  height: auto;
  display: block;
  border-radius: 10px;
  border: 1px solid var(--border);
}

.photo-caption {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  color: var(--muted);
  margin-top: 0.5rem;
  line-height: 1.4;
}

/* .post-text's own typography rules retired 24 July 2026 — every post uses
   .article-body now (see PROJECT-NOTES.md, "Merge Standard/Photo Post into
   one template"); the fixed-left lead-photo layout's text column carries
   both classes (class="post-text article-body") so .post-text still hooks
   the grid layout below, it just no longer duplicates typography rules
   .article-body already has (and had more of, since .post-text never
   picked up heading/list/quote/image styling — a gap this merge fixes). */

.post-extra-photos {
  margin-top: 2.5rem;
  padding-top: 1.75rem;
  border-top: 1px solid var(--border);
}

.extra-photos-label {
  font-family: var(--font-mono);
  font-size: 0.65rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 1rem;
}

.post-back {
  margin-top: 3rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--border);
}
.post-back a {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--signal);
  text-decoration: none;
}
.post-back a:hover { text-decoration: underline; }

/* Prev/next post nav (24 July 2026, simplified same day — see
   PROJECT-NOTES.md) — filled in client-side by initPostNav() in
   js/site.js, which reads search-index.json (the same newest-first order
   the CMS already generates on every rebuild) to work out each post's
   neighbours. Just "Previous"/"Next" labels, no post title — Richard felt
   the title made the row too busy for what it's for. Prev/next links
   start [hidden] in the markup and are only revealed once JS resolves a
   real neighbour, so a post at either end of the list (or a page load
   before JS runs) just shows "All posts" alone rather than a dead arrow. */
.post-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 3rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--border);
  font-family: var(--font-mono);
}
.post-nav__link {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.78rem;
  color: var(--signal);
  text-decoration: none;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.post-nav__link[hidden] { display: none; }
.post-nav__link:hover { text-decoration: underline; }
.post-nav__all {
  flex: 0 0 auto;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.72rem;
}
.post-nav__all:hover { text-decoration: underline; }
.post-nav__arrow { font-size: 1rem; line-height: 1; }

/* Any post generated before this simplification still has an empty
   <span class="post-nav__title"> in its markup (JS no longer writes to
   it) — hidden outright so nothing shows even for those older files
   without needing to regenerate them again. */
.post-nav__title { display: none; }

@media (max-width: 560px) {
  .post-nav { flex-wrap: wrap; }
  .post-nav__all { order: -1; flex: 1 1 100%; text-align: center; margin-bottom: 0.4rem; }
}

@media (max-width: 680px) {
  .post-body,
  .post-body--right { grid-template-columns: 1fr; gap: 1.5rem; }
  .post-lead-photo { position: static; }
  /* fixed-right's markup has the text div first (so the photo lands in
     the right-hand column on desktop) — on a single stacked column that
     would put the photo below the text, so pull it back above with
     `order` to match fixed-left's stacked look. */
  .post-body--right .post-lead-photo { order: -1; }
}

/* Narrow phones: the photo card stacks (image above text).
 *
 * `align-items: stretch` and the explicit width on the thumb LINK are both
 * load-bearing (21 July 2026). Switching to `flex-direction: column` alone
 * left the flex items shrink-wrapping their content, so
 * `.post-card__thumb-link` had no width of its own and the thumb's
 * `width: 100%` resolved against an already-collapsed box. The result was
 * thumbnails that kept the 180px height but took whatever width their
 * photo's natural shape implied — measured at 412px wide: portrait photos
 * 134px, landscape ones 257–266px, none of them full width. Cards looked
 * ragged and inconsistent on mobile. Stretching the link makes the 100%
 * mean the card width, as intended. */
@media (max-width: 500px) {
  .post-card--photo { flex-direction: column; align-items: stretch; }
  .post-card__thumb-link { width: 100%; }
  .post-card__thumb { width: 100%; height: 180px; }
}

/* Homepage shapes collapse gracefully on narrow screens. */
@media (max-width: 720px) {
  .hp-grid--row3 { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 500px) {
  .hp-grid--2x2,
  .hp-grid--row3 { grid-template-columns: 1fr; }
  .hp-hero-text { padding: 1.25rem 1.25rem 1.5rem; }
  .hp-carousel-nav { display: none; }
}

/* ── PHOTO GALLERY + LIGHTBOX ────────────────────────────────────
   Shared component used by the Photo Post template (multi-photo
   extra-photos section) and by the standalone Photo Tool's
   multi-photo snippet on freeform pages.

   Visible state is a fixed 2-column thumbnail grid (crops to a
   consistent frame — normal for a thumbnail). Clicking a thumbnail
   opens a single shared full-screen lightbox (one instance per page,
   reused across every .photo-gallery on it) which shows each photo
   at its natural aspect ratio, un-cropped, so portrait and landscape
   photos both display correctly — only the small grid preview crops.

   Markup (filenames baked in at generation time — no runtime
   folder discovery):
     <div class="photo-gallery"
          data-path="../images/articles/my-post/"
          data-images="a.webp|b.webp|c.webp"
          data-captions="Caption one||Caption three">
       <!-- thumbnails injected by initGalleries() in js/site.js -->
     </div>
   ───────────────────────────────────────────────────────────── */
.photo-gallery {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.75rem;
  margin: 1.5rem 0;
}
.photo-gallery__thumb {
  position: relative;
  display: block;
  width: 100%;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  background: var(--surface);
  cursor: pointer;
}
.photo-gallery__thumb img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  display: block;
  transition: transform 0.25s ease;
}
.photo-gallery__thumb:hover img { transform: scale(1.04); }
.photo-gallery__caption {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.65rem;
  color: var(--muted);
  padding: 0.4rem 0.6rem;
  background: var(--card);
  text-align: left;
}

@media (max-width: 500px) {
  .photo-gallery { grid-template-columns: 1fr; }
}

/* ── LIGHTBOX — one shared overlay per page ─────────────────── */
.photo-lightbox {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(16, 18, 26, 0.9);
  z-index: 1000;
  align-items: center;
  justify-content: center;
  padding: 3rem 1rem;
}
.photo-lightbox.open { display: flex; }
body.lightbox-open { overflow: hidden; }

.photo-lightbox__figure {
  max-width: min(90vw, 1000px);
  max-height: 85vh;
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.photo-lightbox__img {
  max-width: 100%;
  max-height: 75vh;
  width: auto;
  height: auto;
  object-fit: contain;
  display: block;
  border-radius: 6px;
  background: var(--card);
}
.photo-lightbox__caption {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--subtle);
  margin-top: 0.75rem;
  text-align: center;
}
.photo-lightbox__counter {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  color: var(--subtle);
  margin-top: 0.4rem;
  letter-spacing: 0.08em;
}
.photo-lightbox__close,
.photo-lightbox__arrow {
  position: fixed;
  background: rgba(255,255,255,0.1);
  border: 1px solid rgba(255,255,255,0.3);
  color: #fff;
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s;
}
.photo-lightbox__close:hover,
.photo-lightbox__arrow:hover { background: rgba(255,255,255,0.25); }
.photo-lightbox__close {
  top: 1.25rem;
  right: 1.25rem;
  width: 40px;
  height: 40px;
  font-size: 1.3rem;
}
.photo-lightbox__arrow {
  top: 50%;
  transform: translateY(-50%);
  width: 46px;
  height: 46px;
  font-size: 1.4rem;
}
.photo-lightbox__arrow--prev { left: 1rem; }
.photo-lightbox__arrow--next { right: 1rem; }

@media (max-width: 600px) {
  .photo-lightbox__close { width: 34px; height: 34px; font-size: 1.1rem; top: 0.75rem; right: 0.75rem; }
  .photo-lightbox__arrow { width: 38px; height: 38px; font-size: 1.2rem; }
  .photo-lightbox__arrow--prev { left: 0.5rem; }
  .photo-lightbox__arrow--next { right: 0.5rem; }
}

/* ── SEARCH OVERLAY — fuzzy site search, one shared instance per page ── */
.search-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(16, 18, 26, 0.85);
  z-index: 1000;
  align-items: flex-start;
  justify-content: center;
  padding: 4rem 1rem 2rem;
}
.search-overlay.open { display: flex; }
body.search-open { overflow: hidden; }

.search-overlay__panel {
  width: 100%;
  max-width: 640px;
  max-height: 80vh;
  background: var(--card);
  border-radius: var(--radius);
  border: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.search-overlay__head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.9rem 1rem;
  border-bottom: 1px solid var(--border);
}
.search-overlay__icon {
  font-size: 0.95rem;
  color: var(--muted);
  flex-shrink: 0;
}
.search-overlay__input {
  flex: 1;
  min-width: 0;
  border: none;
  outline: none;
  background: none;
  font-family: var(--font-body);
  font-size: 1rem;
  color: var(--text);
}
.search-overlay__input::placeholder { color: var(--muted); }
.search-overlay__close {
  flex-shrink: 0;
  background: none;
  border: none;
  color: var(--muted);
  font-size: 1.1rem;
  cursor: pointer;
  padding: 0.2rem 0.4rem;
  border-radius: 6px;
}
.search-overlay__close:hover { color: var(--signal); background: var(--signal-dim); }

.search-overlay__results {
  overflow-y: auto;
  padding: 0.5rem;
}
.search-overlay__empty {
  padding: 1.5rem 1rem;
  text-align: center;
  font-size: 0.85rem;
  color: var(--muted);
}

.search-overlay__result {
  display: block;
  padding: 0.75rem 0.85rem;
  border-radius: 8px;
  text-decoration: none;
  color: var(--text);
  transition: background 0.12s;
}
.search-overlay__result:hover,
.search-overlay__result.active {
  background: var(--signal-dim);
  text-decoration: none;
}
.search-overlay__result-meta {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--signal);
  margin-bottom: 0.2rem;
}
.search-overlay__result-title {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 0.92rem;
  color: var(--text);
  margin-bottom: 0.2rem;
}
.search-overlay__result-excerpt {
  font-size: 0.8rem;
  color: var(--muted);
  line-height: 1.5;
}

@media (max-width: 600px) {
  .search-overlay { padding: 2.5rem 0.5rem 1rem; }
  .search-overlay__panel { max-height: 85vh; }
}

/* Single "floating" image — freeform pages, text wraps around it */
.floating-photo {
  float: right;
  max-width: 340px;
  margin: 0 0 1.5rem 1.5rem;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--card);
  overflow: hidden;
}
.floating-photo--left { float: left; margin: 0 1.5rem 1.5rem 0; }
/* margin:0 override added 1 Aug 2026 — without it, the generic
   .article-body img margin (1.5rem 0, from wholesale reuse of .article-body
   typography in text blocks) gets trapped inside this wrapper's own
   overflow:hidden (a new block-formatting context), showing as an
   unwanted coloured gap top/bottom where the wrapper's own background
   peeks through around the photo. The wrapper's own margin already
   handles spacing, so the image itself doesn't need any. */
.floating-photo img { width: 100%; height: auto; display: block; margin: 0; }
.floating-photo figcaption {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  color: var(--muted);
  padding: 0.5rem 0.75rem;
}
@media (max-width: 600px) {
  .floating-photo, .floating-photo--left { float: none; max-width: 100%; margin: 0 0 1.5rem; }
}

/* Single in-flow image — same look as .floating-photo (border, radius,
   figcaption style) but no float, so it just sits in the text and scrolls
   with it rather than being wrapped. Added 24 July 2026 for the admin
   insert-photo toolbar's "inline" placement option, alongside float
   left/right. Centred by default since an in-flow image with no wrap
   reads oddly hugging one edge. */
.inline-photo {
  max-width: 100%;
  margin: 0 auto 1.5rem;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--card);
  overflow: hidden;
}
/* margin:0 override added 1 Aug 2026 — same trapped-margin bug as
   .floating-photo above (see its comment): this wrapper's own
   overflow:hidden traps the inherited .article-body img margin instead
   of letting it collapse outside, showing as an unwanted coloured gap
   top/bottom. The wrapper's own margin already provides the spacing. */
.inline-photo img { width: 100%; height: auto; display: block; margin: 0; }
.inline-photo figcaption {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  color: var(--muted);
  padding: 0.5rem 0.75rem;
}

/* ── UTILITIES ──────────────────────────────────────────────── */
.mt3 { margin-top: 3rem; }

@media (max-width: 600px) {
  .product-card,
  .download-card { max-width: 100%; }
}

/* ── CONTACT FORM ───────────────────────────────────────────── */
.contact-form {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  max-width: 560px;
}
.form-group {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.form-group label {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}
.form-group input,
.form-group textarea {
  font-family: var(--font-body);
  font-size: 1rem;
  color: var(--text);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.65rem 0.85rem;
  transition: border-color 0.15s;
}
.form-group input:focus,
.form-group textarea:focus {
  outline: none;
  border-color: var(--signal);
}
.form-group textarea { resize: vertical; min-height: 140px; }

.contact-form .btn {
  border: none;
  cursor: pointer;
  align-self: flex-start;
}

/* Honeypot field — hidden from sighted users, tabbed-past by keyboard nav,
   but left in the markup/DOM for bots that fill in every field indiscriminately */
.contact-form__hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.contact-message {
  border-radius: var(--radius);
  padding: 1rem 1.25rem;
  margin-bottom: 1.5rem;
  font-size: 0.95rem;
}
.contact-message--success {
  background: var(--signal-dim);
  border: 1px solid var(--signal);
  color: var(--signal);
}
.contact-message--error {
  background: rgba(181,86,44,0.08);
  border: 1px solid var(--warn);
  color: var(--warn);
}
