/* =====================================================================================
   Atlas of Wales — TOWN / VILLAGE SHARED PRESENTATION LAYER  (assets/town-guide.css)

   Extracted verbatim (2026-07-23, town/village Phase 2) from the inline stylesheet of the
   CANONICAL MASTER TEMPLATE, pages/locations/beaumaris-town.html. Beaumaris is the sole
   authority for this layer: nothing here was averaged with abersoch.html (a later adaptive
   example) or conwy/town-village/betws-y-coed.html (an older-generation implementation).

   Rules for this file:
     - It is the complete town/village visual system. A town page should carry NO component
       CSS of its own; anything genuinely page-specific goes in a small page-level <style>
       AFTER this file.
     - LOAD ORDER MATTERS. It must come AFTER assets/gallery-lightbox.css and
       assets/saved-places.css, because it deliberately overrides both at equal specificity
       (the lightbox backdrop, and the saved-place control skin). That ordering was the
       reason the rules lived in a page-level <style> before extraction; the requirement is
       unchanged, only its location.
     - Selectors, values, ordering and breakpoints are byte-identical to the Beaumaris
       original. Nothing was renamed, merged or "tidied" during extraction — the point of
       Phase 2 is provable parity, not a redesign.
     - Typography (including the Playfair Display webfont linked by the page) is out of
       scope for this phase and is unchanged.

   Adaptive counts: components that scale honestly with the evidence read their own item
   count from a data-count attribute (.ways3, .lifecats, .cdlg__facts, .cdlg__split). Never
   pad a grid to fill it — see the audit's adaptive-count rules.
   ===================================================================================== */

/* =====================================================================================
   TOKENS — token-identical to the production location pages. No new palette invented.
   ===================================================================================== */
:root {
    color-scheme: dark;
    --ink: #1a2c21;
    --night: #20372c;
    --deep-sea: #1b4230;
    --forest: #2b5440;
    --gold: #d8bd85;
    --gold-warm: #d7b271;
    --gold-bright: #efce92;
    --amber: #d9823b;
    --cream: #f8efd8;
    --stone: #eadfc8;

    --paper-top: #f8eed2;
    --paper: #f4e8c9;
    --paper-warm: #efe1bd;
    --paper-ink: #232a1f;
    --paper-soft: rgba(48, 49, 31, 0.86);
    /* 0.64 measured 3.85-4.09:1 on the cream surfaces — under AA for body text at every
       size it is used at (.catcard__meta, .cdlg__fact dt, .ptab-panel__note,
       .orient-card.is-active .orient-card__cat). This is the classic "muted ink on tinted
       near-white" failure: elegant in isolation, unreadable in daylight, which is exactly
       the context this page is planned in. 0.8 clears AA on all four consumers and still
       reads as a clear step below --paper-soft. */
    --paper-mute: rgba(48, 49, 31, 0.8);
    --paper-rule: rgba(118, 94, 50, 0.24);
    /* #8a6a2e was the marginal token of the whole cream band: 4.34:1 on --paper-top and
       4.12:1 on --paper, so .ptab-panel__link failed outright while .catcard__cue (4.52),
       .pfact__label (4.55), .gcard__no (4.62) and .step__transport (4.67) all sat within a
       rounding error of failing. Darkening the token once lifts every bronze consumer clear
       instead of patching them one at a time: now 5.13:1 / 4.87:1. Chosen over #836427,
       which only reached 4.51 on the darker cream stop — passing on paper, not in practice.
       --bronze-soft is a separate border token and is deliberately untouched. */
    --bronze: #7d5f26;
    --bronze-soft: rgba(150, 116, 52, 0.5);
    --paper-heading: #2b3022;

    --green-card: #18382b;
    --green-card-top: #214534;
    --green-rule: rgba(228, 196, 122, 0.26);

    /* --- Chapter surfaces -------------------------------------------------------------
       Surface tone encodes the CHAPTER; the seam encodes a section within it. Tone changes
       therefore happen only at chapter boundaries — never as decoration between two
       sections of the same chapter.
       These replace the old .band--deep, which was #12241b -> #16281f -> #142519: within a
       few hex points of the body it sat on, so the band named "deep" read as no change at
       all and eight of eleven sections shared one surface. */
    --surface-deep: #101d16;
    --surface-base: #16281f;
    --surface-mid:  #1c3527;
    /* The tier-2 seam. Deliberately weaker than any tone change, so a section boundary can
       never out-weigh a chapter boundary. */
    --seam-line: rgba(228, 196, 122, 0.28);

    /* --- v2 layout system ------------------------------------------------------------
       One stage width for the whole page. Every band aligns to it, which is what makes
       the page read as one document rather than ten stacked mock-ups. */
    --stage:      min(92vw, 1520px);
    --stage-wide: min(96vw, 1720px);
    --stage-tight: min(90vw, 1200px);
    --gutter: clamp(20px, 4vw, 56px);
    --read: 66ch;
    /* Real, measured .topbar height (10px padding + 46px link min-height + 1px border,
       same at every breakpoint) — used as the scroll-margin-top offset for in-page anchor
       targets that would otherwise land flush behind the sticky bar. */
    --topbar-h: 67px;

    /* Vertical rhythm. Bands own their spacing; nothing relies on a gap between panels. */
    /* v3: trimmed from 64/104 and 92/140 — the band seams were reading as empty transitions
       at 1440 rather than as breathing room. */
    --band-pad:    clamp(54px, 5.2vw, 84px);
    /* Chapter-OPENING bands only. Deliberately SMALLER than --band-pad: the chapter marker
       itself now occupies the space the padding used to hold, so keeping the full band pad
       above it stacked two empty gaps and read as a blank strip before the heading (worst
       before "04 Plan"). This puts the marker 48-64px below the surface boundary. */
    --chapter-pad: clamp(48px, 4.2vw, 64px);

    /* --- Hero / guide-strip tuck ------------------------------------------------------
       ONE owned overlap value, not two clamps that have to sum correctly.
       Previously .hero__inner reserved `clamp(190px, 23vh, 260px)` while .guidestrip
       pulled up `clamp(-215px, -19.5vh, -165px)`: different vh coefficients and
       different bounds, so the surviving clear space was whatever the two happened to
       leave — and at some viewport shapes that was NEGATIVE. Measured collisions:
       820x1000 the CTA row overlapped the strip by 15px, and 1024x820 the scroll cue
       did. Because .guidestrip sits at --z-raised it painted over the buttons AND took
       their clicks, so this cost hit area on a primary CTA, not just pixels. (The scroll
       cue had already been moved once to escape a collision with the CTAs — see
       .hero__scroll — and landed in this one, which is the tell that the values were
       never the problem.)
       Now: the strip pulls up by --strip-pull, the hero reserves that SAME token plus
       --strip-clear. Clear space is therefore always exactly --strip-clear, at every
       viewport, and a breakpoint retunes the tuck by overriding one variable rather
       than by re-deriving both sides of a subtraction. */
    --strip-pull: clamp(230px, 27vh, 300px);
    --strip-clear: clamp(32px, 4vh, 48px);

    --r-sm: 10px;
    --r-md: 16px;
    --r-lg: 22px;
    /* The Local-flavours joined-pair motif: --r-plate on outer corners, --r-join on the
       shared inner edge of the lead + photograph pair. Tokenised so the chapter's five
       hardcoded radii stay one decision. */
    --r-plate: 13px;
    --r-join: 4px;

    --serif: "Playfair Display", Georgia, "Times New Roman", serif;
    --sans: "Segoe UI", Arial, sans-serif;

    /* Semantic z-scale — no arbitrary 999s. */
    --z-base: 1;
    --z-raised: 5;
    --z-sticky: 40;
    --z-nav: 50;
}

* { box-sizing: border-box; }
/* The `hidden` attribute must actually hide. Any author `display` declaration — `.btn`'s
   inline-flex, `.cdlg__access`'s flex — sits at equal specificity to the UA stylesheet's
   `[hidden] { display: none }` and, coming later, silently wins: the element stays visible
   AND keyboard-focusable while `el.hidden` reads true, so scripted checks all pass while
   the page shows a control that should not be there. The .ptab-panel[hidden] rule further
   down was a one-off patch for the same trap; this makes it hold everywhere by default. */
[hidden] { display: none !important; }
/* Anchor offset lives in ONE place — the .band[id] scroll-margin-top rule below. A
   scroll-padding-top here would be a second, competing system. */
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
    margin: 0;
    overflow-x: hidden;
    background: #16281f;
    color: var(--cream);
    font-family: var(--sans);
    font-size: 1.06rem;
    line-height: 1.62;
    -webkit-font-smoothing: antialiased;
}

img { display: block; max-width: 100%; }
svg { display: block; }

/* CONTINUITY DEVICE 1 — one contour field behind the entire page, fixed so it reads as a
   single sheet the content moves over, rather than a texture restarting each section. */
body::before {
    content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none; opacity: 0.5;
    background-image:
        radial-gradient(ellipse 58% 40% at 22% 18%, transparent 46%, rgba(228, 196, 122, 0.07) 46.6%, transparent 48.4%),
        radial-gradient(ellipse 46% 32% at 22% 18%, transparent 56%, rgba(228, 196, 122, 0.055) 56.6%, transparent 58.4%),
        radial-gradient(ellipse 64% 44% at 84% 74%, transparent 42%, rgba(228, 196, 122, 0.06) 42.6%, transparent 44.4%),
        radial-gradient(ellipse 50% 34% at 84% 74%, transparent 54%, rgba(228, 196, 122, 0.045) 54.6%, transparent 56.4%);
}
body > * { position: relative; z-index: var(--z-base); }

/* =====================================================================================
   BANDS — the v2 structural primitive. A band spans the viewport and owns its own
   surface; content inside aligns to the shared stage. Replaces v1's panel-in-a-gap.
   ===================================================================================== */
.band { position: relative; padding: var(--band-pad) var(--gutter); }
/* The dedicated chapter-opening rule. Carried by exactly the three bands that open a chapter
   (#experiences, #story, #visit) and nothing else, so the compact value is safe here. */
.band--chapter { padding-top: var(--chapter-pad); }
/* The matching chapter-ENDING rule: clear dark space beneath a chapter's final card before
   the surface changes. Same token as the opening on purpose — a chapter's entry and exit
   gutters should share one rhythm. Carried only by chapters that END on a large card or
   panel (#life -> .life__foot, #practical -> .planv__arrive); a chapter ending on a button or
   a chip row does not need it, and ordinary same-chapter seams must not get it. */
.band--chapter-end { padding-bottom: var(--chapter-pad); }
.band--tight-top { padding-top: clamp(32px, 3.4vw, 52px); }
/* Two bands inside the SAME chapter sit closer than two chapters do. Without this, the
   in-chapter seam read as the same break as a chapter change. */
.band--pair { padding-bottom: clamp(40px, 4vw, 62px); }
/* .band--flush-bottom (padding-bottom: 0) was removed here. Its only user was #life, where it
   collapsed the gap entirely: the "A slower pace" card's bottom edge WAS the surface boundary,
   so the panel read as merged into the Plan chapter below. #life now uses .band--chapter-end. */
/* The marker sits above the heading inside the band, so an in-page jump has to clear the
   sticky topbar AND leave the marker visible. --topbar-h is the real measured height. */
.band[id] { scroll-margin-top: calc(var(--topbar-h) + 24px); }
.stage { width: var(--stage); margin: 0 auto; }
.stage--wide { width: var(--stage-wide); margin: 0 auto; }
.stage--tight { width: var(--stage-tight); margin: 0 auto; }

/* Cream band: an intentional FEATURE surface, not a chapter tone. The catalogue is an
   editorial insert sitting inside the Discover chapter — it is the one light plate on the
   page, and it does not signal a chapter change. Its own top/bottom rules are kept: they
   belong to the insert, and are why it needs no .band--seam. */
.band--cream {
    background: linear-gradient(178deg, var(--paper-top), var(--paper) 60%, var(--paper-warm));
    color: var(--paper-ink);
}
.band--cream::before, .band--cream::after {
    content: ""; position: absolute; left: 0; right: 0; height: 1px;
    background: linear-gradient(90deg, transparent, rgba(158, 108, 36, 0.4), transparent);
}
.band--cream::before { top: 0; }
.band--cream::after { bottom: 0; }

/* --- Chapter surfaces. One flat tone per chapter, flipped only at a chapter boundary. --- */
.band--deep { background: var(--surface-deep); }
.band--mid  { background: var(--surface-mid); }

/* TIER 2 — the section seam. Used ONLY between two sections that share a surface; where the
   surface already changes, a seam on top of it is doubled-up noise. Attached to the INCOMING
   section so the rule belongs to what you are arriving at, and drawn at stage width so it
   reads as a rule beneath the content rather than a border around the band. */
.band--seam::before {
    content: ""; position: absolute; top: 0; left: 50%; transform: translateX(-50%);
    width: var(--stage); height: 1px;
    background: linear-gradient(90deg, transparent, var(--seam-line), transparent);
}

/* =====================================================================================
   TYPOGRAPHY — materially larger than v1 across every level.
   ===================================================================================== */
h1, h2, h3, h4 { font-family: var(--serif); font-weight: 600; margin: 0; text-wrap: balance; }
p { text-wrap: pretty; }

.h-display {
    font-size: clamp(3.1rem, 6.4vw, 5.2rem);
    line-height: 0.96; letter-spacing: -0.02em;
}
.h-section {
    font-size: clamp(2.3rem, 4.2vw, 3.7rem);
    line-height: 1.04; letter-spacing: -0.016em;
}
.h-sub {
    font-size: clamp(1.5rem, 2.2vw, 2.05rem);
    line-height: 1.14; letter-spacing: -0.01em;
}
.lede {
    margin: 20px 0 0; max-width: var(--read);
    font-size: clamp(1.08rem, 1.22vw, 1.24rem); line-height: 1.62;
    color: rgba(248, 239, 216, 0.84);
}
.band--cream .lede { color: var(--paper-soft); }

/* A single named kicker system, used on three sections only — not above every heading. */
.kicker {
    display: inline-flex; align-items: center; gap: 12px; margin: 0 0 16px;
    color: rgba(236, 199, 110, 0.92);
    font-family: var(--sans); font-size: 0.8rem; font-weight: 700;
    letter-spacing: 0.14em; text-transform: uppercase;
}
.kicker::before { content: ""; width: 30px; height: 1px; background: currentColor; opacity: 0.7; }

/* =====================================================================================
   TIER 1 — CHAPTER MARKER. Four on the page, one per chapter. The page already promises
   four chapters in the guide strip and, before this, never marked where any of them began
   (they existed only in HTML comments). Deliberately COMPACT: one row, 45-65px including
   its bottom margin. It is a wayfinding mark, not a title card — the chapter's own
   .h-section heading is what carries the weight directly below it.
   Type values are .kicker's, reused rather than re-declared.
   ===================================================================================== */
.chaptermark {
    display: flex; align-items: center; gap: 12px;
    /* 24-32px to the heading below — close enough to read as one unit with it, not as its
       own floating row. */
    margin: 0 0 clamp(24px, 2.2vw, 32px);
    color: rgba(236, 199, 110, 0.92);
    font-family: var(--sans); font-size: 0.8rem; font-weight: 700;
    letter-spacing: 0.14em; text-transform: uppercase;
}
.chaptermark::before {
    content: ""; flex: 0 0 auto; width: 40px; height: 1px;
    background: currentColor; opacity: 0.7;
}
.chaptermark__no { flex: 0 0 auto; font-variant-numeric: tabular-nums; opacity: 0.75; }
/* One icon PER CHAPTER, not one repeated motif — a roofline, a compass, an open book, a
   signpost. The repeated abstract glyph said nothing about the chapter it sat on; a specific
   mark does. 16px still sits inside the 12.8px/1.62 text line box (~20.7px), so the marker
   height is unchanged. Restrained opacity keeps it secondary to the chapter name, which is
   the thing that should be read first. Same line language as every other icon on the page. */
.chaptermark__glyph {
    width: 16px; height: 16px; flex: 0 0 auto; opacity: 0.6;
    fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round;
}
.band--cream .chaptermark { color: var(--bronze); }
.band--cream .kicker { color: var(--bronze); }

.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: 10px;
    min-height: 54px; padding: 0 28px;
    border: 1px solid rgba(228, 196, 122, 0.5); border-radius: 999px;
    background: rgba(228, 196, 122, 0.14); color: #f8e8bd; text-decoration: none;
    font: inherit; font-size: 1.02rem; font-weight: 600; cursor: pointer;
    transition: border-color 0.28s cubic-bezier(0.22, 1, 0.36, 1), background 0.28s cubic-bezier(0.22, 1, 0.36, 1), transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}
.btn:hover, .btn:focus-visible { border-color: rgba(244, 216, 161, 0.85); background: rgba(217, 130, 59, 0.2); transform: translateY(-2px); }
.btn:focus-visible { outline: 2px solid var(--gold-warm); outline-offset: 3px; }
.btn--solid { background: linear-gradient(180deg, #c19543, #a37a30); border-color: rgba(244, 216, 161, 0.55); color: #fff8e6; }
.btn--solid:hover, .btn--solid:focus-visible { background: linear-gradient(180deg, #cea04a, #b08636); }
.btn--onpaper { border-color: var(--bronze-soft); background: rgba(255, 255, 255, 0.4); color: var(--paper-heading); }
.btn--onpaper:hover, .btn--onpaper:focus-visible { background: rgba(255, 255, 255, 0.72); border-color: var(--bronze); }
.btn svg { width: 18px; height: 18px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
@media (prefers-reduced-motion: reduce) { .btn { transition: none; } .btn:hover, .btn:focus-visible { transform: none; } }

/* Reveal: enhances an ALREADY-VISIBLE default (never gates content on a transition). */
.reveal { opacity: 1; }
html.js .reveal { opacity: 0; transform: translateY(14px); transition: opacity 0.75s cubic-bezier(0.22, 1, 0.36, 1), transform 0.75s cubic-bezier(0.22, 1, 0.36, 1); }
html.js .reveal.is-visible, html.reveal-failsafe .reveal { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) { html.js .reveal { opacity: 1; transform: none; transition: none; } }

/* =====================================================================================
   STICKY NAV — compact, current-section aware.
   ===================================================================================== */
.topbar {
    position: sticky; top: 0; z-index: var(--z-sticky);
    display: flex; align-items: center; justify-content: space-between; gap: 24px;
    padding: 10px var(--gutter);
    background: rgba(18, 33, 25, 0.86); backdrop-filter: blur(12px);
    border-bottom: 1px solid rgba(228, 196, 122, 0.16);
}
.brand { display: inline-flex; align-items: center; gap: 11px; min-height: 46px; color: rgba(248, 239, 216, 0.95); font-family: var(--serif); font-size: 1.08rem; font-style: italic; text-decoration: none; }
/* 0.64rem is the page's type floor — 0.56rem (~9px) was below comfortable legibility. */
.brand__tag { padding: 3px 10px; border: 1px solid rgba(228, 196, 122, 0.42); border-radius: 999px; color: rgba(236, 199, 110, 0.92); font-family: var(--sans); font-size: 0.64rem; font-style: normal; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; }
.brand:focus-visible { outline: 2px solid var(--gold-warm); outline-offset: 3px; border-radius: 4px; }
.topnav { display: flex; align-items: center; gap: clamp(4px, 1.1vw, 18px); }
.topnav a { position: relative; display: inline-flex; align-items: center; min-height: 46px; padding: 0 4px; color: rgba(248, 239, 216, 0.78); text-decoration: none; font-size: 0.95rem; }
.topnav a::after { content: ""; position: absolute; left: 4px; right: 4px; bottom: 8px; height: 1.5px; background: var(--gold-warm); transform: scaleX(0); transform-origin: left; transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1); }
.topnav a:hover { color: var(--cream); }
.topnav a.is-current { color: var(--gold-bright); }
.topnav a.is-current::after { transform: scaleX(1); }
.topnav a:focus-visible { outline: 2px solid var(--gold-warm); outline-offset: 2px; border-radius: 3px; }
@media (prefers-reduced-motion: reduce) { .topnav a::after { transition: none; } }
@media (max-width: 1080px) { .topnav { display: none; } }

/* -------------------------------------------------------------------------------------
   PILOT (Conwy town, 2026-07) — place-guide header parity. Opt in with
   `class="town-pilot-header"` on <body>; without it a town page is untouched.

   Brings the town header into line with the canonical-coast place-guide
   (.site-header in each place-guide page's own CSS): the bar is absolute over the
   hero and scrolls away with it instead of following down the page, it carries no
   surface of its own, and the right-hand end closes on Save + Enter the Map.

   It also retires the fixed bottom .mininav, which the place-guide template has no
   equivalent of. On <=1080px that leaves the page with no section jump — the
   place-guide trade-off, adopted deliberately, not overlooked.
   ------------------------------------------------------------------------------------- */
body.town-pilot-header .topbar {
    position: absolute; left: 0; right: 0;
    /* Clears a notch / status bar the same way location-motion.css does for .site-header. */
    top: env(safe-area-inset-top, 0px);
    padding: 18px var(--gutter);
    background: none; backdrop-filter: none; border-bottom: 0;
}
/* No sticky bar left to clear, so an in-page jump only needs the marker's own clearance. */
body.town-pilot-header .band[id] { scroll-margin-top: 24px; }
/* Belt-and-braces: the markup drops .mininav, so the 1080px query's body padding
   would otherwise leave 64px of dead space under the footer. */
body.town-pilot-header .mininav { display: none; }
body.town-pilot-header { padding-bottom: 0; }

.topbar__end { display: flex; align-items: center; gap: clamp(10px, 1.4vw, 22px); }
.nav-cta {
    display: inline-flex; align-items: center; justify-content: center;
    min-height: 42px; padding: 0 18px;
    border: 1px solid rgba(228, 196, 122, 0.48); border-radius: 999px;
    color: var(--cream); text-decoration: none; font-size: 0.9rem;
    transition: background 0.2s, border-color 0.2s;
}
.nav-cta:hover { background: rgba(228, 196, 122, 0.12); border-color: var(--gold-warm); }
.nav-cta:focus-visible { outline: 2px solid var(--gold-warm); outline-offset: 3px; }
/* saved-places.css hides .nav-save above 760px because a place-guide's hero carries its
   own Save there. A town page has no hero Save, so the header icon is the only entry
   point and stays visible at every width. */
body.town-pilot-header .nav-save.atlas-save { display: inline-flex; flex: 0 0 auto; }
/* ...and it keeps the compact icon form at every width, which saved-places.css scopes to
   <=760px. Two text buttons side by side would out-weigh the brand. */
body.town-pilot-header .nav-save .atlas-save__primary { width: 44px; height: 44px; min-height: 44px; padding: 0; border-radius: 50%; gap: 0; }
body.town-pilot-header .nav-save .atlas-save__icon { width: 16px; height: 19px; }
body.town-pilot-header .nav-save .atlas-save__label { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }

.skip { position: absolute; left: -9999px; top: 0; z-index: var(--z-nav); padding: 14px 20px; background: var(--paper); color: var(--paper-ink); border-radius: 0 0 8px 0; text-decoration: none; font-weight: 700; }
.skip:focus { left: 0; }

/* =====================================================================================
   CHAPTER 1 — ARRIVAL: hero + snapshot, joined by an overlapping cream guide strip.
   ===================================================================================== */
.hero {
    position: relative; isolation: isolate;
    min-height: min(96vh, 1000px);
    display: grid; align-items: end;
    padding: clamp(80px, 11vh, 150px) var(--gutter) 0;
    overflow: hidden;
}
/* CONTINUITY DEVICE 2 — the photograph bleeds to the viewport edge, not into a rounded box. */
.hero__media { position: absolute; inset: 0; z-index: -2; }
.hero__media img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 56%; filter: brightness(1.12); }
/* Brightness lift (above) + flattened scrim (below), rolled out from the Beaumaris pilot,
   2026-07-24: the original scrim's left 0.95 / right 0.22 swing read as a stark dark-left,
   bright-right split once any hero photo brightened past a certain point. Flattened to a
   narrower 0.72-0.34 range so every hero reads as one evenly-lit scene; the hero title/copy
   (left) still sits on the more-darkened end for legibility. */
.hero__scrim {
    position: absolute; inset: 0; z-index: -1; pointer-events: none;
    background:
        linear-gradient(96deg, rgba(12, 24, 18, 0.72) 0%, rgba(14, 28, 21, 0.62) 34%, rgba(16, 32, 24, 0.42) 60%, rgba(18, 34, 26, 0.34) 100%),
        linear-gradient(180deg, rgba(12, 24, 18, 0.72) 0%, transparent 26%, transparent 58%, rgba(12, 24, 18, 0.82) 100%);
}
/* Reserves the strip's own tuck plus a guaranteed clearance — see --strip-pull. */
.hero__inner { width: var(--stage); margin: 0 auto; padding-bottom: calc(var(--strip-pull) + var(--strip-clear)); }
.hero__place { display: inline-flex; align-items: center; gap: 10px; margin: 0 0 18px; color: rgba(236, 199, 110, 0.94); font-size: 0.86rem; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; }
.hero__place svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.hero h1 { max-width: 14ch; color: #fdf7e6; text-shadow: 0 4px 40px rgba(0, 0, 0, 0.5); }
.hero__welsh { display: block; margin-top: 0.16em; color: var(--gold-warm); font-size: 0.26em; font-style: italic; font-weight: 500; letter-spacing: 0.02em; }
.hero__rule { width: 74px; height: 3px; margin: clamp(18px, 2vh, 26px) 0 0; background: var(--gold-warm); border: 0; opacity: 0.9; }
.hero__stand { margin: 22px 0 0; max-width: 36ch; font-size: clamp(1.02rem, 1.12vw, 1.14rem); line-height: 1.62; color: rgba(250, 243, 224, 0.86); }

/* Facts read as a quiet instrument panel: small line icon, gold caps label, plain value. */
/* A <ul> of label/value spans, NOT a <dl>: the icon + inner wrapper this layout needs are
   non-conforming inside dl > div (dt/dd must be its direct children), and a broken dl
   degrades to plain text in the accessibility tree anyway. A list loses nothing. */
.hero__facts { display: flex; flex-wrap: wrap; gap: 0; margin: clamp(26px, 3.2vh, 40px) 0 0; padding: 0; list-style: none; }
.hero__facts > li { display: flex; align-items: center; gap: 12px; padding: 0 clamp(16px, 1.7vw, 28px); border-left: 1px solid rgba(228, 196, 122, 0.26); }
.hero__facts > li:first-child { border-left: 0; padding-left: 0; }
.hero__facts svg { width: 30px; height: 30px; flex: 0 0 auto; color: rgba(233, 201, 128, 0.95); fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round; }
.hero__facts-label { display: block; margin: 0 0 3px; color: rgba(236, 199, 110, 0.82); font-size: 0.63rem; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; }
.hero__facts-val { display: block; margin: 0; color: rgba(251, 245, 228, 0.94); font-size: 0.93rem; font-weight: 500; line-height: 1.3; }

.hero__ctas { display: flex; flex-wrap: wrap; gap: 14px; margin: clamp(26px, 3.2vh, 40px) 0 0; }
/* In flow under the CTAs (left), not floating right — it used to collide with the CTA row. */
/* 0.46 measured 3.99:1 over the hero scrim, and it sits on a photograph whose local
   luminance varies — so the real-world worst case is below the measurement. 0.62 holds AA
   against the scrim's darkest and lightest regions alike. min-height 40 -> 44 to meet the
   touch-target floor; it is a real link, not decoration. */
.hero__scroll { display: inline-flex; align-items: center; gap: 12px; margin: clamp(22px, 2.6vh, 34px) 0 0; min-height: 44px; color: rgba(248, 239, 216, 0.62); text-decoration: none; font-size: 0.63rem; font-weight: 700; letter-spacing: 0.22em; text-transform: uppercase; }
.hero__scroll:hover { color: rgba(248, 239, 216, 0.85); }
.hero__scroll:focus-visible { outline: 2px solid var(--gold-warm); outline-offset: 4px; border-radius: 4px; }
.hero__scroll svg { width: 17px; height: 26px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; animation: bob 2.8s cubic-bezier(0.45, 0, 0.55, 1) infinite; }
@keyframes bob { 0%, 100% { transform: translateY(0); opacity: 0.65; } 50% { transform: translateY(5px); opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .hero__scroll svg { animation: none; } }
@media (max-width: 900px) { .hero__scroll { display: none; } }

/* ---------------- Hero cursor-follow glow (fine pointers only; toggled by town-guide.js) ----------------
   Ported from assets/location-motion.css (place-guide's .location-hero::after), adapted to this
   template's own .hero class and --gold-warm token. Optional: does nothing until town-guide.js's
   initHeroGlow() finds a .hero and a fine pointer and starts setting --mx/--my. */
.hero::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    opacity: 0;
    background: radial-gradient(280px 280px at var(--mx, 30%) var(--my, 60%), rgba(215, 178, 113, 0.18), transparent 70%);
    transition: opacity 0.5s ease;
    mix-blend-mode: screen;
}
.hero.glow-on::after { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .hero::after { transition: none; } }

/* ---------------- Scroll route thread (large desktop only; opt-in per page) ----------------
   A cartographic progress rail in the left gutter, ported from assets/location-motion.css. A page
   opts in with one element right after <body>:
       <div class="route-thread" aria-hidden="true"><span class="route-thread__fill"></span></div>
   town-guide.js's initRouteThread() is a no-op when that element is absent, so every town-village
   page that hasn't added it (Abersoch, Llangefni, Llanrwst) is unaffected. Two-tone: warm gold over
   this template's dark chapters, bronze over its one paper chapter (.band--cream), swapped by JS
   based on the chapter at the viewport centre — town-village's equivalent of place-guide's
   .chapter--paper hook. left uses this template's own --stage (1520px), not place-guide's 1180px. */
.route-thread {
    --thread-track: rgba(228, 196, 122, 0.22);
    --thread-fill: var(--gold-warm);
    --thread-glow: 0 0 12px rgba(217, 130, 59, 0.5);
    position: fixed;
    top: 0;
    bottom: 0;
    left: max(20px, calc((100vw - 1520px) / 2 - 26px));
    width: 2px;
    z-index: var(--z-raised);
    background: repeating-linear-gradient(180deg, var(--thread-track) 0 4px, transparent 4px 12px);
    pointer-events: none;
    display: none;
}
.route-thread.on-paper {
    --thread-track: rgba(158, 108, 36, 0.28);
    --thread-fill: var(--bronze);
    --thread-glow: 0 0 8px rgba(158, 108, 36, 0.22);
}
/* Hidden by default (display:none above) and revealed ONLY once .has-location-progress-rail lands
   on <html> — town-guide.js adds it after the rail is genuinely tracking scroll, never from static
   CSS, so an un-initialised rail (JS off/failed) never paints beside the native scrollbar. */
@media (min-width: 1320px) {
    .has-location-progress-rail .route-thread { display: block; }
}
.route-thread__fill {
    position: absolute;
    top: 0;
    left: -0.5px;
    width: 3px;
    height: 0;
    border-radius: 3px;
    background-color: var(--thread-fill);
    box-shadow: var(--thread-glow);
    transition: background-color 0.5s ease, box-shadow 0.5s ease;
}
@media (prefers-reduced-motion: reduce) { .route-thread__fill { transition: none; } }
/* The rail replaces the native scrollbar as the scroll-position affordance, so the browser's own
   is hidden — scoped to the rail's own breakpoint and to the root element only (no descendant
   selector), so nested scrollers (the orientation panel, dialogs, the practical tabs) keep theirs.
   Scrolling itself is untouched. */
@media (min-width: 1320px) {
    .has-location-progress-rail { scrollbar-width: none; }
    .has-location-progress-rail::-webkit-scrollbar { width: 0; height: 0; }
}
/* The same rail below the desktop breakpoint — ported from assets/location-motion.css alongside
   the 2026-08-03 rollout of the approved llanddwyn-island pilot. Same element, same JS, same gold
   fill and same gold/bronze swap; the only reason it was desktop-only was the gate above.
   Still class-gated, so JS off means no rail and no suppressed scrollbar at any width.

   The one deliberate difference from location-motion.css: the box is this template's own
   --gutter (clamp(20px, 4vw, 56px)) rather than place-guide's literal 42px, because the line
   must be centred inside the gutter the page actually has. That resolves to exactly 20px at
   phone widths, so the phone position matches the canonical page; at mid widths each template
   centres in its own inset, which is the parity that matters. */
@media (max-width: 1319px) {
    .has-location-progress-rail .route-thread {
        display: block;
        /* The rail element becomes the band's own left padding: a full-height, invisible box
           the line is centred inside. pointer-events:none is inherited by the fill, so the
           wider box stays clear of taps and the iOS back-swipe. */
        left: 0;
        width: var(--gutter);
        /* No dashed track on mobile — one solid gold line for the distance travelled. The
           desktop repeating-gradient track above is deliberately untouched. */
        background: none;
    }
    /* Centre in the rail rather than at a hand-picked offset, so a --gutter change cannot
       drift the line. Width, colour, glow, height and the gold/bronze swap are untouched. */
    .has-location-progress-rail .route-thread__fill { left: 50%; transform: translateX(-50%); }
    /* One scroll indicator, not two — same class gate and same root-only scoping as above, so
       the orientation panel, catalogue dialogs and practical tabs keep their own scrollbars. */
    .has-location-progress-rail { scrollbar-width: none; }
    .has-location-progress-rail::-webkit-scrollbar { width: 0; height: 0; }
}

/* CONTINUITY DEVICE 3 — a cream panel straddling the hero/snapshot seam, so chapter one
   reads as one composition instead of two stacked sections. */
/* Tucked into the bottom of the photograph (roughly two-thirds inside it) rather than
   hanging below as a separate floating slab. */
/* Same stage as the hero copy above and the snapshot plate below — it must not read as a
   wider element than the page it introduces. */
.guidestrip { position: relative; z-index: var(--z-raised); margin: calc(-1 * var(--strip-pull)) 0 0; padding: 0 var(--gutter); }
.guidestrip__inner {
    width: calc(var(--stage) * 0.855); margin: 0 auto;
    display: grid; grid-template-columns: minmax(170px, 0.72fr) repeat(4, minmax(0, 1fr));
    gap: clamp(10px, 1.1vw, 16px); align-items: stretch;
    padding: clamp(16px, 1.7vw, 22px);
    border-radius: var(--r-lg);
    background: linear-gradient(172deg, var(--paper-top), var(--paper));
    box-shadow: 0 18px 44px rgba(6, 14, 10, 0.34);
    color: var(--paper-ink);
}
.guidestrip__head { align-self: center; padding-right: 8px; }
.guidestrip__head p { margin: 0 0 6px; color: var(--bronze); font-size: 0.66rem; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; }
.guidestrip__head h2 { font-size: clamp(1.2rem, 1.5vw, 1.52rem); line-height: 1.16; color: var(--paper-heading); }
.guidestrip__head svg { width: 20px; height: 20px; margin-top: 14px; color: var(--bronze-soft); fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round; }
.gcard { display: flex; flex-direction: column; overflow: hidden; border-radius: var(--r-sm); background: rgba(255, 255, 255, 0.42); border: 1px solid rgba(118, 94, 50, 0.2); text-decoration: none; color: inherit; transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.3s cubic-bezier(0.22, 1, 0.36, 1), border-color 0.3s; }
.gcard:hover, .gcard:focus-visible { transform: translateY(-3px); box-shadow: 0 16px 30px rgba(60, 46, 20, 0.2); border-color: var(--bronze-soft); }
.gcard:focus-visible { outline: 2px solid var(--bronze); outline-offset: 2px; }
.gcard__img { aspect-ratio: 2.3 / 1; background: #cfc6ad; overflow: hidden; }
.gcard__img img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1); }
.gcard:hover .gcard__img img { transform: scale(1.05); }
.gcard__row { display: flex; flex: 1 1 auto; align-items: center; gap: 9px; min-height: 46px; padding: 10px 12px; }
.gcard__row b { flex: 1 1 auto; font-size: 0.92rem; font-weight: 600; color: var(--paper-heading); }
.gcard__no { flex: 0 0 auto; color: var(--bronze); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; font-variant-numeric: tabular-nums; }
.gcard__icon { width: 17px; height: 17px; flex: 0 0 auto; color: var(--bronze); fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round; }
.gcard__go { width: 15px; height: 15px; flex: 0 0 auto; color: var(--bronze); fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
@media (prefers-reduced-motion: reduce) { .gcard, .gcard__img img { transition: none; } .gcard:hover { transform: none; } .gcard:hover .gcard__img img { transform: none; } }

/* --- Place Snapshot: one bordered plate. Copy left, photograph occupying the right of the
   same plate and feathering into it, then facts and "known for" inside the same border, so
   the whole snapshot reads as a single object rather than three stacked rows. --- */
/* #snapshot opens chapter 01, so it takes --chapter-pad like every other chapter opening.
   This rule (not .band--chapter) is what actually applies here — both selectors have the same
   specificity and this one comes later, so it wins; pointing it at the same token keeps the
   four chapter openings on one source of truth. The old clamp(72px, 7vw, 116px) left ~101px
   of empty green between the guide strip above and the "01 The village" marker. */
.snapshot { position: relative; padding-top: var(--chapter-pad); }
.snapshot__card {
    --snap-bg: #16291e;
    --snap-pad: clamp(24px, 2.7vw, 42px);
    position: relative; overflow: hidden;
    border: 1px solid rgba(228, 196, 122, 0.2);
    border-radius: clamp(18px, 1.6vw, 26px);
    background: linear-gradient(178deg, #1a3126, var(--snap-bg) 62%, #142418);
    box-shadow: 0 30px 70px rgba(6, 14, 10, 0.4);
}
.snapshot__pad { padding: var(--snap-pad); padding-bottom: calc(var(--snap-pad) * 1.15); }
/* align-items: stretch (not center) so the copy column runs the full height of the
   photograph on desktop; the copy is a flex column and its "Why it matters" panel flexes to
   fill the remaining space, so the column bottom aligns cleanly with the image bottom instead
   of floating centred and short. Flexible by design — no hard-coded column height. On mobile
   .snapshot__top becomes a reverse flex column (below), where there is no height to fill and
   the copy keeps its natural content height. */
.snapshot__top { display: grid; grid-template-columns: minmax(0, 0.88fr) minmax(0, 1.12fr); align-items: stretch; }
/* No max-width: the column IS the measure. Capping it left a dead band between the copy and
   the point where the photograph became visible. */
/* No bottom padding on the desktop two-column layout: the "Why it matters" panel is the
   flex filler, so dropping it lands the panel's bottom edge exactly on the photograph's.
   Left/right padding is untouched — the panel keeps its inset from the plate edge and never
   runs under the photograph. Restored in the ≤1080px stacked layout below, where the copy
   sits above .snapshot__pad and needs the gap. */
.snapshot__copy { position: relative; z-index: 1; padding: var(--snap-pad) var(--snap-pad) 0; display: flex; flex-direction: column; }
/* Smaller than the page's section heading: inside the plate it has half the width to run in. */
.snapshot__copy h2 { font-size: clamp(2rem, 3.05vw, 2.7rem); }
.snapshot__copy p { margin: 18px 0 0; font-size: clamp(0.98rem, 1.06vw, 1.08rem); line-height: 1.64; color: rgba(248, 239, 216, 0.82); }
.snapshot__rule { width: 68px; height: 3px; margin: clamp(14px, 1.4vw, 20px) 0 0; background: var(--gold-warm); border: 0; opacity: 0.9; }
.snapshot__why { margin: clamp(20px, 2vw, 28px) 0 0; padding: clamp(16px, 1.6vw, 24px); border: 1px solid rgba(228, 196, 122, 0.26); border-radius: var(--r-md); background: rgba(228, 196, 122, 0.045); flex: 1 1 auto; display: flex; flex-direction: column; justify-content: center; }
.snapshot__why h3 { display: flex; align-items: center; gap: 10px; margin: 0 0 12px; font-family: var(--sans); font-size: 0.7rem; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: var(--gold-warm); }
.snapshot__why h3 svg { width: 22px; height: 22px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; }
.snapshot__why p { margin: 0; font-size: 0.94rem; line-height: 1.62; color: rgba(248, 239, 216, 0.78); }
/* The photograph fills its half of the plate to the top and right edges and dissolves into
   the card colour on the left, so there is no hard seam down the middle. */
.snapshot__media { position: relative; align-self: stretch; min-height: clamp(330px, 28vw, 470px); }
.snapshot__media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* A short feather, not a long one: it only has to soften the seam. A wide fade read as
   another 300px of empty plate before the photograph started. */
.snapshot__media::after {
    content: ""; position: absolute; inset: 0; pointer-events: none;
    background: linear-gradient(90deg, var(--snap-bg) 0%, rgba(22, 41, 30, 0.7) 7%, rgba(22, 41, 30, 0.28) 17%, transparent 32%);
}

.facts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: clamp(12px, 1.2vw, 18px); margin: 0; }
.facts > div { padding: clamp(16px, 1.5vw, 23px); border: 1px solid rgba(228, 196, 122, 0.2); border-radius: var(--r-md); background: rgba(255, 255, 255, 0.022); }
.facts__head { display: flex; align-items: center; gap: 15px; margin: 0 0 11px; }
.facts svg { width: 24px; height: 24px; flex: 0 0 auto; color: var(--gold-warm); fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round; }
.facts__ic--landscape { transform: translate(5%, -15%); }
.facts__label { color: rgba(236, 199, 110, 0.88); font-size: 0.66rem; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; }
.facts h3 { font-size: clamp(1.1rem, 1.28vw, 1.34rem); line-height: 1.2; color: var(--cream); }
.facts p { margin: 9px 0 0; font-size: 0.91rem; line-height: 1.56; color: rgba(248, 239, 216, 0.7); }

/* =====================================================================================
   CHAPTER 2 — DISCOVER: experiences -> orientation -> catalogue, read as one sequence.
   ===================================================================================== */
.secthead { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: clamp(24px, 4vw, 72px); align-items: end; margin: 0 0 clamp(26px, 2.8vw, 42px); }
.secthead__aside { padding-bottom: 6px; font-size: clamp(1.02rem, 1.14vw, 1.16rem); line-height: 1.66; color: rgba(248, 239, 216, 0.76); }
.band--cream .secthead__aside { color: var(--paper-soft); }

/* Section heading with no aside column — scoped modifier, NOT a change to the shared
   .secthead used by orient/catalogue/story/visit below. */
.secthead--solo { grid-template-columns: 1fr; }

/* Signature experiences — four tall photographic cards directly on the band. No wrapper
   panel: this is the single change that removes the "box inside a box" reading. */
.sig { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: clamp(14px, 1.5vw, 22px); }
.sigcard { position: relative; display: flex; flex-direction: column; justify-content: flex-end; min-height: clamp(420px, 46vw, 560px); padding: clamp(22px, 1.9vw, 30px); border-radius: var(--r-md); overflow: hidden; isolation: isolate; }
.sigcard img { position: absolute; inset: 0; z-index: -2; width: 100%; height: 100%; object-fit: cover; transition: transform 0.8s cubic-bezier(0.22, 1, 0.36, 1); }
.sigcard::after { content: ""; position: absolute; inset: 0; z-index: -1; background: linear-gradient(180deg, rgba(10, 22, 16, 0.15) 0%, rgba(10, 22, 16, 0.4) 42%, rgba(9, 20, 15, 0.93) 100%); }
.sigcard:hover img { transform: scale(1.045); }
.sigcard__n { position: absolute; top: clamp(18px, 1.6vw, 24px); left: clamp(22px, 1.9vw, 30px); font-family: var(--serif); font-size: 1.02rem; font-weight: 700; color: rgba(250, 243, 224, 0.6); letter-spacing: 0.06em; }
.sigcard h3 { font-size: clamp(1.34rem, 1.62vw, 1.68rem); line-height: 1.16; color: #fdf7e6; }
.sigcard p { margin: 12px 0 0; font-size: 1rem; line-height: 1.55; color: rgba(248, 239, 216, 0.8); }
.sigcard__time { display: inline-flex; align-items: center; gap: 8px; margin: 18px 0 0; padding: 8px 15px; align-self: flex-start; border: 1px solid rgba(228, 196, 122, 0.4); border-radius: 999px; color: var(--gold-bright); font-size: 0.82rem; font-weight: 600; }
.sigcard__time svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
@media (prefers-reduced-motion: reduce) { .sigcard img { transition: none; } .sigcard:hover img { transform: none; } }

/* Interactive moments — the whole card is one control. JS injects a stretched trigger + a
   "View details" cue that open the SAME catalogue detail dialog (#catdlg) for the card's
   place (data-cat-ref), so there is no second content source. Everything below is gated on
   the injected trigger, so with JS off nothing advertises an interaction that cannot happen. */
.sigcard__trigger { position: absolute; inset: 0; z-index: 3; appearance: none; -webkit-appearance: none; margin: 0; padding: 0; border: 0; background: transparent; cursor: pointer; border-radius: var(--r-md); }
/* Card-shaped focus ring, drawn inset so overflow:hidden never clips it. */
.sigcard__trigger:focus-visible { outline: 2px solid var(--gold-bright); outline-offset: -3px; }
.sigcard__cue { display: none; align-items: center; gap: 7px; margin: 16px 0 0; color: var(--gold-bright); font-family: var(--sans); font-size: 0.76rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; }
html.js .sigcard:has(.sigcard__trigger) .sigcard__cue { display: inline-flex; }
.sigcard__cue svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1); }
.sigcard:has(.sigcard__trigger):hover .sigcard__cue svg, .sigcard:has(.sigcard__trigger:focus-visible) .sigcard__cue svg { transform: translateX(4px); }
@media (prefers-reduced-motion: reduce) { .sigcard__cue svg { transition: none; } }

/* Soft editorial bridge into the catalogue below — no button, no CTA framing. A quiet,
   italic aside (same restrained treatment as .lifecat__cap / .credits__note elsewhere in
   this system) plus a small bobbing down-arrow, centred, with generous breathing room
   either side. Originated as a Beaumaris-local refinement while each master-template file
   still carried its own inline stylesheet; as of the Phase 2 extraction it is part of the
   SHARED layer, so it is now the canonical .sigtip for every town/village page. Betws-y-Coed
   still renders its own older inline copy until it is converted. */
.sigtip { display: flex; flex-direction: column; align-items: center; gap: 14px; max-width: 480px; margin: clamp(48px, 5vw, 72px) auto 0; text-align: center; }
.sigtip__lead { margin: 0; font-size: 0.98rem; font-style: italic; line-height: 1.6; color: rgba(248, 239, 216, 0.6); }
.sigtip__cue { display: inline-flex; color: rgba(228, 196, 122, 0.5); animation: sigtip-bob 2.6s ease-in-out infinite; }
.sigtip__cue svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
@keyframes sigtip-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(5px); } }
@media (prefers-reduced-motion: reduce) { .sigtip__cue { animation: none; } }

/* --- Orientation: given real presence. Wider, taller, no enclosing panel. --- */
.orientband { padding-top: clamp(36px, 3.4vw, 54px); }
/* The map sets the row height; the panel stretches to match it, which is what
   aligns their top AND bottom edges. The panel's index is the only flexible
   part (flex:1 + its own scroll), so 4 areas leave it short of the map's
   bottom and 10 areas scroll inside it — neither case changes section height. */
/* --- Count-based layout mode -------------------------------------------------------
   The number of orientation rows drives map height and index density, via CSS
   quantity queries (`:has(.orient-card:nth-child(N):last-child)` = "exactly N rows").
   Reading the count in CSS rather than tagging it with JS keeps the whole system
   working with no JS at all, which the index markup already relies on, and means a
   future place changes ONLY by having a different number of rows in its HTML.
   Each mode sets custom properties; the rules further down are the only consumers,
   so adding a mode is one line, not a parallel set of selectors.
   The defaults below are the 6-8 band, used if a page ever falls outside 4-10. */
.orient { --orient-map-h: 660px; --orient-row-py: 10px; --orient-row-gap: 6px; }
/* 4-5: slightly taller rows */
.orient:has(.orient-card:nth-child(4):last-child)  { --orient-map-h: 580px; --orient-row-py: 13px; --orient-row-gap: 7px; }
.orient:has(.orient-card:nth-child(5):last-child)  { --orient-map-h: 600px; --orient-row-py: 13px; --orient-row-gap: 7px; }
/* 6-7: standard compact rows */
.orient:has(.orient-card:nth-child(6):last-child)  { --orient-map-h: 640px; --orient-row-py: 10px; --orient-row-gap: 6px; }
.orient:has(.orient-card:nth-child(7):last-child)  { --orient-map-h: 660px; --orient-row-py: 10px; --orient-row-gap: 6px; }
/* 8-10: denser — reduced VERTICAL PADDING only; the title keeps its size and its
   two-line wrap, so density never costs readability. */
.orient:has(.orient-card:nth-child(8):last-child)  { --orient-map-h: 720px; --orient-row-py: 5px; --orient-row-gap: 4px; }
.orient:has(.orient-card:nth-child(9):last-child)  { --orient-map-h: 730px; --orient-row-py: 5px; --orient-row-gap: 4px; }
.orient:has(.orient-card:nth-child(10):last-child) { --orient-map-h: 740px; --orient-row-py: 5px; --orient-row-gap: 4px; }

/* align-items:stretch, with the PANEL driving the row height and the MAP stretching to
   meet it. The panel is sized by its own content (fixed feature card + its rows), so
   nothing inside it is ever stretched to fill — but the map, which has no such
   constraint, grows to match, and the bottoms align exactly at every viewport width
   and every area count. A hardcoded map height could only line up at one width, since
   the panel's height moves with text wrapping.
   --orient-map-h is therefore a FLOOR, not a target: it only matters if a page's panel
   content is ever shorter than the map should be. */
.orient { display: grid; grid-template-columns: minmax(0, 1.78fr) minmax(380px, 1fr); gap: clamp(16px, 1.8vw, 26px); align-items: stretch; margin: clamp(30px, 3.2vw, 44px) 0 0; }
/* min-width:0 — a grid item defaults to min-width:auto, which lets it be pushed wider
   than its column by its own content. That is what turned the mobile horizontal
   selector into a page-widening row instead of a scrolling one at high area counts. */
/* max-height caps the whole layout: the panel drives the row height under
   align-items:stretch, so bounding the panel bounds the map too, and the bottoms stay
   aligned. Below the cap nothing changes; above it the index scrolls (see
   .orient-cards) instead of the section growing without limit. Row padding is tightened
   first, at 8-10 areas, so the cap is only reached by genuinely long indexes. */
.orient-panel { display: flex; flex-direction: column; gap: 10px; min-height: 0; min-width: 0; max-height: 760px; padding: 12px; border: 1px solid var(--green-rule); border-radius: var(--r-md); background: linear-gradient(180deg, rgba(33, 69, 52, 0.5), rgba(24, 56, 43, 0.5)); }
.orient-map { position: relative; overflow: hidden; min-height: var(--orient-map-h); border: 1px solid var(--green-rule); border-radius: var(--r-md); background: #0a1c15; box-shadow: 0 28px 64px rgba(6, 13, 9, 0.5); }
.orient-map__canvas { position: absolute; inset: 0; }
.orient-map__canvas .mapboxgl-ctrl-top-right { top: 12px; right: 12px; }
.orient-map__canvas .mapboxgl-ctrl-group { background: rgba(10, 28, 21, 0.86); border: 1px solid rgba(228, 196, 122, 0.24); }
.orient-map__canvas .mapboxgl-ctrl-group button { width: 37px; height: 37px; }
.orient-map__canvas .mapboxgl-ctrl-group button + button { border-top: 1px solid rgba(228, 196, 122, 0.16); }
.orient-map__canvas .mapboxgl-ctrl-icon { filter: invert(1) brightness(1.6); }
.orient-map__canvas .mapboxgl-ctrl-attrib { background: rgba(6, 15, 11, 0.7); font-size: 0.7rem; }
.orient-map__canvas .mapboxgl-ctrl-attrib a { color: rgba(248, 239, 216, 0.72); }
.orient-map__hint { position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 2; min-height: 40px; padding: 0 18px; border: 1px solid rgba(228, 196, 122, 0.4); border-radius: 999px; background: rgba(10, 28, 21, 0.9); color: rgba(248, 239, 216, 0.9); font: inherit; font-size: 0.86rem; cursor: pointer; transition: opacity 0.25s; }
.orient-map__hint.is-hidden { opacity: 0; pointer-events: none; }
.orient-map__controls { position: absolute; left: 12px; top: 12px; z-index: 2; display: flex; gap: 8px; }
.orient-map__reset { display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 44px; padding: 0 15px 0 13px; border-radius: 999px; border: 1px solid rgba(228, 196, 122, 0.4); background: rgba(10, 28, 21, 0.88); color: rgba(248, 239, 216, 0.92); font: inherit; font-size: 0.84rem; font-weight: 600; cursor: pointer; }
.orient-map__reset svg { width: 16px; height: 16px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.orient-map__reset:hover, .orient-map__reset:focus-visible { border-color: rgba(244, 216, 161, 0.8); color: var(--gold-warm); outline: none; }

/* NEVER set position on .orient-pin: Mapbox GL requires this element to stay
   position:absolute to compute its translate(lng,lat) transform.

   The explicit z-index:1 exists for the optional parking layer (see the
   .orient-park block below). Parking markers are created on first enable, i.e.
   AFTER these numbered pins, so they come later in the marker container's DOM
   order; with z-index:auto here a parking marker painted OVER an overlapping
   attraction pin and SWALLOWED its tap. Giving every pin the same explicit 1
   restores the intended hierarchy without changing how the pins relate to EACH
   OTHER — ties still break on DOM order exactly as before, and .is-active still
   wins at var(--z-raised). Inert on pages with no parking layer. */
.orient-pin { display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 5px; min-width: 44px; min-height: 44px; padding: 2px; border: 0; background: none; cursor: pointer; z-index: 1; }
.orient-pin__label { display: none; padding: 4px 11px; border-radius: 999px; white-space: nowrap; background: rgba(8, 20, 15, 0.95); border: 1px solid var(--gold-warm); color: var(--gold-bright); font-size: 0.76rem; font-weight: 700; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45); }
/* Numeral centring (rolled out from the Llandudno pilot, 2026-08-07). Playfair Display
   defaults to OLD-STYLE figures: 3/4/5/7/9 descend below the baseline, 6/8 ascend, 1/2 sit
   at x-height. Flex centring then centres each digit's own uneven ink, so the badges
   disagreed with each other by ~2.3px and several read as sitting low.
     lining-nums  - one uniform cap-height shape for every digit (spread 2.5px -> 0.5px)
     line-height:1 - removes the remaining ~1.0px drop from the font's tall `normal` line box
     font-size    - 0.84rem -> 0.78rem: lining figures are cap-height so they render larger
                    than the old-style ones the original size was set against
   Both of the first two are load-bearing; lining-nums alone still sits 1.0px low. A
   padding-bottom nudge is NOT needed and over-corrects - do not reintroduce one.
   Measured residual +-0.7px at 1440/390/360. Badge box stays 28px (36px active). */
.orient-pin__badge { display: flex; align-items: center; justify-content: center; position: relative; width: 28px; height: 28px; border-radius: 50%; background: rgba(8, 20, 15, 0.92); border: 1.5px solid rgba(228, 196, 122, 0.45); color: rgba(248, 239, 216, 0.92); font-family: var(--serif); font-weight: 700; font-size: 0.78rem; line-height: 1; font-variant-numeric: lining-nums; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45); transition: width 0.18s, height 0.18s, background 0.18s, border-color 0.18s, color 0.18s, box-shadow 0.18s; }
.orient-pin:hover .orient-pin__badge, .orient-pin:focus-visible .orient-pin__badge { border-color: var(--gold-warm); color: var(--gold-bright); }
.orient-pin:focus-visible { outline: 2px solid var(--gold-warm); outline-offset: 2px; border-radius: 50%; }
.orient-pin.is-active { z-index: var(--z-raised); }
.orient-pin.is-active .orient-pin__badge { width: 36px; height: 36px; background: var(--gold-warm); border-color: var(--gold-bright); color: #20372c; font-size: 0.90rem; box-shadow: 0 0 0 6px rgba(228, 196, 122, 0.24), 0 4px 14px rgba(0, 0, 0, 0.5); }
.orient-pin.is-active .orient-pin__label { display: inline-flex; }
.orient-pin--directional .orient-pin__badge::after { content: "\2197"; position: absolute; top: -7px; right: -7px; width: 15px; height: 15px; border-radius: 50%; background: rgba(8, 20, 15, 0.95); border: 1px solid rgba(228, 196, 122, 0.5); color: var(--gold-warm); font-size: 0.58rem; line-height: 1; display: flex; align-items: center; justify-content: center; }
.orient-pin--directional.is-active .orient-pin__badge::before { content: ""; position: absolute; left: 50%; bottom: 100%; width: 0; height: 34px; border-left: 2px dashed rgba(228, 196, 122, 0.55); transform-origin: bottom center; transform: translateX(-50%) rotate(-26deg); pointer-events: none; }

/* ==========================================================================
   OPTIONAL PARKING LAYER  ·  production pilot: Conwy (2026-08-07)

   Every selector below is new and additive. A town whose orientation JSON has
   no "parking" array never renders any of it — the toggle stays hidden, no
   marker is built, and the attraction markers, controls row, feature panel and
   catalogue are untouched.

   COLOUR DISCIPLINE: gold (--gold-warm / --gold-bright) is the ATTRACTION
   language and is deliberately not used here. Parking reads in road-sign blue
   so it is legible but plainly secondary — that separation is what stops the
   map becoming noisy when the layer is on.
   ========================================================================== */

/* ---- the toggle, sitting in the existing controls row --------------------
   .orient-map__controls is already display:flex; gap:8px, so Reset and Parking
   stay on one horizontal line at every width with no layout CSS here. The pill
   geometry mirrors .orient-map__reset so the pair reads as one control group.

   OFF is a LIGHT pill carrying a real blue road-sign chip: the control has to
   advertise the layer before anyone presses it. A dark pill among dark pills
   reads as another map button, whereas a cream pill with a blue P reads as
   "there is parking here". It must still not read as SELECTED — hence light,
   not blue, until pressed. */
.orient-map__parking { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 15px 0 11px; border-radius: 999px; border: 1px solid rgba(15, 76, 155, 0.3); background: rgba(246, 249, 246, 0.95); color: #16281f; font: inherit; font-size: 0.84rem; font-weight: 600; cursor: pointer; transition: background 0.18s, border-color 0.18s, color 0.18s; }
/* The sign chip — same blue plate as the map markers, so the control and what
   it summons are visibly the same thing. */
.orient-map__parking-sign { display: grid; place-items: center; flex: 0 0 auto; width: 20px; height: 20px; border-radius: 6px; background: #0f4c9b; transition: background 0.18s; }
.orient-map__parking-sign svg { width: 13px; height: 13px; fill: none; stroke: #fff; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.orient-map__parking:hover { background: #fff; border-color: rgba(15, 76, 155, 0.55); }
.orient-map__parking:focus-visible { outline: 2px solid #0f4c9b; outline-offset: 2px; }
/* ON: the whole pill takes the sign blue. Unmistakable, and it borrows no gold,
   so an enabled parking layer still never competes with the attractions. */
.orient-map__parking[aria-pressed="true"] { background: #0f4c9b; border-color: #0f4c9b; color: #fff; }
.orient-map__parking[aria-pressed="true"] .orient-map__parking-sign { background: rgba(255, 255, 255, 0.2); }
.orient-map__parking[aria-pressed="true"]:hover { background: #1560bd; border-color: #1560bd; }
.orient-map__parking[aria-pressed="true"]:focus-visible { outline-color: #fff; }

/* ---- parking markers -----------------------------------------------------
   NEVER set position on this element: Mapbox GL owns it and requires
   position:absolute to apply its translate(lng,lat) transform. z-index is safe
   (the element is already positioned) and is what keeps parking beneath both an
   active attraction pin (var(--z-raised)) and an inactive one (z-index:1). */
.orient-park { display: grid; place-items: start center; width: 44px; height: 44px; padding: 0; border: 0; background: none; cursor: pointer; z-index: 0; }
/* Road-sign blue, so the badge reads as PARKING / infrastructure before it reads
   as an Atlas marker. Deliberately NOT a map-pin teardrop: a flat rounded-square
   plate, so the silhouette differs from the attraction circle at a glance and
   not only in colour. #0f4c9b against white is ~7.6:1 — comfortably AA for the
   glyph — and blue stays clear of the attraction gold. */
.orient-park__badge { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 7px; background: #0f4c9b; border: 1.5px solid rgba(255, 255, 255, 0.62); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); transition: background 0.18s, border-color 0.18s; }
.orient-park__badge svg { width: 15px; height: 15px; fill: none; stroke: #fff; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.orient-park:hover .orient-park__badge { background: #1560bd; border-color: #fff; }
.orient-park:focus-visible { outline: 2px solid #fff; outline-offset: 0; border-radius: 10px; }

/* ---- the popup -----------------------------------------------------------
   Reskins the shared Mapbox popup only inside .orient-park-popup, so no other
   popup anywhere on the site is affected.
   z-index:6 because giving .orient-pin an explicit z-index above would
   otherwise put the numbered pins ABOVE the popup (which has none of its own),
   showing the map straight through the open card's text. */
.orient-park-popup { z-index: 6; }
/* A small Atlas map card, not a Mapbox popup. Smoked Atlas green, a thin warm
   rule rather than a cool grey one, a softer shadow and a modest radius. Blue
   appears ONLY as the small sign chip — a blue panel would shout, and the card
   is meant to sit quietly on top of the map, not replace it. Translucency
   without backdrop blur: the alpha reads as depth, and skipping the blur keeps
   the card cheap to paint over a moving map. */
.orient-park-popup .mapboxgl-popup-content { padding: 11px 12px 12px; border-radius: 10px; border: 1px solid rgba(228, 196, 122, 0.22); background: rgba(9, 22, 17, 0.94); color: rgba(240, 247, 243, 0.92); box-shadow: 0 8px 22px rgba(0, 0, 0, 0.42); }
/* The tip is a zero-size element drawn from its borders, and Mapbox colours only
   the ONE side facing the card, leaving the others transparent. Colouring all
   four (as the prototype did) makes those transparent sides opaque and the
   triangle renders as a small dark SQUARE floating off the popup — visible at
   390px in rendered QA. So recolour per anchor, exactly as Mapbox's own CSS
   does, and keep every other side transparent. */
.orient-park-popup .mapboxgl-popup-tip { border-color: transparent; }
.orient-park-popup.mapboxgl-popup-anchor-top .mapboxgl-popup-tip,
.orient-park-popup.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip,
.orient-park-popup.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip { border-bottom-color: rgba(9, 22, 17, 0.94); }
.orient-park-popup.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip,
.orient-park-popup.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip,
.orient-park-popup.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip { border-top-color: rgba(9, 22, 17, 0.94); }
.orient-park-popup.mapboxgl-popup-anchor-left .mapboxgl-popup-tip { border-right-color: rgba(9, 22, 17, 0.94); }
.orient-park-popup.mapboxgl-popup-anchor-right .mapboxgl-popup-tip { border-left-color: rgba(9, 22, 17, 0.94); }
/* Quieter close: still a 30px target, but visually a small mark rather than a
   button competing with the card's own action. */
.orient-park-popup .mapboxgl-popup-close-button { width: 30px; height: 30px; padding: 0; color: rgba(226, 238, 231, 0.5); font-size: 0.95rem; line-height: 1; background: none; border-radius: 7px; }
.orient-park-popup .mapboxgl-popup-close-button:hover, .orient-park-popup .mapboxgl-popup-close-button:focus-visible { color: #fff; background: rgba(255, 255, 255, 0.08); outline: none; }
/* Title: the blue sign chip is the only accent, set INLINE so a wrapped second
   line returns to the card's left edge instead of hanging off the chip. */
.orient-park-pop__sign { display: inline-grid; place-items: center; width: 17px; height: 17px; margin-right: 6px; vertical-align: -3px; border-radius: 5px; background: #0f4c9b; }
.orient-park-pop__sign svg { width: 11px; height: 11px; fill: none; stroke: #fff; stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; }
.orient-park-pop__name { margin: 0 16px 3px 0; font-family: var(--serif); font-size: 0.94rem; font-weight: 700; line-height: 1.26; color: rgba(246, 251, 248, 0.96); overflow-wrap: break-word; }
.orient-park-pop__type { margin: 0; font-size: 0.75rem; line-height: 1.3; color: rgba(214, 230, 221, 0.66); }
/* Map-style context: a short located phrase, not a sentence. */
.orient-park-pop__context { margin: 6px 0 0; font-size: 0.75rem; line-height: 1.38; color: rgba(236, 245, 240, 0.84); }
/* A compact link, not a CTA block — but still a 36px-tall target. */
.orient-park-pop__cta { display: inline-flex; align-items: center; gap: 5px; margin-top: 9px; min-height: 36px; padding: 0 11px; border-radius: 8px; border: 1px solid rgba(228, 238, 232, 0.3); color: rgba(240, 247, 243, 0.94); font-size: 0.76rem; font-weight: 600; text-decoration: none; white-space: nowrap; }
.orient-park-pop__cta:hover, .orient-park-pop__cta:focus-visible { border-color: rgba(228, 196, 122, 0.75); color: #fff; background: rgba(255, 255, 255, 0.07); outline: none; }
@media (min-width: 761px) {
    .orient-park-popup .mapboxgl-popup-content { padding: 12px 13px 13px; }
    .orient-park-pop__name { font-size: 1rem; }
    .orient-park-pop__type, .orient-park-pop__context { font-size: 0.78rem; }
}

/* ---- Practical -> Parking list ------------------------------------------
   Sits inside the existing .ptab-panel__right column, using the panel's own
   paper tokens, bronze links and hairline rule — the same rule .pfact and
   .ptab-panel__link already draw with. Deliberately NOT built from .pfact
   boxes: five bordered cards for five car parks is exactly the heavy,
   bolted-on look this section should avoid, and .pfact__label is an uppercase
   field label, wrong for a proper name. A hairline-separated list scans in a
   couple of seconds and stays quiet.
   --bronze on --paper measures 4.90:1, so the links clear AA. */
.pcarparks { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
.pcarpark { display: flex; flex-wrap: wrap; align-items: baseline; gap: 1px 12px; padding: 11px 0; border-top: 1px solid rgba(118, 94, 50, 0.16); }
.pcarpark:first-child { padding-top: 2px; border-top: 0; }
.pcarpark:last-child { padding-bottom: 2px; }
.pcarpark__name { flex: 1 1 100%; margin: 0; font-size: 0.94rem; font-weight: 700; line-height: 1.3; color: var(--paper-heading); }
.pcarpark__where { flex: 1 1 auto; min-width: 0; margin: 0; font-size: 0.83rem; line-height: 1.4; color: var(--paper-mute); }
/* A small text action, not a third CTA button — but still a 32px target. */
.pcarpark__link { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 4px; min-height: 32px; font-size: 0.81rem; font-weight: 600; color: var(--bronze); text-decoration: none; white-space: nowrap; }
.pcarpark__link:hover { text-decoration: underline; }
.pcarpark__link:focus-visible { outline: 2px solid var(--bronze); outline-offset: 2px; border-radius: 4px; }
/* The quiet "See them on the map" tie-back, sitting under the list rather than
   inside a row — same text-action treatment, separated by the list's own rule. */
.pcarparks__map { margin-top: 10px; padding-top: 11px; border-top: 1px solid rgba(118, 94, 50, 0.16); }
/* Below the panel's own stacking breakpoint the right column loses its rule and
   sits under the left one; the list needs no other change. */
@media (max-width: 900px) { .pcarpark { padding: 10px 0; } }

/* Screen-reader-only: the "(opens in a new tab)" suffix and the toggle's
   role="status" announcement. */
.orient-park-pop__sr, .orient-park-sr, .pcarpark__sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }

@media (prefers-reduced-motion: reduce) {
    .orient-map__parking, .orient-park__badge { transition: none; }
}

.orient-map__fallback { display: none; position: absolute; inset: 0; padding: clamp(20px, 3vw, 34px); background: linear-gradient(160deg, #0a1c15 0%, #06120e 100%); }
.orient--fallback .orient-map__canvas, .orient--fallback .orient-map__controls, .orient--fallback .orient-map__hint { display: none; }
.orient--fallback .orient-map__fallback, html.no-js .orient-map__fallback { display: block; }
html.no-js .orient-map__canvas, html.no-js .orient-map__controls, html.no-js .orient-map__hint { display: none; }
.orient-fb__frame { position: relative; width: 100%; height: 100%; min-height: 300px; }
.orient-fb__dot { position: absolute; transform: translate(-50%, -50%); display: flex; flex-direction: column; align-items: center; gap: 7px; }
.orient-fb__dot i { display: block; width: 12px; height: 12px; border-radius: 50%; background: var(--gold-warm); box-shadow: 0 0 0 5px rgba(228, 196, 122, 0.26); }
.orient-fb__dot span { padding: 3px 10px; border-radius: 999px; background: rgba(10, 28, 21, 0.9); border: 1px solid rgba(228, 196, 122, 0.3); color: rgba(248, 239, 216, 0.9); font-size: 0.72rem; font-weight: 700; white-space: nowrap; }
/* 0.55 measured 4.00:1. This caption only renders in the no-JS / map-failed fallback —
   which is exactly when it is load-bearing, so it is the last thing that should be faint. */
.orient-fb__caption { position: absolute; left: 50%; bottom: 10px; transform: translateX(-50%); color: rgba(228, 196, 122, 0.72); font-size: 0.74rem; font-style: italic; }

/* Active-area feature — the section's one large image, never repeated per row. */
/* The feature absorbs the panel's spare height, so the count of areas changes the
   SPLIT inside the panel rather than the section's height: at 4 areas the image
   grows into the room the short index leaves, at 10 the index takes that room back
   and the feature settles to its 4/3 minimum. Without this the low-count case left
   a large empty void under the last row. */
/* Vertical: full-width landscape image, then the text underneath it — never text over
   the image. flex:0 0 auto plus a fixed media height means neither the card nor the
   image can ever be stretched to fill spare panel height. */
.orient-feature { display: flex; flex-direction: column; gap: 10px; flex: 0 0 auto; padding: 12px; border: 1px solid var(--green-rule); border-radius: var(--r-sm); background: rgba(8, 20, 15, 0.42); }
/* No aspect-ratio and no height here: a grid item already stretches to the row, so
   the column sets the width and the panel's spare height sets the height. Pairing
   aspect-ratio WITH a stretched height makes the ratio drive WIDTH instead, which
   overflowed the image across the text — min-height is the safe way to keep it from
   collapsing when the index is long. */
/* A stable landscape frame: full panel width at a fixed height, with the image fitted
   INSIDE it. Never the other way round — sizing the frame from the image is what let a
   portrait source stretch the card. Fixed height also means the feature is exactly the
   same size at 4 areas as at 10. */
.orient-feature__media { position: relative; margin: 0; height: 230px; flex: 0 0 auto; border-radius: var(--r-sm); overflow: hidden; background: #0a1c15; border: 1px solid rgba(228, 196, 122, 0.22); }
/* object-fit/-position are set inline per area by orientation-map.js from
   data-orient-fit / data-orient-pos; these are the defaults for the no-JS state. */
.orient-feature__media img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; display: block; }
/* contain: the image is shown whole against the dark green, letterboxed rather than
   cropped. For sources a cover crop would ruin (tall portraits, wide panoramas). */
.orient-feature__media.is-contain { background: var(--green-card); }
.orient-feature__media.is-contain img { object-fit: contain; }
/* Deliberate branded empty state — used instead of borrowing an unrelated photograph
   when an area has no verified image of its own. */
.orient-feature__noimg { display: none; }
.orient-feature--noimage .orient-feature__media { background: var(--green-card); border-style: dashed; }
.orient-feature--noimage .orient-feature__media img { display: none; }
.orient-feature--noimage .orient-feature__noimg { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 9px; padding: 12px; text-align: center; }
.orient-feature__noimg svg { width: 30px; height: 30px; fill: none; stroke: var(--gold-warm); stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; opacity: 0.75; }
.orient-feature__noimg span { color: rgba(228, 196, 122, 0.72); font-size: 0.76rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; }
/* min-height reserves room for the longest area's text (title + 3 description lines +
   chips) so the card is the same height whichever area is selected — otherwise the
   index rows below it shift as you click through. Reserving it on the BODY rather than
   on the description puts any slack BELOW the chips, instead of opening a gap between
   the description and them. */
/* Content-driven: the body grows with whatever it holds. min-height is a generic
   floor in rem (so it tracks the type scale), NOT a height fitted to the current
   entries — a place with one-line descriptions keeps a sensible card, and a place with
   longer ones simply grows. The per-element line limits below are what stop unusually
   long content distorting the panel. */
.orient-feature__body { min-width: 0; min-height: 5.5rem; display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
.orient-feature__head { display: flex; align-items: center; gap: 10px; margin: 0; }
.orient-feature__num { flex: 0 0 auto; width: 34px; height: 34px; display: grid; place-items: center; border-radius: 50%; background: var(--gold-warm); color: #20372c; font-family: var(--serif); font-weight: 700; font-size: 1rem; }
/* Two lines maximum — a long place name wraps once, then clips rather than pushing
   the description and chips down the panel. */
.orient-feature__title { font-family: var(--serif); font-weight: 700; font-size: clamp(1.1rem, 1.2vw, 1.32rem); line-height: 1.15; color: var(--cream); display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; }
/* The description always occupies its full three-line allowance: clamped at 3 so long
   content cannot grow it, and min-height 4.5em (3 lines x 1.5 line-height, em being its
   OWN font-size) so short content cannot shrink it. That keeps the card — and therefore
   the panel and the map that stretches to meet it — exactly the same height whichever
   area is selected. Without the floor, selecting a one-line area after a two-line one
   visibly resized the whole section.
   This is a rule (reserve the maximum the clamp allows), expressed in the element's own
   line metrics — not a pixel height measured from any particular place's copy, so it
   holds for any dataset and any type scale.
   THE CLAMP IS A SAFETY NET, NOT A COPY BUDGET TO SPEND. It is the one permitted clamp on
   the page (town-village-contract.md §13c-ii) because it holds the panel — and therefore the
   map — at a constant height; but no page's copy may actually REACH it. A data-orient-desc
   that renders past 3 lines is failing the copy rule, not the layout rule: shorten it.
   Measured 2026-07-27, before that rule existed: 72/155 anchor descriptions clipped at 390px
   across 21/26 pages (9/155 even at 1440px). Those are copy defects to fix in the county
   migrations, not a reason to raise the clamp. */
.orient-feature__desc { margin: 0; color: rgba(248, 239, 216, 0.82); font-size: 0.93rem; line-height: 1.5; min-height: 4.5em; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden; }
/* flex-wrap + a real gap: several chips, or long category names, flow onto further
   lines instead of overlapping or overflowing the card. */
.orient-feature__tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; width: 100%; }
.orient-tag { padding: 3px 10px; border-radius: 999px; border: 1px solid rgba(228, 196, 122, 0.32); color: rgba(248, 239, 216, 0.82); font-size: 0.74rem; font-weight: 600; letter-spacing: 0.02em; }

/* Compact index — one row per area, no per-row thumbnail. */
/* ONE full-width column at every desktop count. Rows are selectors, not cards: they
   size to their own content and only their vertical padding changes with the count
   (--orient-row-py), so density adapts without any row ever being stretched to fill. */
.orient-cards { flex: 0 1 auto; min-width: 0; min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: flex; flex-direction: column; gap: var(--orient-row-gap); }
.orient-cards::-webkit-scrollbar { width: 6px; height: 6px; }
.orient-cards::-webkit-scrollbar-thumb { background: rgba(228, 196, 122, 0.28); border-radius: 999px; }
/* number | title | category | chevron, on one full-width line. The title column is the
   only flexible one, so it takes all the room the others don't need. */
.orient-card { display: grid; grid-template-columns: 26px minmax(0, 1fr) auto 16px; align-items: center; gap: 11px; text-align: left; padding: var(--orient-row-py) 12px; border: 1px solid rgba(228, 196, 122, 0.14); border-radius: var(--r-sm); cursor: pointer; background: rgba(8, 20, 15, 0.28); color: var(--cream); font: inherit; transition: background 0.18s, border-color 0.18s, color 0.18s; }
.orient-card:hover { background: rgba(228, 196, 122, 0.09); }
.orient-card:focus-visible { outline: 2px solid var(--gold-warm); outline-offset: -2px; }
.orient-card.is-active { background: linear-gradient(180deg, var(--paper-top), var(--paper-warm)); border-color: var(--gold-warm); color: var(--paper-ink); box-shadow: 0 6px 18px rgba(6, 13, 9, 0.4); }
.orient-card__num { flex: 0 0 auto; width: 26px; height: 26px; display: grid; place-items: center; border-radius: 50%; border: 1px solid rgba(228, 196, 122, 0.4); color: rgba(248, 239, 216, 0.9); font-family: var(--serif); font-size: 0.84rem; }
.orient-card.is-active .orient-card__num { background: var(--gold-warm); border-color: var(--gold-bright); color: #20372c; }
/* Names wrap to a second line rather than being cut with an ellipsis — a truncated
   place name is not a usable index entry. Clamped at 2 lines so a row stays compact. */
.orient-card__label { min-width: 0; font-size: 0.95rem; font-weight: 600; line-height: 1.25; white-space: normal; overflow-wrap: break-word; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; }
/* 0.55 computed ~4.7:1 on the resting row but dipped to ~3.8:1 over the gold hover tint —
   0.66 holds AA in both states. */
.orient-card__cat { color: rgba(248, 239, 216, 0.66); font-size: 0.78rem; white-space: nowrap; }
.orient-card.is-active .orient-card__cat { color: var(--paper-mute); }
.orient-card__chev { display: grid; place-items: center; color: rgba(248, 239, 216, 0.42); }
.orient-card.is-active .orient-card__chev { color: var(--bronze); }
.orient-card__chev svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
@media (prefers-reduced-motion: reduce) { .orient-map__hint, .orient-pin__badge, .orient-card { transition: none; } }

/* --- Things to see and do: CREAM band. The page's first big light surface. --- */
/* Visually-hidden text that stays in the accessibility tree — used for the catalogue
   trigger's "View details" label, so no card title is ever duplicated into an aria-label. */
.vh { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }

/* Catalogue-only stage. Scoped to #catalogue so no other band moves: a slightly wider
   measure below ~1400px, and a 1360px ceiling above it so four columns never stretch into
   over-wide cards. The gutter has to come in with it — .band's own padding is what actually
   caps the stage, so widening --stage alone would just overflow the band.
   `min(var(--stage), 100%)` is the guard for exactly that: 100% is the band's content box,
   so the stage can never exceed it at any viewport, whatever the two clamps resolve to. */
/* Gated at 1281px, which is where .catgrid actually becomes four columns — not 1081px.
   Applied any lower it would widen the TWO-column tablet layout instead, stretching cards
   to ~600px, the opposite of the intent. */
@media (min-width: 1281px) {
    #catalogue { --gutter: clamp(24px, 2vw, 40px); --stage: min(96vw, 1360px); }
    #catalogue .stage { width: min(var(--stage), 100%); }
}

/* Four fixed tracks, so a final row of three sits at its natural width rather than
   stretching to fill the row. Seven entries therefore read as 4 + 3, by design. */
.catgrid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: clamp(16px, 1.7vw, 26px); }
/* The card is a plain article until JS gives it a trigger. Hover lift, pointer cursor and
   the "View details" cue are ALL gated behind html.js + the injected .catcard__trigger, so
   with JS off nothing advertises an interaction that cannot happen. */
.catcard { position: relative; display: flex; flex-direction: column; overflow: hidden; border-radius: var(--r-md); background: rgba(255, 255, 255, 0.46); border: 1px solid rgba(118, 94, 50, 0.18); transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.3s cubic-bezier(0.22, 1, 0.36, 1), border-color 0.3s; }
.catcard:has(.catcard__trigger):hover { transform: translateY(-4px); box-shadow: 0 20px 40px rgba(60, 46, 20, 0.18); border-color: var(--bronze-soft); }
.catcard:has(.catcard__trigger:focus-visible) { border-color: var(--bronze); }
/* One media ratio for the whole catalogue, at every breakpoint — a consistent frame is what
   makes seven different photographs read as one set. 3:2 matches the shape most of these
   sources were shot at, so the crops stay shallow. */
.catcard__media { position: relative; aspect-ratio: 3 / 2; overflow: hidden; background: #cfc6ad; }
/* Per-entry focal point, set on the card so it survives with JS off. Defaults to centre;
   each entry overrides it only where the subject genuinely sits off-centre. */
.catcard__media img { width: 100%; height: 100%; object-fit: cover; object-position: var(--card-pos, 50% 50%); transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1); }
.catcard:has(.catcard__trigger):hover .catcard__media img { transform: scale(1.05); }
.catcard__body { flex: 1 1 auto; display: flex; flex-direction: column; padding: clamp(18px, 1.6vw, 24px); }
/* Scan-level eyebrow — one honest word already established elsewhere on the page (the same
   category used by the detail overlay's .cdlg__cat, or, where it is more useful than the
   generic category, an already-stated fact such as "Seasonal"/"Working church"). Same
   specificity trap as .catcard__meta below: needs `p.` to outrank the plain `.catcard p`. */
.catcard p.catcard__tag { margin: 0 0 8px; color: var(--bronze); font-family: var(--sans); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; }
.catcard h3 { font-size: clamp(1.16rem, 1.34vw, 1.36rem); line-height: 1.2; color: var(--paper-heading); }
.catcard p { margin: 11px 0 0; font-size: 1rem; line-height: 1.55; color: var(--paper-soft); }
/* `p.` is load-bearing, not decoration: `.catcard p` above is (0,1,1) and would otherwise
   outrank a plain `.catcard__meta` (0,1,0), replacing `margin-top: auto` with its 11px and
   quietly cancelling the bottom pin — the metadata then floats at a different height in
   every card. Same specificity trap as the [hidden] rule near the top of this stylesheet. */
.catcard p.catcard__meta { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 18px; margin: auto 0 0; padding: 18px 0 0; color: var(--paper-mute); font-size: 0.9rem; }
.catcard__meta span { display: inline-flex; align-items: center; gap: 7px; }
.catcard__meta svg { width: 15px; height: 15px; flex: 0 0 auto; color: var(--bronze); fill: none; stroke: currentColor; stroke-width: 1.65; stroke-linecap: round; stroke-linejoin: round; }

/* The whole card is the control. The button is INJECTED by JS (same pattern as the gallery
   lightbox trigger on the production guide pages), stretched over the card, so it is one
   focus stop and the entire surface is clickable — and so it simply does not exist when JS
   is unavailable. Its name comes from its own hidden "View details" text plus the card's
   visible <h3> via aria-labelledby; the title is never restated in an aria-label. */
.catcard__trigger { position: absolute; inset: 0; z-index: 2; appearance: none; -webkit-appearance: none; padding: 0; border: none; background: transparent; cursor: pointer; border-radius: var(--r-md); }
.catcard__trigger:focus-visible { outline: 2px solid var(--bronze); outline-offset: -3px; }
/* Restrained cue — decorative twin of the hidden button label, so it is aria-hidden. Shown
   only once JS is running; the arrow is the only thing that moves on hover. */
.catcard__cue { display: none; align-items: center; gap: 7px; margin: 14px 0 0; color: var(--bronze); font-family: var(--sans); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; }
html.js .catcard:has(.catcard__trigger) .catcard__cue { display: inline-flex; }
.catcard__cue svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1); }

/* Official visitor link — the route to a CHANGING condition (tickets, opening, tides,
   timetable, transport, access). Authored ONCE, here in the card, never in CATALOGUE_DETAIL:
   that is what keeps it reachable with JavaScript off, when no dialog exists at all. The
   dialog does not author a second copy — town-guide.js CLONES this anchor into the overlay,
   the same single-authorship pattern the media already uses.
   It is distinct from `source` (evidence for what we assert, in the credits panel) and from
   `guideUrl` (internal, deeper into the Atlas). See town-village-contract.md §6.
   z-index clears the injected full-surface .catcard__trigger so the link stays clickable in
   the no-trigger case; it is a real link, not a button, so it works without JS. */
.catcard__visit { position: relative; z-index: 3; display: inline-flex; align-items: center; gap: 7px; margin: 14px 0 0; align-self: flex-start; color: var(--bronze); font-family: var(--sans); font-size: 0.86rem; font-weight: 700; text-decoration: underline; text-underline-offset: 3px; }
.catcard__visit svg { width: 14px; height: 14px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.catcard__visit:focus-visible { outline: 2px solid var(--bronze); outline-offset: 3px; }
/* Hidden on the card only once a trigger genuinely exists — i.e. only when the dialog is
   really there to carry the cloned link. Mirrors the .catcard__cue gate above, so a page
   whose dialog failed to initialise keeps the link on the card rather than losing it. */
html.js .catcard:has(.catcard__trigger) .catcard__visit { display: none; }
.catcard:has(.catcard__trigger):hover .catcard__cue svg, .catcard:has(.catcard__trigger:focus-visible) .catcard__cue svg { transform: translateX(4px); }
@media (prefers-reduced-motion: reduce) { .catcard, .catcard__media img, .catcard__cue svg { transition: none; } .catcard:hover { transform: none; } .catcard:hover .catcard__media img { transform: none; } .catcard:hover .catcard__cue svg { transform: none; } }

/* Designed no-photo plate — a finished treatment, not a missing-image placeholder. */
.plate { position: relative; width: 100%; height: 100%; display: grid; place-items: center; background: linear-gradient(155deg, #23422f 0%, #16301f 100%); }
/* Contour rings — the same line-art register as the page's other glyphs. Lifted slightly
   (0.55 -> 0.72, and a fourth, widest ring) so the plate reads as a designed route card
   rather than a flat green fill, without competing with the six photographs beside it. */
/* The ring box keeps its own 3:2 aspect instead of filling the plate, so the contours stay
   landscape ellipses in the tall overlay frame too — filling the box turned them into egg
   shapes there, which is not the Atlas line-art register. */
.plate::before { content: ""; position: absolute; top: 50%; left: 0; width: 100%; aspect-ratio: 3 / 2; transform: translateY(-50%); opacity: 0.72; background-image: radial-gradient(ellipse 84% 64% at 50% 50%, transparent 25%, rgba(228, 196, 122, 0.11) 25.6%, transparent 27.4%), radial-gradient(ellipse 70% 52% at 50% 50%, transparent 33%, rgba(228, 196, 122, 0.14) 33.7%, transparent 35.6%), radial-gradient(ellipse 56% 40% at 50% 50%, transparent 45%, rgba(228, 196, 122, 0.1) 45.7%, transparent 47.6%), radial-gradient(ellipse 42% 30% at 50% 50%, transparent 57%, rgba(228, 196, 122, 0.08) 57.7%, transparent 59.6%); }
.plate__in { position: relative; display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 18px; text-align: center; }
.plate__in > svg { width: 40px; height: 40px; color: var(--gold-warm); fill: none; stroke: currentColor; stroke-width: 1.15; stroke-linecap: round; stroke-linejoin: round; }
.plate__in b { color: rgba(236, 199, 110, 0.86); font-family: var(--sans); font-size: 0.7rem; font-weight: 700; letter-spacing: 0.2em; text-transform: uppercase; }
/* One restrained fact line, so the plate carries as much as the photographs' captions do. */
/* 0.6 measured 3.79:1 against the plate gradient. This is the no-photo card's only fact
   line — the thing that lets it carry as much as the six photographs' captions do — so it
   has to survive being read. */
.plate__sub { display: block; margin-top: -3px; color: rgba(236, 199, 110, 0.78); font-family: var(--sans); font-size: 0.72rem; letter-spacing: 0.04em; }
.plate__rule { width: 26px; height: 1px; background: rgba(228, 196, 122, 0.42); }

.catfoot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 20px; margin: clamp(20px, 2.1vw, 30px) 0 0; padding-top: 19px; border-top: 1px solid rgba(118, 94, 50, 0.24); }
.catfoot p { margin: 0; max-width: 60ch; font-size: 1rem; color: var(--paper-soft); }

/* --- Catalogue detail overlay -------------------------------------------------------
   A native <dialog> opened with showModal(): the focus trap, Escape handling, top-layer
   stacking and dialog semantics are the platform's, not hand-rolled. The element itself
   is stripped to nothing and the visible surface is .cdlg__inner — that is what makes
   `e.target === dialog` mean "the backdrop" and nothing else, including the panel's own
   padding, which a naive check gets wrong. */
/* `position: fixed`, not `relative` — a modal <dialog>'s UA stylesheet already centres it via
   `dialog:modal { position: fixed; inset-block: 0; margin: auto; }`; declaring `relative` here
   would override that fixed positioning entirely and drop the dialog back into normal
   document flow (found via browser QA: the panel rendered inline in the page instead of as a
   centred overlay). `fixed` keeps the native centring and still works as the containing block
   for .cdlg__close's `position: absolute` below. */
.cdlg { position: fixed; width: min(980px, 92vw); max-width: none; max-height: 88vh; padding: 0; border: 0; background: transparent; overflow: visible; color: var(--paper-ink); }
.cdlg::backdrop { background: rgba(12, 22, 16, 0.62); backdrop-filter: blur(7px); -webkit-backdrop-filter: blur(7px); }
/* Wide editorial layout: one landscape banner across the top, full-bleed to the panel's own
   edges, then content flowing beneath it — replacing the earlier 42/58 portrait side-split.
   .cdlg__inner is now the single scrolling surface (image included), capped at 88vh, so a
   long entry scrolls as one column instead of the old two independent panes. */
.cdlg__inner { display: flex; flex-direction: column; max-height: 88vh; overflow-y: auto; overscroll-behavior: contain; border-radius: var(--r-lg); background: linear-gradient(172deg, var(--paper-top), var(--paper)); box-shadow: 0 40px 90px rgba(6, 14, 10, 0.5); }
/* One consistent ratio for all seven entries — wide enough to read as a banner, not so
   extreme that a portrait-leaning source has nothing left to show. Each entry supplies its
   own object-position (CATALOGUE_DETAIL.dialogImagePosition) to keep its actual subject in
   frame at this ratio; none needs object-fit other than cover. */
.cdlg__media { flex: 0 0 auto; position: relative; aspect-ratio: 2.5 / 1; overflow: hidden; background: #cfc6ad; }
.cdlg__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Sits on .cdlg itself (see the button's new position in the markup, a sibling of
   .cdlg__inner rather than a child of .cdlg__media) so it stays put through the panel's own
   scroll — position:absolute against a scrolling ancestor would otherwise carry it away with
   the image the moment a tall entry's content is scrolled. */
.cdlg__close { position: absolute; top: 14px; right: 14px; z-index: 5; display: grid; place-items: center; width: 42px; height: 42px; padding: 0; border: 1px solid var(--green-rule); border-radius: 50%; background: rgba(16, 29, 22, 0.86); color: #f6ecd2; cursor: pointer; transition: background 0.22s, border-color 0.22s; }
.cdlg__close svg { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; }
.cdlg__close:hover { background: #101d16; border-color: var(--gold-warm); }
.cdlg__close:focus-visible { outline: 2px solid var(--gold-warm); outline-offset: 2px; }
.cdlg__col { padding: clamp(24px, 2.6vw, 40px); }
.cdlg__cat { margin: 0; color: var(--bronze); font-family: var(--sans); font-size: 0.74rem; font-weight: 700; letter-spacing: 0.2em; text-transform: uppercase; }
.cdlg__t { margin: 10px 0 0; font-family: var(--serif); font-size: clamp(1.5rem, 2.1vw, 2.05rem); line-height: 1.14; color: var(--paper-heading); }
.cdlg__sum { margin: 14px 0 0; font-size: 1.04rem; line-height: 1.6; color: var(--paper-soft); }
/* Adaptive fact strip — 2, 3 or 4 fields, never a partially-empty row. JS counts the
   VISIBLE fact rows and stamps that count onto .cdlg__facts as data-count, the same
   adaptive-component convention already used by .ways3[data-count]/.lifecats[data-count]
   elsewhere on this page — a genuinely reusable pattern, not a one-off. Each fact carries
   its own fixed icon (by fact type, not by entry), matching the approved reference design. */
.cdlg__facts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; margin: 22px 0 0; padding: 0; border: 1px solid var(--paper-rule); border-radius: var(--r-sm); overflow: hidden; background: var(--paper-rule); }
.cdlg__facts[data-count="1"] { grid-template-columns: minmax(0, 1fr); }
.cdlg__facts[data-count="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.cdlg__facts[data-count="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.cdlg__fact { display: flex; flex-direction: column; align-items: center; gap: 7px; padding: 16px 8px; text-align: center; background: rgba(255, 255, 255, 0.4); }
.cdlg__fact svg { width: 19px; height: 19px; color: var(--bronze); fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.cdlg__fact dt { margin: 0; color: var(--paper-mute); font-family: var(--sans); font-size: 0.64rem; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
.cdlg__fact dd { margin: 2px 0 0; font-size: 0.95rem; font-weight: 600; color: var(--paper-heading); }

/* Why stop here / Good to know: two balanced blocks side by side (the reference layout),
   stacked at ≤760px (see that breakpoint). data-count follows the same adaptive convention
   as .cdlg__facts, for the same reason: if a future entry only carries one of the two, the
   lone block should span full width, not leave an empty column beside it. */
.cdlg__split { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; margin: 22px 0 0; align-items: stretch; }
.cdlg__split[data-count="1"] { grid-template-columns: minmax(0, 1fr); }
/* The one line that answers "should I give this my time?" — a star badge plus a short
   editorial judgement, given a quiet frame rather than a loud one. */
.cdlg__why { display: flex; gap: 14px; margin: 0; padding: 18px 20px; border-radius: var(--r-sm); background: rgba(228, 196, 122, 0.16); }
.cdlg__why-badge { display: grid; flex: 0 0 auto; place-items: center; width: 30px; height: 30px; border-radius: 50%; background: var(--bronze); color: var(--paper-top); }
.cdlg__why-badge svg { width: 15px; height: 15px; fill: currentColor; stroke: none; }
.cdlg__why-body b { display: block; margin: 0 0 6px; color: var(--paper-heading); font-family: var(--sans); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; }
.cdlg__why-body span { font-size: 1rem; line-height: 1.6; color: var(--paper-soft); }

/* 2–3 short, scannable, verified bullets — hidden as a whole block when an entry has none. */
.cdlg__good { margin: 0; padding: 16px 18px; border: 1px solid var(--paper-rule); border-radius: var(--r-sm); background: rgba(118, 94, 50, 0.06); }
.cdlg__good-head { display: flex; align-items: center; gap: 8px; margin: 0 0 10px; color: var(--paper-mute); font-family: var(--sans); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; }
.cdlg__good-head svg { width: 15px; height: 15px; color: var(--bronze); fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; }
.cdlg__good ul { display: flex; flex-direction: column; gap: 8px; margin: 0; padding: 0; list-style: none; }
.cdlg__good li { display: flex; align-items: flex-start; gap: 9px; font-size: 0.94rem; line-height: 1.45; color: var(--paper-soft); }
.cdlg__good li svg { flex: 0 0 auto; width: 15px; height: 15px; margin-top: 2px; fill: none; stroke: var(--bronze); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

/* Optional practical/access note — a single line, not a box, so it reads as a coda rather
   than another callout. Hidden entirely on an entry with nothing to add here. */
.cdlg__access { display: flex; gap: 10px; margin: 18px 0 0; font-size: 0.94rem; line-height: 1.5; color: var(--paper-mute); }
.cdlg__access svg { width: 17px; height: 17px; flex: 0 0 auto; margin-top: 2px; color: var(--bronze); fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }

/* Cloned destination for the card's .catcard__visit anchor. The wrapper is INJECTED by
   town-guide.js immediately before .cdlg__guide, so no page's dialog markup has to carry it
   and every shared-layer page inherits the control automatically. Styled as a visitor ACTION
   distinct from the internal "Open full guide" button below it and from the evidence links
   inside the collapsed credits panel. */
.cdlg__visit { margin: 18px 0 0; }
.cdlg__visit .catcard__visit { display: inline-flex; margin: 0; font-size: 0.92rem; }
.cdlg__guide { margin: 18px 0 0; }

/* Provenance, not content — a collapsed disclosure rather than an always-open box, so
   sourcing stays available without competing with the overview/facts/why/good-to-know
   above it. Was 0.5 alpha = 2.70:1, the worst contrast on the page, when this lived in the
   old always-visible .cdlg__foot box carrying the CC BY-SA attribution close to invisible —
   fixed then, preserved here: provenance may recede, it may not become unreadable. */
.cdlg__credits { margin: 20px 0 0; padding-top: 14px; border-top: 1px solid var(--paper-rule); }
.cdlg__credits summary { cursor: pointer; color: rgba(138, 106, 46, 0.75); font-family: var(--sans); font-size: 0.78rem; font-weight: 600; list-style: none; }
.cdlg__credits summary::-webkit-details-marker { display: none; }
.cdlg__credits summary:hover, .cdlg__credits summary:focus-visible { color: var(--bronze); }
.cdlg__credits-b { margin-top: 10px; }
/* .cdlg__credit (the per-entry "Image: ..." line) was retired 2026-07-31 with
   CATALOGUE_DETAIL.imageCredit — image attribution is footer-level metadata, so this
   disclosure is sources-only now. Its rules are deleted, not disabled: the credit had to
   stop being emitted, not be painted over. */
.cdlg__src { margin: 0; font-size: 0.76rem; line-height: 1.45; color: rgba(48, 49, 31, 0.78); }
.cdlg__src a { color: rgba(138, 106, 46, 0.82); text-underline-offset: 2px; }
.cdlg__src a:hover { color: var(--bronze); }
/* Subtle only: opacity plus an 8px lift, no scale. */
.cdlg[open] .cdlg__inner { animation: cdlg-in 0.24s cubic-bezier(0.22, 1, 0.36, 1); }
@keyframes cdlg-in { from { opacity: 0; transform: translateY(8px); } }
@media (prefers-reduced-motion: reduce) { .cdlg[open] .cdlg__inner { animation: none; } .cdlg__close { transition: none; } }

/* =====================================================================================
   CHAPTER 3 — UNDERSTAND AND FEEL
   ===================================================================================== */
/* Story: 42/58 with the image running the FULL height of the composition — this is the
   fix for v1's dead lower-right region. */
.story { display: grid; grid-template-columns: minmax(0, 0.76fr) minmax(0, 1fr); gap: clamp(30px, 4vw, 68px); align-items: stretch; }
.story__col { display: flex; flex-direction: column; justify-content: center; }
.spine { margin: clamp(30px, 3vw, 42px) 0 0; padding: 0; list-style: none; }
.layer { position: relative; display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: 20px; padding: 0 0 clamp(24px, 2.4vw, 32px); }
.layer:last-child { padding-bottom: 0; }
.layer:not(:last-child)::before { content: ""; position: absolute; left: 28px; top: 58px; bottom: 8px; width: 1px; background: linear-gradient(180deg, rgba(228, 196, 122, 0.45), rgba(228, 196, 122, 0.12)); }
.layer__m { position: relative; z-index: var(--z-base); display: grid; place-items: center; width: 56px; height: 56px; border-radius: 50%; border: 1px solid rgba(228, 196, 122, 0.36); background: rgba(10, 26, 19, 0.66); }
.layer__m svg { width: 25px; height: 25px; color: var(--gold-warm); fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round; }
.layer h3 { margin: 13px 0 0; font-size: clamp(1.2rem, 1.42vw, 1.42rem); line-height: 1.2; color: var(--gold-bright); }
.layer p { margin: 9px 0 0; font-size: 1rem; line-height: 1.6; color: rgba(248, 239, 216, 0.78); }

.story__fig { position: relative; margin: 0; border-radius: var(--r-md); overflow: hidden; min-height: 100%; }
.story__fig img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 38%; }
/* Long, multi-stop fade — a short one produced a visible hard edge where the scrim began. */
/* margin:0 is load-bearing: a blockquote's UA default margin (1em 40px) applies even when
   absolutely positioned with left/right/bottom set, which floated this scrim 40px in from
   each side of the photograph with hard visible edges. */
.story__quote { position: absolute; left: 0; right: 0; bottom: 0; margin: 0; padding: clamp(140px, 18vw, 240px) clamp(26px, 2.6vw, 40px) clamp(26px, 2.6vw, 36px); background: linear-gradient(180deg, rgba(8, 20, 15, 0) 0%, rgba(8, 20, 15, 0.28) 28%, rgba(8, 20, 15, 0.72) 56%, rgba(8, 20, 15, 0.94) 78%); }
.story__quote p { margin: 0; font-family: var(--serif); font-size: clamp(1.4rem, 2.1vw, 2rem); line-height: 1.28; color: #fdf7e6; }
.story__quote cite { display: block; margin: 16px 0 0; color: rgba(236, 199, 110, 0.72); font-size: 0.76rem; font-style: normal; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; }

/* =====================================================================================
   Local flavours (v7) — EDITORIAL CHAPTER, all-green. No chapter title above the
   composition and no cream: the chapter names itself from inside the left column.
     ROW 1   dark editorial lead (21%) │ tall photograph (28%) │ dark grouped panel (51%)
             panel = "eat, drink, shop local" (4 image cards) then "local atmosphere" (4)
     ROW 2   full-width dark plate: slower-pace note │ 2 wide + 2 narrow shots │ pull quote

   v6 floated cream panels on the band, which read as a separate component dropped onto
   the page, and led with a large centred "7. Local flavours…" heading. v7 keeps the same
   IA and proportions but renders every surface in the band's own green, so the chapter
   reads as part of the page rather than a window on top of it.
   ===================================================================================== */
.life { display: flex; flex-direction: column; gap: clamp(20px, 2vw, 32px); }
/* No container override: this chapter uses the same `.stage` as every section around it,
   so its left and right edges line up with the ones above and below. */

/* Track widths ARE the composition: 21/28/51 — editorial / photograph / grouped panel.
   fr-capped minmax keeps the ratio while refusing to crush the narrow tracks below a
   readable minimum. */
/* align-items:start — the photograph's fixed height is the row's only deliberate anchor;
   the editorial panel and the grouped panel size to their own content instead of being
   stretched to match it, which is what produced blank card interiors before. */
/* Deliberately uneven column gaps: the lead and the photograph are ONE editorial unit
   (tight gap, joined corners, matched height), and the grouped panel sits apart from it. */
/* Column split widened on the left (21fr -> 26fr, at the hero's expense 28 -> 25): the
   intro panel is the tallest column, and at the old 275px width its two paragraphs and tip
   wrapped to a lot of lines — the single biggest driver of this section's height. A wider
   lead column wraps them in fewer lines, shortening the whole row, and the hero loses only
   a little width (still a comfortable landscape crop of the Green). */
.life__top { display: grid; grid-template-columns: minmax(300px, 26fr) minmax(320px, 25fr) minmax(0, 49fr); column-gap: clamp(7px, 0.5vw, 9px); row-gap: clamp(16px, 1.2vw, 20px); align-items: start; }

/* Same material as .life__lead — the whole chapter is one green surface, separated by
   gold hairlines rather than by a change of paper. */
.lifepanel { display: flex; flex-direction: column; padding: clamp(20px, 1.7vw, 26px); border-radius: var(--r-plate); border: 1px solid rgba(228, 196, 122, 0.28); background: linear-gradient(158deg, #1d4133, #0f2119); color: var(--cream); }
/* The rule runs out to the panel edge, as in the reference — it groups the block below it. */
.lifepanel__head { display: flex; align-items: center; gap: 10px; margin: 0 0 clamp(9px, 1vw, 13px); }
.lifepanel__head::after { content: ""; flex: 1 1 auto; height: 1px; background: linear-gradient(90deg, rgba(228, 196, 122, 0.42), rgba(228, 196, 122, 0.08)); }
.lifepanel__head svg { width: 20px; height: 20px; flex: 0 0 auto; color: var(--gold-warm); fill: none; stroke: currentColor; stroke-width: 1.35; stroke-linecap: round; stroke-linejoin: round; }
.lifepanel__head h3 { margin: 0; font-family: var(--sans); font-size: 0.74rem; font-weight: 700; letter-spacing: 0.17em; text-transform: uppercase; color: var(--gold-warm); }
/* A fixed gap, deliberately: the panel's spare height is absorbed by the card images
   above (.lifecats/.lifecat__img grow), not dumped into this divider as a dead band. */
.lifepanel__head--sub { margin-top: clamp(13px, 1.3vw, 18px); padding-top: clamp(13px, 1.2vw, 17px); border-top: 1px solid rgba(228, 196, 122, 0.18); }

/* Dark card on a dark band: the gold hairline is what separates it, as in the reference. */
/* Stretched to the row and squared on its inner edge — the lead and the photograph read as
   one editorial unit rather than two cards that happen to sit next to each other. The tip
   is pushed to the foot (margin-top:auto below) so stretching adds no blank interior. */
.life__lead { align-self: stretch; display: flex; flex-direction: column; padding: clamp(22px, 1.9vw, 28px); border-radius: var(--r-plate) var(--r-join) var(--r-join) var(--r-plate); border: 1px solid rgba(228, 196, 122, 0.28); border-right: 0; background: linear-gradient(158deg, #1d4133, #0f2119); color: var(--cream); }
/* flex-start, not center: this label wraps to two or three lines in the 21% track, and
   centring floated the icon into the middle of the wrapped block. */
.life__kicker { display: flex; align-items: flex-start; gap: 11px; margin: 0 0 clamp(11px, 1.1vw, 15px); color: var(--gold-warm); font-family: var(--sans); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.18em; line-height: 1.5; text-transform: uppercase; }
.life__kicker svg { width: 22px; height: 22px; margin-top: -1px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; }
.life__lead h3 { font-family: var(--serif); font-size: clamp(1.5rem, 1.85vw, 2.05rem); line-height: 1.14; letter-spacing: -0.008em; color: var(--cream); text-wrap: pretty; }
.life__rule { width: 74px; height: 3px; margin: 11px 0 0; background: linear-gradient(90deg, var(--gold-bright), rgba(228, 196, 122, 0.2)); border: 0; }
.life__lead > p { margin: 12px 0 0; font-size: clamp(0.9rem, 0.92vw, 0.97rem); line-height: 1.46; color: rgba(248, 239, 216, 0.84); }
/* margin-top:auto pins the tip to the foot of the stretched panel; the paragraph above
   carries the minimum gap for the case where there is no free space to absorb. */
.life__lead > p:last-of-type { margin-bottom: clamp(14px, 1.4vw, 18px); }
.life__tip { margin-top: auto; padding: clamp(11px, 1vw, 14px); border: 1px solid rgba(228, 196, 122, 0.42); border-radius: var(--r-sm); background: rgba(228, 196, 122, 0.07); }
.life__tip b { display: flex; align-items: center; gap: 9px; margin: 0 0 8px; color: var(--gold-bright); font-size: 0.7rem; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; }
.life__tip b svg { width: 18px; height: 18px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round; }
.life__tip p { margin: 0; font-size: 0.9rem; line-height: 1.48; color: rgba(248, 239, 216, 0.84); }

/* The photograph is the one element that DOES stretch to the row (the grouped panel sets
   the height, as in the reference); min-height keeps it an anchor if the panel is short.
   The lead and the panel still size to their own content — stretching those is what
   produced blank card interiors before. */
/* min-height reduced from clamp(420,34vw,560): with the right-hand panel now much shorter
   (landscape theme frames + one-line card copy), the hero no longer needs to reserve that
   much height to keep the row balanced, and a shorter hero is part of the ~20-25% section
   trim. It is still a floor — the row height is set by whichever column is naturally tallest. */
.life__fig { align-self: stretch; position: relative; margin: 0; min-height: clamp(340px, 26vw, 440px); border-radius: var(--r-join) var(--r-plate) var(--r-plate) var(--r-join); overflow: hidden; }
/* Crop weighted right and just below centre: it holds the shopfronts and the stone terrace
   in frame, and drops most of the pale sky and the empty foreground road. */
.life__fig img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 66% 54%; }
.life__fig figcaption { position: absolute; left: 0; right: 0; bottom: 0; padding: 56px clamp(16px, 1.5vw, 22px) clamp(15px, 1.4vw, 20px); background: linear-gradient(180deg, transparent, rgba(8, 20, 15, 0.86) 52%); color: rgba(248, 239, 216, 0.88); font-size: 0.86rem; line-height: 1.45; }

/* The margin, not the grid gap, is what sets this panel apart from the lead+photograph pair.
   Stretched to the row so its foot lines up with the photograph's; the rhythm block below
   takes the slack (margin-top:auto) instead of leaving it as blank panel interior. */
.life__right { min-width: 0; margin-left: clamp(12px, 0.9vw, 15px); align-self: stretch; display: flex; }
.life__right > .lifepanel { flex: 1 1 auto; }

/* Four category cards across, as in the reference. flex-start + min-height on the label
   reserves the two lines the longest one needs, so all four photographs start on the same
   baseline instead of stepping down under a wrapped heading. */
/* flex:1 — this block takes whatever height the stretched panel has spare, and passes it
   down to the four plates so the slack becomes picture. */
/* Adaptive by theme count — a reusable rule, not a page-specific hack: the component is
   driven by data-count on the .lifecats root (1/2/3/4), read together with however many
   .lifecat children actually exist, so a future page can carry any of the four counts
   without touching this CSS.
     4 -> the original four-across composition (unchanged)
     3 -> three equal columns (Beaumaris uses this: Castle Street / waterfront / working town)
     2 -> two equal editorial features
     1 -> one full-width editorial feature (image + copy side by side), never an empty
          three-quarters-blank four-column shell */
.lifecats { flex: 1 1 auto; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: clamp(12px, 1.2vw, 18px); }
.lifecats[data-count="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.lifecats[data-count="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.lifecats[data-count="1"] { grid-template-columns: minmax(0, 1fr); }
.lifecats[data-count="1"] .lifecat { display: grid; grid-template-columns: minmax(220px, 0.9fr) minmax(0, 1.4fr); gap: clamp(14px, 1.4vw, 20px); align-items: start; }
.lifecats[data-count="1"] .lifecat h3 { grid-column: 1 / -1; min-height: 0; }
.lifecats[data-count="1"] .lifecat__img { aspect-ratio: 4 / 3; margin: 0; }
.lifecats[data-count="1"] .lifecat p { min-height: 0; align-self: center; }
@media (max-width: 640px) {
    .lifecats[data-count="1"] .lifecat { grid-template-columns: 1fr; }
}
.lifecat { display: flex; flex-direction: column; }
.lifecat h3 { display: flex; align-items: flex-start; gap: 8px; min-height: 2.2em; margin: 0 0 8px; font-family: var(--sans); font-size: 0.68rem; font-weight: 700; letter-spacing: 0.11em; line-height: 1.35; text-transform: uppercase; color: var(--gold-warm); }
.lifecat h3 svg { width: 19px; height: 19px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.35; stroke-linecap: round; stroke-linejoin: round; }
/* The caption names what the PICTURE shows; the heading is a category, so no image is
   presented as a named business. */
/* Fixed by its own aspect-ratio, NOT flex-grow: `flex: 1 1 auto` here (inherited from the
   Betws original) let each image absorb whatever vertical space its own card's heading/
   paragraph text happened to leave over, so three cards with three genuinely different copy
   lengths rendered three genuinely different image heights — the "mismatched, uneven" bug.
   `flex: 0 0 auto` fixes every image to the same 1:1 shape from its own (grid-equal) width,
   independent of how long its neighbours' captions run. */
/* 3:2 landscape, not 1:1 — the three photographs are all native-landscape townscapes
   (a shop street, an inn frontage, the pier), so a landscape card frame crops them far
   less than a square did, and the shorter frame is the single biggest lever in trimming
   this section's total height. `flex: 0 0 auto` still fixes every image to one identical
   shape from its own (grid-equal) width, so the three stay aligned regardless of how long
   each caption runs — the fix for the earlier "mismatched, uneven" bug is unchanged. */
.lifecat__img { flex: 0 0 auto; position: relative; aspect-ratio: 3 / 2; margin: 0 0 8px; border-radius: var(--r-sm); overflow: hidden; border: 1px solid rgba(228, 196, 122, 0.2); background: rgba(8, 20, 15, 0.5); }
.lifecat__img img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* Sentence case, not a second gold caps label. Four of these stacked directly under four
   caps headings made the panel read as all label and no content. It still names what the
   PHOTOGRAPH shows, which is the honesty job it exists for. */
.lifecat__cap { display: block; margin: 0 0 6px; color: rgba(236, 199, 110, 0.66); font-size: 0.74rem; font-style: italic; line-height: 1.35; }
/* Was 4.5em, reserved for the old two-to-three-sentence cards. The copy is now one short
   fragment each (all two lines at this column width), so 2.2em holds them without a dead
   gap under the shortest — the reserve still equalises the three so their captions line up. */
.lifecat p { margin: 0; min-height: 2.2em; font-size: 0.85rem; line-height: 1.5; color: rgba(248, 239, 216, 0.78); }

/* Atmosphere: four short columns under a hairline, divided by rules rather than cards —
   it is a coda to the cards above, not a second card grid competing with them. */
.rhythm { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(14px, 1.4vw, 22px); margin: 0; padding: 0; }
.rhythm > div { padding-left: clamp(12px, 1.2vw, 18px); border-left: 1px solid rgba(228, 196, 122, 0.16); }
.rhythm > div:first-child { padding-left: 0; border-left: 0; }
/* min-height reserves the two lines the longest label needs at narrow widths, so all four
   descriptions start on the same line rather than stepping down under a wrapped label. */
.rhythm dt { display: flex; align-items: flex-start; gap: 8px; min-height: 2.1em; margin: 0; line-height: 1.35; color: var(--gold-warm); font-family: var(--sans); font-size: 0.68rem; font-weight: 700; letter-spacing: 0.11em; text-transform: uppercase; }
.rhythm svg { width: 17px; height: 17px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.35; stroke-linecap: round; stroke-linejoin: round; }
.rhythm dd { margin: 9px 0 0; color: rgba(248, 239, 216, 0.78); font-size: 0.82rem; line-height: 1.5; }

/* ROW 3 — one dark plate closing the chapter: a composed six-track panel — an editorial
   note, four equal heritage photographs, and a pull-quote. Recovered from the pre-checkpoint
   design (commit f0af813), which a later pass had replaced with a weaker standalone centred
   text note. The shared plate (background/border/radius/padding) is unchanged; the grid,
   thumbnail, quote and CTA rules below are the restored composition. The left note keeps a
   heading (carried over from the removed centred card, whose copy now lives here). */
/* align-items:center (not stretch) so the four photographs keep their own compact media
   height instead of stretching to fill the taller text column — the fix for the strip having
   grown noticeably taller than its accepted proportions. The note and quote centre as before. */
.life__foot { display: grid; grid-template-columns: minmax(230px, 1fr) repeat(4, minmax(0, 1fr)) minmax(215px, 0.9fr); gap: clamp(14px, 1.4vw, 22px); align-items: center; padding: clamp(18px, 1.9vw, 28px); border-radius: var(--r-md); border: 1px solid rgba(228, 196, 122, 0.26); background: linear-gradient(158deg, #1d4133, #0f2119); }
.life__note { align-self: center; }
/* Own the kicker's bottom margin here, so the more-specific `.life__foot p` rule below does
   not steal it and collapse the gap above the heading. */
.life__note .life__kicker { margin: 0 0 10px; }
.life__note h3 { margin: 0; font-family: var(--serif); font-size: clamp(1.08rem, 1.1vw, 1.24rem); line-height: 1.18; color: var(--cream); }
.life__foot p { margin: 12px 0 0; font-size: 0.93rem; line-height: 1.5; color: rgba(248, 239, 216, 0.82); }
.life__more { display: inline-flex; align-items: center; gap: 9px; margin: 14px 0 0; color: var(--gold-warm); text-decoration: none; font-size: 0.88rem; font-weight: 600; border-bottom: 1px solid rgba(228, 196, 122, 0.45); padding-bottom: 3px; }
.life__more:hover { color: var(--gold-bright); border-bottom-color: var(--gold-bright); }
.life__more:focus-visible { outline: 2px solid var(--gold-warm); outline-offset: 3px; border-radius: 3px; }
.life__more svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
/* display:contents lets the four <li> sit directly on the .life__foot grid tracks, so a
   single row aligns note + images + quote. */
.life__thumbs { display: contents; margin: 0; padding: 0; list-style: none; }
.life__thumbs li { position: relative; min-height: clamp(190px, 16vw, 265px); border-radius: var(--r-sm); overflow: hidden; border: 1px solid rgba(228, 196, 122, 0.18); }
.life__thumbs img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.life__pull { position: relative; align-self: center; margin: 0; padding: 0 0 0 clamp(22px, 2.2vw, 32px); border-left: 1px solid rgba(228, 196, 122, 0.3); }
.life__pull svg { width: 34px; height: 27px; margin: 0 0 12px; color: rgba(228, 196, 122, 0.5); fill: currentColor; }
.life__pull p { margin: 0; font-family: var(--serif); font-size: clamp(1.02rem, 1.15vw, 1.24rem); line-height: 1.32; color: var(--cream); }
.life__pullrule { width: 38px; height: 2px; margin: 15px 0 0; background: var(--gold-warm); border: 0; opacity: 0.85; }
.life__pull cite { display: block; margin: 11px 0 0; color: rgba(236, 199, 110, 0.75); font-size: 0.7rem; font-style: normal; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; }

/* =====================================================================================
   CHAPTER 4 — PLAN: itineraries + practical, read as one planning system.
   ===================================================================================== */
/* Itinerary (v3): one compact, wide route card.
     LEFT   ≈26% dark anchor panel — the anchor photograph is the panel BACKGROUND, not a
            block stacked under the text. v2 stacked it, which set the row height at ~457px
            and left the cream step rail two-thirds empty.
     RIGHT  ≈74% cream route rail — five stops, each with a real thumbnail, a large numbered
            badge and a visible connector, so the route fills the surface it sits on. */
/* One plan open at a time. Collapsed plans stay fully readable as ribbons; the same
   markup carries both states, so no content is duplicated for the two layouts.
   Disclosure reuses the practical accordion's 0fr→1fr mechanism — nothing measured. */
.itins { display: flex; flex-direction: column; gap: clamp(14px, 1.5vw, 20px); }
.itin { display: grid; grid-template-columns: 1fr; border-radius: var(--r-md); overflow: hidden; box-shadow: 0 20px 44px rgba(6, 14, 10, 0.42); }
.itin.is-open { grid-template-columns: minmax(266px, 0.52fr) minmax(0, 2.5fr); }

/* The head is the heading element, but only .itin__name should read as heading type —
   the badge, summary and dots keep the body face. */
.itin__head { position: relative; isolation: isolate; margin: 0; font-family: var(--sans); font-weight: 400; font-size: 1rem; background: linear-gradient(158deg, var(--green-card-top), var(--green-card)); }
.itin__head img { position: absolute; inset: 0; z-index: -2; width: 100%; height: 100%; object-fit: cover; object-position: 50% 46%; }
/* Scrim is weighted, not opaque: the anchor photograph has to stay legible as a photograph
   (an earlier pass at 0.90/0.96 rendered the panel as flat green). */
.itin__head::after { content: ""; position: absolute; inset: 0; z-index: -1; background: linear-gradient(158deg, rgba(20, 52, 38, 0.7), rgba(14, 38, 28, 0.82) 58%, rgba(10, 28, 21, 0.9)); }

/* COLLAPSED: horizontal ribbon — badge · title · summary · five dots · chevron. */
.itin__btn { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; width: 100%; height: 100%; margin: 0; padding: 14px clamp(16px, 1.6vw, 22px); border: 0; background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; }
.itin__btn:focus-visible { outline: 2px solid var(--gold-bright); outline-offset: -4px; border-radius: 6px; }
.itin__dur { display: inline-flex; align-items: center; gap: 8px; flex: 0 0 auto; margin: 0; padding: 6px 13px; border: 1px solid rgba(228, 196, 122, 0.42); border-radius: 999px; background: rgba(8, 20, 15, 0.52); color: var(--gold-bright); font-size: 0.76rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; white-space: nowrap; }
.itin__dur svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
.itin__name { flex: 0 0 auto; font-family: var(--serif); font-weight: 600; font-size: clamp(1.06rem, 1.2vw, 1.2rem); line-height: 1.2; color: var(--cream); text-shadow: 0 2px 18px rgba(0, 0, 0, 0.45); }
.itin__desc { flex: 1 1 220px; min-width: 0; font-size: 0.93rem; line-height: 1.5; color: rgba(248, 239, 216, 0.8); }
.itin__mode { display: none; align-items: center; gap: 8px; align-self: flex-start; padding: 6px 12px; border: 1px dashed rgba(228, 196, 122, 0.45); border-radius: 999px; background: rgba(8, 20, 15, 0.34); color: rgba(236, 199, 110, 0.94); font-size: 0.8rem; }
.itin__mode svg { width: 14px; height: 14px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
/* Five small numbered dots — the route in miniature, dashed connectors and all. */
.itin__dots { display: flex; align-items: center; flex: 0 0 auto; margin-left: auto; }
.itin__dots span { position: relative; display: grid; place-items: center; width: 23px; height: 23px; border-radius: 50%; border: 1px solid rgba(228, 196, 122, 0.42); background: rgba(8, 20, 15, 0.55); color: var(--gold-bright); font-family: var(--serif); font-size: 0.74rem; }
.itin__dots span + span { margin-left: 13px; }
.itin__dots span + span::before { content: ""; position: absolute; right: 100%; top: 50%; width: 13px; border-top: 1px dashed rgba(228, 196, 122, 0.4); }
.itin__chev { display: grid; place-items: center; flex: 0 0 auto; width: 24px; height: 24px; border-radius: 50%; border: 1px solid rgba(228, 196, 122, 0.42); color: var(--gold-bright); transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1); }
.itin__chev svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }

/* EXPANDED: the original dark anchor panel, unchanged in composition. */
.itin.is-open .itin__btn { flex-direction: column; flex-wrap: nowrap; align-items: flex-start; justify-content: center; gap: 0; padding: clamp(22px, 2vw, 30px); }
/* Defensive, not decorative: the anchor panel is a narrow (min 266px) column, and
   .itin__dur is `white-space: nowrap` for the wide collapsed row layout. A duration
   string long enough to exceed that column's width would otherwise overflow the pill
   shape and collide with .itin__name below it (the exact bug a too-long duration string
   produced here once already — see the shortened pill text in the markup). Wrapping is
   the fallback, not the fix: the real fix is keeping duration strings pill-length and
   moving any extra nuance into .itin__desc, which already wraps normally. */
.itin.is-open .itin__dur { margin: 0 0 12px; max-width: 100%; white-space: normal; }
.itin.is-open .itin__name { font-size: clamp(1.34rem, 1.66vw, 1.72rem); line-height: 1.16; }
.itin.is-open .itin__desc { flex: 0 0 auto; margin: 10px 0 0; font-size: 0.98rem; line-height: 1.52; color: rgba(248, 239, 216, 0.82); }
.itin.is-open .itin__mode { display: inline-flex; margin: 14px 0 0; }
.itin.is-open .itin__dots { display: none; }
.itin.is-open .itin__chev { position: absolute; top: 14px; right: 14px; transform: rotate(180deg); }

/* Content-driven disclosure: 0fr→1fr grows to the route rail's own height, so nothing
   is measured, hard-coded or clipped. */
.itin__panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.85s cubic-bezier(0.22, 1, 0.36, 1); }
.itin.is-open .itin__panel { grid-template-rows: 1fr; }
.itin__panel > div { min-height: 0; overflow: hidden; visibility: hidden; transition: visibility 0s linear 0.85s; }
.itin.is-open .itin__panel > div { visibility: visible; transition-delay: 0s; }
/* No JS: every route reads open — including the steps themselves. Their opacity-0 reveal
   start state only clears under .itin.is-open (or reduced motion), so without these two
   rules the two closed routes opened as large empty cream slabs. */
html.no-js .itin__panel { grid-template-rows: 1fr; }
html.no-js .itin__panel > div { visibility: visible; }
html.no-js .step { opacity: 1; transform: none; }
html.no-js .step:not(:last-child) .step__bar::after { transform: scaleX(1); }

.steps { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); align-content: center; gap: 0 clamp(10px, 1vw, 18px); margin: 0; height: 100%; padding: clamp(20px, 1.8vw, 26px) clamp(18px, 1.7vw, 26px); list-style: none; background: linear-gradient(172deg, var(--paper-top), var(--paper)); }

/* SHORT-ROUTE CROP (approved on the Pwllheli pilot, rolled out 2026-07-27).
   The step grid above is a fixed five columns, so a three- or four-stop route used to leave one or
   two empty paper columns at the right-hand end of the open panel — dead space that reads as
   missing content. The fix CROPS the card rather than resizing the stops: images, gaps and padding
   keep exactly their five-column dimensions, and the open article simply stops after the last real
   stop.

   The width is this file's own geometry solved for n stops. Above 900px the left column
   minmax(266px, 0.52fr) ALWAYS resolves to its 266px minimum — 0.52/3.02 of --stage only exceeds
   266px past a 1545px stage, and --stage caps at 1520px — so the step content box is
   100% - 266px - 2*PAD, one column is (that - 4*GAP)/5, and the cropped width is
   266px + 2*PAD + n*col + (n-1)*GAP, expanded below. The PAD/GAP clamps repeat .steps verbatim: if
   they change above, change them here too.

   The track count must follow the crop, or the stops would be squeezed into five columns inside the
   narrower box. Ordered widest-first, because a three-stop list matches both selectors.
   Five- and six-stop routes match neither and keep the full-width card. Below 900px .steps becomes
   a horizontal scroller (see the max-width: 900px block), so this is scoped above it. */
@media (min-width: 901px) {
    .itin.is-open:not(:has(.steps > :nth-child(5))) {
        width: calc(80% + 53.2px + 0.4 * clamp(18px, 1.7vw, 26px) - 0.2 * clamp(10px, 1vw, 18px));
    }
    .itin.is-open:not(:has(.steps > :nth-child(4))) {
        width: calc(60% + 106.4px + 0.8 * clamp(18px, 1.7vw, 26px) - 0.4 * clamp(10px, 1vw, 18px));
    }
    .steps:not(:has(> :nth-child(5))) { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    .steps:not(:has(> :nth-child(4))) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* Stops rise and fade in sequence; the connector draws left-to-right behind them. */
.step { position: relative; display: flex; flex-direction: column; list-style: none; opacity: 0; transform: translateY(10px); transition: opacity 0.75s ease, transform 0.75s cubic-bezier(0.22, 1, 0.36, 1); }
.itin.is-open .step { opacity: 1; transform: none; }
.itin.is-open .step:nth-child(1) { transition-delay: 0.25s; }
.itin.is-open .step:nth-child(2) { transition-delay: 0.40s; }
.itin.is-open .step:nth-child(3) { transition-delay: 0.55s; }
.itin.is-open .step:nth-child(4) { transition-delay: 0.70s; }
.itin.is-open .step:nth-child(5) { transition-delay: 0.85s; }
.step__media { position: relative; display: block; aspect-ratio: 16 / 10; border-radius: var(--r-sm); overflow: hidden; background: #cfc6ad; border: 1px solid rgba(90, 74, 46, 0.2); }
.step__media img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1); }
/* Step photographs open the shared image lightbox (trigger injected by JS). The zoom cue
   matches the Local-life gallery so a tappable photo reads the same across the page. */
.step__media:hover img { transform: scale(1.05); }
@media (prefers-reduced-motion: reduce) { .step__media img { transition: none; } .step__media:hover img { transform: none; } }
/* Marker row: badge centred, connector running between neighbouring badges. */
.step__bar { position: relative; display: flex; justify-content: center; margin: 12px 0 0; }
.step:not(:last-child) .step__bar::after { content: ""; position: absolute; top: 50%; left: calc(50% + 24px); width: calc(100% + clamp(10px, 1vw, 18px) - 48px); border-top: 2px dashed rgba(118, 94, 50, 0.45); transform: scaleX(0); transform-origin: 0 50%; transition: transform 1.05s ease; }
.itin.is-open .step:not(:last-child) .step__bar::after { transform: scaleX(1); }
.itin.is-open .step:nth-child(1) .step__bar::after { transition-delay: 0.40s; }
.itin.is-open .step:nth-child(2) .step__bar::after { transition-delay: 0.70s; }
.itin.is-open .step:nth-child(3) .step__bar::after { transition-delay: 1.00s; }
.itin.is-open .step:nth-child(4) .step__bar::after { transition-delay: 1.30s; }
.step__n { position: relative; z-index: var(--z-base); display: grid; place-items: center; width: 38px; height: 38px; border-radius: 50%; background: linear-gradient(170deg, #23422f, #18382b); color: var(--gold-bright); font-family: var(--serif); font-weight: 700; font-size: 1.08rem; box-shadow: 0 0 0 5px var(--paper-top); }
.step h4 { margin: 11px 0 0; text-align: center; font-size: clamp(1.04rem, 1.18vw, 1.2rem); line-height: 1.22; color: var(--paper-heading); }
.step p { margin: 6px 0 0; text-align: center; font-size: 0.93rem; line-height: 1.46; color: var(--paper-soft); }
/* Marks the stop where the full-day route stops being walkable from the centre. */
.step__transport { display: inline-flex; align-items: center; gap: 6px; align-self: center; margin: 9px 0 0; padding: 4px 11px; border: 1px solid rgba(158, 108, 36, 0.42); border-radius: 999px; background: rgba(255, 255, 255, 0.5); color: var(--bronze); font-size: 0.76rem; font-weight: 700; letter-spacing: 0.04em; }
.step__transport svg { width: 13px; height: 13px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }

/* Reduced motion: state changes are immediate, never absent. */
@media (prefers-reduced-motion: reduce) {
    .itin__panel, .itin__panel > div, .itin__chev, .step, .step__bar::after { transition: none; }
    .step { opacity: 1; transform: none; }
    .itin .step:not(:last-child) .step__bar::after { transform: scaleX(1); }
}


/* --- Practical: one full-width dark band, no split columns, no accordion. --- */
/* "Plan your visit": heading + lede, then Getting here (3 equal cards), then Practical
   details (a fixed tablist over ONE permanent cream panel — switching topics swaps the
   panel's content, it never expands/collapses/moves anything around it), then one
   closing "Once you arrive" bar. No map of any kind. */
/* The #practical-only scroll-margin-top that used to live here is now the generic
   `.band[id]` rule — same intent, every band, one place. An ID selector here would out-rank
   it and silently reintroduce the 4px drift. */
.planv__head .h-section { margin: 14px 0 0; }
.planv__head .lede { max-width: 62ch; }
.planv__sub { display: flex; align-items: center; gap: 10px; margin: clamp(24px, 2.4vw, 34px) 0 0; color: var(--gold-warm); font-family: var(--sans); font-size: 0.8rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; }
.planv__sub:first-of-type { margin-top: clamp(30px, 3vw, 40px); }
.planv__sub svg { width: 18px; height: 18px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }

/* Getting here — equal cards, never taller for one topic than another. Icon sits beside the
   title (not stacked above it), so the card opens straight into its two lines of copy instead
   of spending a row on vertical icon clearance.
   Adaptive by mode count — reusable, not page-specific: driven by data-count on .ways3 (1/2/3),
   so a place with a railway (3 modes) and a place without one (2 modes, like Beaumaris) both
   render as equal, FULL-WIDTH tracks filling the whole row — never a dead empty column and
   never an arbitrarily narrowed one either. A previous revision capped data-count="2"/"1" at a
   fixed max-width (760px / 420px), which read as correct in isolation but, inside the page's
   much wider .stage, left the exact "dead space on the right" gap this component exists to
   avoid — the cards must fill the row at every count, not just avoid a missing column. */
.ways3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(14px, 1.6vw, 22px); margin: 16px 0 0; }
.ways3[data-count="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.ways3[data-count="1"] { grid-template-columns: minmax(0, 1fr); }
.way3 { display: flex; flex-direction: column; min-height: 148px; padding: 27px 18px 25px; border: 1px solid rgba(228, 196, 122, 0.22); border-radius: var(--r-sm); background: rgba(10, 26, 19, 0.4); }
.way3__head { display: flex; align-items: center; gap: 12px; }
.way3__ic { display: grid; place-items: center; width: 38px; height: 38px; flex: 0 0 auto; border-radius: var(--r-sm); border: 1px solid rgba(228, 196, 122, 0.3); background: rgba(10, 26, 19, 0.6); }
.way3__ic svg { width: 19px; height: 19px; color: var(--gold-warm); fill: none; stroke: currentColor; stroke-width: 1.35; stroke-linecap: round; stroke-linejoin: round; }
.way3 h3 { margin: 0; font-size: 1.06rem; color: var(--cream); }
.way3 p { margin: 10px 0 0; font-size: 0.92rem; line-height: 1.5; color: rgba(248, 239, 216, 0.78); }
/* margin-top:auto puts the link on the same baseline in every card regardless of how
   many lines the description above it wrapped to; a card with no link just leaves the
   matching blank space, which is what keeps all three the same height. */
.way3__link { display: inline-flex; align-items: center; gap: 8px; width: 100%; margin: auto 0 0; padding: 14px 0 0; border: 0; border-top: 1px solid rgba(228, 196, 122, 0.18); background: none; color: var(--gold-bright); font: inherit; font-size: 0.86rem; font-weight: 600; text-align: left; text-decoration: none; cursor: pointer; }
.way3__link svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.way3__link:focus-visible { outline: 2px solid var(--gold-warm); outline-offset: 2px; border-radius: 2px; }

/* Practical details — a real WAI-ARIA tablist. Inactive tabs are dark/outlined, the
   active tab is the same cream as the panel it controls. Horizontally scrollable so nothing
   wraps or shrinks past comfort on a narrow screen. */
.ptabs { display: flex; gap: 10px; margin: 16px 0 0; padding-bottom: 4px; overflow-x: auto; }
.ptab { display: inline-flex; align-items: center; gap: 9px; flex: 0 0 auto; min-height: 48px; padding: 0 20px; border: 1px solid rgba(228, 196, 122, 0.32); border-radius: 999px; background: rgba(10, 26, 19, 0.4); color: rgba(248, 239, 216, 0.86); font: inherit; font-size: 0.98rem; font-weight: 600; white-space: nowrap; cursor: pointer; transition: background 0.22s ease, border-color 0.22s ease, color 0.22s ease; }
.ptab svg { width: 17px; height: 17px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.ptab:hover { border-color: var(--gold-warm); }
.ptab:focus-visible { outline: 2px solid var(--gold-warm); outline-offset: 2px; }
.ptab[aria-selected="true"] { background: linear-gradient(172deg, var(--paper-top), var(--paper)); border-color: rgba(215, 178, 113, 0.7); color: var(--paper-heading); }

/* The ONE permanent panel. min-height (260px) is measured so the two-column row still
   comfortably fits the tallest topic's content, so swapping topics never noticeably
   resizes the box. This is a fixed measurement, not a formula, because the content is
   fixed too: verify against real content, don't invent headroom for hypothetical growth. */
/* display:flex (not a plain block) is load-bearing: a min-height on a block parent does
   NOT stretch a block child to fill it, so without this the visible .ptab-panel would sit
   at its own natural (varying) content height at the TOP of the box, leaving the source
   link's position drifting between topics. As a flex container with one visible item,
   align-items:stretch (the default) stretches that item to the full 260px, every topic. */
.ptab-panels { position: relative; display: flex; min-height: 260px; margin: 16px 0 0; padding: clamp(20px, 2vw, 26px) clamp(20px, 2.1vw, 28px); border-radius: var(--r-md); background: linear-gradient(172deg, var(--paper-top), var(--paper)); color: var(--paper-ink); }
/* LEFT ~35% identity + summary + (optional) ONE bottom-anchored slot: either the source
   link or the verification note, whichever the topic has (never both) — its margin-top:
   auto is what anchors it to the bottom of the stretched grid row, landing on the same
   baseline in every topic that has one, because the row height itself is stable (see
   .ptab-panels min-height above). RIGHT ~65% adaptive facts, stretched via flex + grid-
   auto-rows so the cards fill the row's full height instead of sitting at the top of it.
   [hidden] must win here: an author `display` declaration at equal specificity beats the
   UA stylesheet's `[hidden] { display: none }`, so without this the inactive panels stay
   laid out (display:grid) despite the `hidden` attribute — exactly the bug that produced
   all five panels stacked at once. */
.ptab-panel { display: grid; flex: 1 1 auto; grid-template-columns: minmax(0, 35%) minmax(0, 1fr); gap: clamp(20px, 2.2vw, 28px); }
.ptab-panel[hidden] { display: none; }
.ptab-panel__left { display: flex; flex-direction: column; }
.ptab-panel__head { display: flex; align-items: center; gap: 12px; }
.ptab-panel__ic { display: grid; place-items: center; width: 40px; height: 40px; flex: 0 0 auto; border-radius: 50%; border: 2px solid var(--bronze); color: var(--bronze); background: rgba(255, 255, 255, 0.4); }
.ptab-panel__ic svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.ptab-panel__head h3 { font-family: var(--serif); font-size: clamp(1.2rem, 1.4vw, 1.42rem); font-weight: 600; color: var(--paper-heading); }
.ptab-panel__sum { margin: 12px 0 0; font-size: 0.94rem; line-height: 1.5; color: var(--paper-soft); }
.ptab-panel a { color: var(--bronze); }
.ptab-panel__link, .ptab-panel__left .ptab-panel__note { margin-top: auto; }
.ptab-panel__link { display: inline-flex; align-items: center; gap: 8px; width: fit-content; padding-top: 13px; border-top: 1px solid rgba(118, 94, 50, 0.16); font-size: 0.88rem; font-weight: 600; text-decoration: none; }
.ptab-panel__link svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.ptab-panel__link:focus-visible { outline: 2px solid var(--bronze); outline-offset: 2px; }
/* Mobile-only disclosure toggle for the fact cards (see the max-width:760px block). Hidden
   on desktop/tablet, where the facts always show; injected by JS, so no-JS never shows it. */
.ptab-panel__disclosure { display: none; }

.ptab-panel__right { display: flex; flex-direction: column; padding-left: clamp(20px, 2.2vw, 28px); border-left: 1px solid rgba(118, 94, 50, 0.16); }
/* The adaptive fact grid: the layout is a deterministic function of a declared count
   class (.pfacts--0…4), not a live formula — the content here is hand-authored and fixed,
   so a small, explicit rule per count is simpler and more robust than a generic n-column
   algorithm (and trivially extends if a future topic needs .pfacts--1).
   Named .pfacts/.pfact, NOT .facts/.fact — this file already has an unrelated four-up
   ".facts" snapshot-stats component (village population/elevation, line ~389); reusing
   that name here would silently overwrite it via the cascade.
   flex:1 1 auto makes the grid consume the full stretched height of .ptab-panel__right;
   grid-auto-rows:1fr then splits that height EVENLY across however many rows the count
   produces — 2 rows for facts--4 (2×2) and facts--3 (1 full-width + 2 equal), 1 row for
   facts--2 (two equal cards sharing the one row) and facts--0/1 (a single row that IS the
   full height). Same two declarations serve every count; only the column count and the
   3-case's spanning first child change per class. */
.pfacts { display: grid; flex: 1 1 auto; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: 1fr; gap: 10px; }
.pfacts--0, .pfacts--1 { grid-template-columns: 1fr; }
.pfacts--3 .pfact:first-child { grid-column: 1 / -1; }
/* justify-content:center (the column flex-container's main axis) vertically centres the
   label+value group inside whatever height grid-auto-rows:1fr gives the card — never
   pinned to the top — while align-items stays at its default stretch/flex-start, so text
   itself stays left-aligned regardless of card height. Applies to every card equally; the
   single-fact and zero-fact ("Good to know") feature block gets no separate treatment for
   this any more, it was already just inheriting the same rule. */
.pfact { display: flex; flex-direction: column; justify-content: center; gap: 5px; padding: 12px 14px; border: 1px solid rgba(118, 94, 50, 0.16); border-radius: var(--r-sm); background: rgba(255, 255, 255, 0.32); }
.pfact__label { display: flex; align-items: center; gap: 7px; font-size: 0.76rem; font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase; color: var(--bronze); }
.pfact__label svg { width: 15px; height: 15px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.pfact__val { margin: 0; font-size: 0.89rem; line-height: 1.42; color: var(--paper-soft); }
/* The single-fact and zero-fact ("Good to know") case still earns extra visual weight by
   spanning the full row width — that's the only thing distinct about it now. */
.pfact--feature { grid-column: 1 / -1; border-color: rgba(215, 178, 113, 0.45); background: rgba(215, 178, 113, 0.16); }
.pfact--feature .pfact__label { color: var(--paper-heading); }

/* Lives at the bottom of .ptab-panel__left now (Accessibility, Dogs), not inside the
   facts column — a topic has a link OR a note, never both, so the two share one anchored
   bottom slot and the right-hand facts column is purely the adaptive grid, free to use
   its full stretched height instead of giving some of it up to a note underneath. */
.ptab-panel__note { display: flex; align-items: flex-start; gap: 9px; margin: 0; padding: 10px 12px; border-radius: var(--r-sm); background: rgba(118, 94, 50, 0.08); font-size: 0.81rem; line-height: 1.42; font-style: italic; color: var(--paper-mute); }
.ptab-panel__note svg { width: 15px; height: 15px; margin-top: 1px; flex: 0 0 auto; font-style: normal; color: var(--bronze); fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
/* No JS: the tablist is inert, so all five topics read as one stacked, fully legible
   sequence rather than losing four of them to a dead control. The four panels after
   Parking AUTHOR `hidden` (so the tabbed initial state needs no JS to be correct), which
   the global [hidden] rule enforces with !important — the display override below carries
   equal importance, higher specificity and a later position, so it wins exactly when
   .no-js is on the root. Same contract as the catalogue grid, which renders without JS. */
html.no-js .ptab-panel[hidden] { display: grid !important; }
/* And the container must stack them: its flex row is a one-visible-panel device — five
   visible panels in a row crush each other. The fixed min-height is likewise meaningless
   for a stacked sequence. */
html.no-js .ptab-panels { display: block; min-height: 0; }
html.no-js .ptab-panel:not(:first-child) { margin-top: 24px; padding-top: 24px; border-top: 1px dashed rgba(118, 94, 50, 0.3); }

.planv__arrive { position: relative; isolation: isolate; display: grid; grid-template-columns: 26px minmax(0, 1fr) auto; align-items: center; gap: 16px; margin: clamp(22px, 2.2vw, 30px) 0 0; padding: clamp(12px, 1.3vw, 16px) clamp(20px, 2vw, 28px); border: 1px solid rgba(228, 196, 122, 0.26); border-radius: var(--r-md); overflow: hidden; }
.planv__arrive > svg:first-child { width: 24px; height: 24px; color: var(--gold-warm); fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round; }
.planv__arrive b { display: block; font-family: var(--serif); font-size: 1.14rem; font-weight: 600; color: var(--cream); }
.planv__arrive p { margin: 5px 0 0; font-size: 0.94rem; line-height: 1.5; color: rgba(248, 239, 216, 0.8); }
.planv__arrive .skyline { width: clamp(120px, 14vw, 190px); height: 46px; color: rgba(228, 196, 122, 0.4); fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round; }
@media (prefers-reduced-motion: reduce) { .ptab { transition: none; } }

/* =====================================================================================
   CHAPTER 5 — CONTINUE: full-bleed image band closing the page into the footer.
   ===================================================================================== */
.next { position: relative; isolation: isolate; padding: clamp(96px, 9vw, 150px) var(--gutter) clamp(56px, 5vw, 78px); overflow: hidden; }
.next__media { position: absolute; inset: 0; z-index: -2; }
.next__media img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 38%; }
/* Weighted toward the base so the closing band settles into the footer instead of leaving
   a bright strip of background photograph beneath the CTAs. */
.next__scrim { position: absolute; inset: 0; z-index: -1; background: linear-gradient(180deg, rgba(14, 28, 21, 0.96) 0%, rgba(15, 30, 22, 0.8) 32%, rgba(14, 27, 20, 0.9) 72%, rgba(15, 31, 23, 0.98) 100%); }
.next__head { max-width: 56ch; margin: 0 0 clamp(28px, 3.2vw, 46px); }
.nextgrid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: clamp(14px, 1.6vw, 24px); }
.ncard { display: flex; flex-direction: column; overflow: hidden; border-radius: var(--r-md); background: rgba(10, 26, 19, 0.72); border: 1px solid rgba(228, 196, 122, 0.26); text-decoration: none; color: inherit; transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1), border-color 0.3s, box-shadow 0.3s; }
.ncard:hover { transform: translateY(-4px); border-color: rgba(228, 196, 122, 0.55); box-shadow: 0 24px 48px rgba(4, 10, 7, 0.55); }
.ncard:focus-visible { outline: 2px solid var(--gold-warm); outline-offset: 3px; }
.ncard__media { aspect-ratio: 16 / 11; overflow: hidden; background: #14291f; }
.ncard__media img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1); }
.ncard:hover .ncard__media img { transform: scale(1.05); }
.ncard__body { flex: 1 1 auto; display: flex; flex-direction: column; padding: clamp(18px, 1.6vw, 24px); }
.ncard h3 { font-size: clamp(1.18rem, 1.36vw, 1.38rem); line-height: 1.18; color: var(--cream); }
.ncard p { margin: 12px 0 0; font-size: 0.98rem; line-height: 1.55; color: rgba(248, 239, 216, 0.74); }
.ncard__d { display: inline-flex; align-items: center; gap: 8px; margin: auto 0 0; padding: 16px 0 0; color: rgba(236, 199, 110, 0.88); font-size: 0.9rem; }
.ncard__d svg { width: 15px; height: 15px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
@media (prefers-reduced-motion: reduce) { .ncard, .ncard__media img { transition: none; } .ncard:hover { transform: none; } .ncard:hover .ncard__media img { transform: none; } }


/* =====================================================================================
   CLOSING · ATLAS MAP BAND — the page's anchor back to the map.
   Structure, class contract and responsive behaviour are the canonical location-page
   preview (see pages/locations/conwy/place-guide/swallow-falls.html); only the surface
   tokens are this page's own. .location-map-preview__map / .preview-marker* class names
   are emitted by assets/location-map-preview.js — do not rename them here.
   Separate from #orient's orientation map: different root, different script, no shared state.
   ===================================================================================== */
.map-banner { position: relative; display: grid; grid-template-columns: minmax(260px, 0.9fr) minmax(0, 1.1fr); align-items: center; overflow: hidden; width: 80%; max-width: 80%; margin: 0 auto; border: 1px solid rgba(228, 196, 122, 0.28); border-radius: var(--r-md); background: linear-gradient(158deg, #1d4133, #0f2119); box-shadow: 0 24px 56px rgba(4, 10, 7, 0.45); transition: border-color 0.28s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.28s cubic-bezier(0.22, 1, 0.36, 1); }
.map-banner:hover, .map-banner:focus-within { border-color: rgba(228, 196, 122, 0.55); box-shadow: 0 28px 64px rgba(4, 10, 7, 0.58); }
.map-banner__visual { position: relative; align-self: stretch; min-height: 320px; overflow: hidden; background: linear-gradient(150deg, #1d2d37, #14232b 72%); border-right: 1px solid rgba(228, 196, 122, 0.18); }
.location-map-preview__map { position: absolute; inset: 0; }
/* Pure picture-map: no logo, no attribution chrome on the preview. */
.location-map-preview__map .mapboxgl-ctrl-logo, .location-map-preview__map .mapboxgl-ctrl-attrib { display: none; }
/* Whole-card click-through; the CTA button sits above it on z-index. */
.map-preview-overlay { position: absolute; inset: 0; z-index: 5; text-decoration: none; }
.map-preview-overlay:focus-visible { outline: 2px solid var(--gold-warm); outline-offset: -2px; }
.preview-marker { display: flex; align-items: center; gap: 8px; pointer-events: none; }
.preview-marker__dot { display: block; flex: 0 0 auto; width: 14px; height: 14px; border: 2px solid rgba(247, 237, 214, 0.9); border-radius: 50%; background: var(--gold-warm); box-shadow: 0 0 14px rgba(239, 180, 91, 0.95), 0 0 0 7px rgba(232, 168, 76, 0.18); }
.preview-marker__label { padding: 3px 10px; border: 1px solid rgba(221, 192, 129, 0.36); border-radius: 999px; background: rgba(8, 20, 15, 0.8); color: rgba(247, 237, 214, 0.92); font-family: var(--serif); font-size: 0.78rem; font-style: italic; white-space: nowrap; text-shadow: 0 1px 6px rgba(0, 0, 0, 0.5); }
.map-banner__content { padding: clamp(26px, 2.6vw, 42px); }
.map-banner__content h2 { margin: 0; color: var(--cream); }
.map-banner__content p { margin: 14px 0 0; max-width: 60ch; color: rgba(248, 239, 216, 0.78); }
.map-banner__content .btn { position: relative; z-index: 6; margin-top: clamp(20px, 1.8vw, 26px); }
.map-banner__content .map-banner__note { margin-top: 12px; color: rgba(248, 239, 216, 0.5); font-size: 0.86rem; }
/* Save control — same shared saved-places-ui.js component and quiet-link skin as the
   mountain-trail cohort's .mapview__save (assets/saved-places.css). Needs its own
   position + z-index above .map-preview-overlay (5), same reason .btn above does: the
   whole-card click-through overlay would otherwise swallow the click as navigation. */
.map-banner__save { display: block; position: relative; z-index: 6; margin-top: 14px; }
.map-banner__save .atlas-save__primary { min-height: 36px; padding: 0 2px; border: 0; background: transparent; box-shadow: none; color: rgba(248, 239, 216, 0.7); font-weight: 500; font-size: 0.88rem; }
.map-banner__save .atlas-save__primary:hover,
.map-banner__save .atlas-save__primary:focus-visible { background: transparent; color: var(--gold-warm); transform: none; }
.map-banner__save.is-saved .atlas-save__primary { border: 0; background: transparent; color: var(--gold-warm); }
.map-banner__save .atlas-save__icon { width: 13px; height: 16px; }
.map-banner__save:not(.is-saved) .atlas-save__label::after { content: " to My Places"; }
@media (prefers-reduced-motion: reduce) { .map-banner { transition: none; } }

/* =====================================================================================
   CLOSING · FINAL STATEMENT — image-led sign-off. Same media + scrim idiom as .next
   above it, centred and stripped back to one line and two CTAs.
   ===================================================================================== */
/* Top padding deliberately smaller than bottom used to read backwards: #map (directly above,
   with its own --band-pad bottom padding of up to 84px) and this section's top padding were
   both full "chapter breathing room" amounts, stacking into ~230px of dead space between the
   map banner and the closing kicker — fixed by trimming top here (#map keeps its own bottom
   padding; only the *sum* is trimmed, and only here).
   The bottom value was left at the same "chapter breathing room" size (up to 148px) as if
   another full band followed — but .foot is a slim sign-off bar, not a band, so that padding
   was the entire ~130px gap between the CTA row and the footer. Retuned to a real "end of
   page" amount instead: enough to separate the CTAs from the footer rule, not enough to look
   like a missing section. No negative margin involved — this is still plain padding. */
.close { position: relative; isolation: isolate; padding: clamp(48px, 5vw, 84px) var(--gutter) clamp(36px, 3.5vw, 56px); overflow: hidden; text-align: center; }
.close__media { position: absolute; inset: 0; z-index: -2; }
.close__media img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 54%; }
.close__scrim { position: absolute; inset: 0; z-index: -1; background: linear-gradient(180deg, rgba(12, 25, 18, 0.92) 0%, rgba(13, 26, 19, 0.72) 46%, rgba(11, 23, 17, 0.95) 100%); }
.close .kicker { justify-content: center; }
/* The closing eyebrow drops the generic .kicker::before rule (the short leading line) — this
   final statement is centred and self-contained, not a rule leading into a left-aligned block
   like the other three .kicker sections on the page. Scoped to .close only: chapter markers and
   every other .kicker (Practical, "Use ... as a base", "On the Atlas") keep their line. */
.close .kicker::before { content: none; }
.close h2 { max-width: 22ch; margin: 0 auto; color: var(--cream); }
.close .lede { margin-left: auto; margin-right: auto; }
.close__ctas { display: flex; flex-wrap: wrap; justify-content: center; gap: 14px; margin: clamp(26px, 2.6vw, 38px) 0 0; }

/* =====================================================================================
   FOOTER + MOBILE NAV
   ===================================================================================== */
/* A quiet sign-off line, not a section — the same register as the canonical micro-footer on
   swallow-falls.html. It sat too lit and too separate: a gold rule and a lighter plate made it
   read as one more band after the closing statement. Now it continues the closing band's own
   bottom tone, with a hairline cream rule instead of a gold one, and type that recedes.
   Touch targets stay 46px: quieter is a colour/tone change, never a smaller tap area. */
.foot { padding: 8px var(--gutter) calc(8px + env(safe-area-inset-bottom)); background: #0b1811; border-top: 1px solid rgba(248, 239, 216, 0.07); }
/* Text was 0.38 = 3.26:1, links 0.55 = 5.49:1. The text failed AA outright. Raising the
   text alone would have flattened the link/non-link distinction to nothing, so both move:
   the footer still recedes as a whole, and the links still read as the more prominent of
   the two. Quieter is a tone relationship, not an accessibility discount. */
.foot__in { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 4px 22px; width: var(--stage); margin: 0 auto; color: rgba(248, 239, 216, 0.56); font-size: 0.82rem; letter-spacing: 0.01em; }
.foot__in > a { display: inline-flex; align-items: center; min-height: 46px; color: rgba(248, 239, 216, 0.74); text-decoration: none; transition: color 0.2s ease; }
.foot__in > a:hover { color: rgba(248, 239, 216, 0.88); }
.foot__in > a:focus-visible { outline: 2px solid var(--gold-warm); outline-offset: 3px; border-radius: 2px; }
@media (prefers-reduced-motion: reduce) { .foot__in > a, .credits > summary { transition: none; } }
.credits { flex: 0 1 auto; }
/* Tracks .foot__in > a — it sits in that row and must read as the same kind of control. */
.credits > summary { display: inline-flex; align-items: center; min-height: 46px; color: rgba(248, 239, 216, 0.74); font-size: 0.82rem; cursor: pointer; list-style: none; transition: color 0.2s ease; }
.credits > summary::-webkit-details-marker { display: none; }
.credits > summary:hover, .credits[open] > summary { color: rgba(248, 239, 216, 0.88); }
.credits > summary:focus-visible { outline: 2px solid var(--gold-warm); outline-offset: 3px; border-radius: 2px; }
/* Open: the credits take a full-width line inside the footer. Without the column/centre
   the trigger jumps to the far left edge while the row above stays centred. */
.credits[open] { flex: 1 1 100%; display: flex; flex-direction: column; align-items: center; }
.credits__b { margin: 14px 0 0; padding: 20px 22px; border: 1px solid rgba(228, 196, 122, 0.2); border-radius: var(--r-sm); background: rgba(8, 20, 15, 0.5); text-align: left; }
.credits__b > p:first-child { margin: 0 0 12px; color: rgba(248, 239, 216, 0.66); font-size: 0.84rem; line-height: 1.55; }
.credits__b ul { margin: 0; padding: 0 0 0 19px; color: rgba(248, 239, 216, 0.62); font-size: 0.82rem; line-height: 1.66; }
.credits__b li { margin: 0 0 6px; }
.credits__b strong { color: rgba(248, 239, 216, 0.84); font-weight: 600; }
.credits__b a { color: rgba(236, 199, 110, 0.88); }
.credits__sub { margin: 16px 0 8px; color: rgba(236, 199, 110, 0.8); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; }
.credits__note { margin: 14px 0 0; color: rgba(248, 239, 216, 0.55); font-size: 0.8rem; font-style: italic; }

.mininav { position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-nav); display: none; gap: 4px; padding: 6px 8px calc(6px + env(safe-area-inset-bottom)); background: rgba(16, 30, 23, 0.95); backdrop-filter: blur(12px); border-top: 1px solid rgba(228, 196, 122, 0.2); }
.mininav a { flex: 1 1 0; display: inline-flex; align-items: center; justify-content: center; min-height: 46px; padding: 0 4px; border-radius: 10px; color: rgba(248, 239, 216, 0.74); text-decoration: none; font-size: 0.82rem; text-align: center; }
.mininav a.is-current { background: rgba(228, 196, 122, 0.16); color: var(--gold-bright); font-weight: 600; }
.mininav a:focus-visible { outline: 2px solid var(--gold-warm); outline-offset: -2px; }
/* The mini-nav takes over at the SAME width the top nav leaves (its display:none lives in
   the topnav block near the top of the sheet) — 901-1080px previously had no section
   navigation at all. This override must sit AFTER the .mininav base rule above to win. */
@media (max-width: 1080px) {
    .mininav { display: flex; }
    body { padding-bottom: 64px; }
}

/* =====================================================================================
   LOCAL FLAVOURS — shared gallery lightbox integration.
   The component (assets/gallery-lightbox.js) is reused unmodified: it finds [data-gallery],
   collects .gallery-item cells, and appends its own .gallery-item__trigger to each. Only
   two things are page-level:
   ===================================================================================== */
/* 1. Backdrop. The shared default is a near-opaque rgba(8,18,14,0.96) — correct for the
      mountain pages, but this page already has a modal idiom (the catalogue .cdlg) that
      dims and BLURS. Matching it keeps one overlay language on one page rather than two. */
.lightbox {
    background: rgba(12, 22, 16, 0.72);
    backdrop-filter: blur(9px);
    -webkit-backdrop-filter: blur(9px);
}
/* The shared caption hardcodes Georgia (correct for pages on the system serif stack). This
   page loads Playfair as --serif, so the overlay would otherwise be the one place the page
   changes typeface mid-journey. */
.lightbox__caption { font-family: var(--serif); }
/* The three theme plates and the street photograph are already positioned, clipped boxes,
   so they need nothing beyond the class — but be explicit that the trigger has a box to
   fill, since an unpositioned parent would let it escape to the nearest positioned ancestor. */
.lifecat__img, .life__fig, .life__thumbs li { position: relative; }
/* The lightbox trigger fills each heritage thumb; without an absolutely-filled figure it
   would escape to the nearest positioned ancestor. */
.life__thumbs li > .gallery-item { position: absolute; inset: 0; margin: 0; }
/* Matches the affordance the catalogue cards already use, so a hoverable image reads the
   same way in both chapters. */
.lifecat__img img, .life__thumbs img { transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1); }
.lifecat__img:hover img, .life__thumbs li:hover img { transform: scale(1.04); }
@media (prefers-reduced-motion: reduce) {
    .lifecat__img img, .life__thumbs img { transition: none; }
    .lifecat__img:hover img, .life__thumbs li:hover img { transform: none; }
}

/* =====================================================================================
   RESPONSIVE — deliberately redesigned per band, not a shrunk desktop.
   ===================================================================================== */
@media (max-width: 1280px) {
    .guidestrip__inner { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    .guidestrip__head { grid-column: 1 / -1; }
    .sig, .catgrid, .nextgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    /* Floor raised 380 -> 430px (2026-07-27), same reason as the 400px phone floor below:
       in the two-column band the card is at its narrowest around 820px, where 40vw resolves
       under the floor, so the floor is what actually applies. At 380px the caption was taking
       51% of the card; 430px brings it to ~45% and keeps the photograph dominant
       (town-village-contract.md §13c-iv). The ceiling is unchanged. */
    .sigcard { min-height: clamp(430px, 40vw, 460px); }
    .orient { grid-template-columns: minmax(0, 1.35fr) minmax(330px, 1fr); }
    /* The 21/27/52 mins (260/340) stop fitting below ~1360px — relax the ratio a step
       rather than let the three-part composition overflow the stage. */
    .life__top { grid-template-columns: minmax(230px, 24fr) minmax(280px, 29fr) minmax(0, 47fr); }
    .life__fig { min-height: clamp(420px, 42vw, 560px); }
    /* The six unequal tracks stop working here: the note and quote take full rows and the
       four photographs share one equal row — a controlled multi-row layout, not tiny tiles. */
    .life__foot { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    .life__note { grid-column: 1 / -1; }
    .life__pull { grid-column: 1 / -1; padding: clamp(18px, 2vw, 26px) 0 0; border-left: 0; border-top: 1px solid rgba(228, 196, 122, 0.3); }
}
@media (max-width: 1080px) {
    /* The old `.hero__inner { padding-bottom: clamp(150px, 18vh, 210px) }` lived here.
       It shrank the hero's reserve WITHOUT shrinking .guidestrip's pull, which is
       precisely what drove the clear space negative and produced the measured 15px
       CTA overlap at 820x1000. The tuck is one token now, so tablet needs no override
       at all — clearance is --strip-clear at every width. */
    .snapshot__top, .story, .life__top { grid-template-columns: 1fr; }
    /* Stacked: the photograph becomes a banner above the copy, so the left-edge feather is
       replaced by a bottom-edge one. */
    .snapshot__top { display: flex; flex-direction: column-reverse; }
    .snapshot__copy { max-width: 100%; padding-top: 0; padding-bottom: var(--snap-pad); }
    .snapshot__media { min-height: 0; aspect-ratio: 16 / 9; }
    .snapshot__media::after { background: linear-gradient(180deg, transparent 46%, rgba(22, 41, 30, 0.55) 76%, var(--snap-bg) 100%); }
    .story__fig { min-height: 420px; }
    /* Tablet: a deliberate staged layout, not shrunken desktop columns. Lead + photograph
       pair on one row and keep the photograph dominant; the grouped panel drops below at
       full width, where the four-across card grid finally has room to breathe. */
    .life__top { grid-template-columns: minmax(0, 0.86fr) minmax(0, 1fr); }
    /* Full stage width on its own row here, so neither the margin that sets it apart from
       the pair nor the stretch that matches the photograph's foot has any job left. */
    .life__right { grid-column: 1 / -1; margin-left: 0; align-self: auto; }
    .lifepanel__head--sub { margin-top: clamp(20px, 1.9vw, 28px); }
    .life__fig { min-height: clamp(360px, 42vw, 460px); }
    .facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .secthead { grid-template-columns: 1fr; align-items: start; }
    /* Stacked: heading, map, then a HORIZONTAL numbered selector, then the active
       area's detail. Ten full cards stacked would be most of a screen each; a swipeable
       row of numbered chips keeps the whole index reachable in one gesture and the map
       usable above it. The index is DOM-ordered after the feature (so no-JS reads
       feature-then-list); order:-1 puts it first visually here. */
    .orient { grid-template-columns: 1fr; }
    /* The count-based map heights are a desktop device — on one column the map gets a
       single mobile height regardless of how many areas there are. */
    .orient-map { min-height: 440px; }
    /* The desktop cap is a two-column device; stacked, the panel is free to be as tall
       as its content (capping it here would clip the active-area feature). */
    .orient-panel { gap: 12px; max-height: none; }
    /* Explicitly back to flex: the desktop two-column GRID must not survive here. */
    .orient-cards { order: -1; display: flex; flex: 0 0 auto; min-width: 0; flex-direction: row; overflow-x: auto; overflow-y: hidden; gap: 8px; padding-bottom: 4px; scroll-snap-type: x proximity; }
    .orient-card { display: flex; align-items: center; gap: 9px; flex: 0 0 auto; scroll-snap-align: start; padding: 8px 14px 8px 10px; border-color: rgba(228, 196, 122, 0.24); border-radius: 999px; }
    /* Chips are one line: the desktop two-line wrap would make the selector ragged. */
    .orient-card__label { display: block; white-space: nowrap; font-size: 0.9rem; -webkit-line-clamp: none; line-clamp: none; overflow: visible; }
    .orient-card__cat, .orient-card__chev { display: none; }
    /* Same stable frame, one step shorter for the narrower column. */
    .orient-feature__media { height: 190px; }
}
@media (max-width: 900px) {
    /* .mininav display + body padding moved to the 1080px query — see the topnav swap. */
    /* Expanded route becomes a swipeable journey — one full stop plus a slice of the next,
       so the five-stop sequence still reads as a sequence without a screen of stacking. */
    .itin.is-open { grid-template-columns: 1fr; }
    .itin.is-open .itin__btn { min-height: 170px; padding: clamp(20px, 3vw, 26px); }
    .steps { display: flex; grid-template-columns: none; overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding-left: clamp(18px, 1.7vw, 26px); overscroll-behavior-x: contain; }
    .step { flex: 0 0 76%; scroll-snap-align: start; }
    /* Collapsed ribbon keeps every part readable, stacked rather than squeezed. */
    .itin__desc { flex: 1 1 100%; }
    .itin__dots { margin-left: 0; }
    /* Still three across — the cream stack is full stage width here, so ~240px columns are
       comfortable and collapsing to one would triple the section's height for no gain. */
    .life__lead { padding: clamp(22px, 3vw, 30px); }
    .life__lead h3 { font-size: clamp(1.8rem, 4vw, 2.3rem); }
}
@media (max-width: 760px) {
    /* --chapter-pad stays at the 48px floor here so the marker sits the same 48-64px below
       the surface boundary on a phone as it does on a desktop; only --band-pad drops. */
    :root { --band-pad: clamp(32px, 6vw, 48px); --chapter-pad: clamp(48px, 7.6vw, 64px); }
    /* The marker is wayfinding, not chrome — at phone width it gets a shorter rule and
       tighter type. The 24px gap to the heading is held, so the two still read as one unit. */
    .chaptermark { gap: 9px; margin-bottom: 24px; font-size: 0.74rem; letter-spacing: 0.12em; }
    .chaptermark::before { width: 26px; }
    .life__top { grid-template-columns: 1fr; }
    /* A portrait crop at 390px would be ~500px tall before any copy — 4/3 keeps the
       photograph prominent without a screen-and-a-half of image. */
    /* height:auto matters — a leftover fixed height plus aspect-ratio transfers a 640px
       minimum WIDTH to the figure, which then overflows the stage. */
    .life__fig { align-self: auto; height: auto; min-height: 0; aspect-ratio: 4 / 3; border-radius: var(--r-plate); }
    /* Two across, not one: four stacked cards would be a screen and a half of image, and at
       390px a half-width plate is still ~165px wide — legible, and half the section height. */
    .lifecats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
    /* Three entries stacked, same reasoning as .rhythm below: a 2-up leaves a lone orphan
       on its own row, which reads as an accident rather than a layout. */
    .lifecats[data-count="3"] { grid-template-columns: 1fr; }
    .lifecat h3 { min-height: 0; }
    /* Two-up and unstretched here — the reserved paragraph height would only be a gap. */
    .lifecat p { min-height: 0; }
    /* Three entries stacked: a 2-up leaves an orphan, and the vertical rules read as
       clutter once the row wraps. Rows separate the entries instead. */
    .rhythm { grid-template-columns: 1fr; gap: 18px; }
    .rhythm > div { padding-left: 0; border-left: 0; }
    .rhythm dt { min-height: 0; }
    /* Stacked: the lead and the photograph are no longer side by side, so the joined
       inner edge that pairs them on wider screens has to come off. */
    .life__lead { border-radius: var(--r-plate); border-right: 1px solid rgba(228, 196, 122, 0.28); }
    .life__right { margin-left: 0; }
    /* Foot panel stacks: editorial note (full width) first, the four photographs as a clean
       2x2 grid, quote (full width) last — never one row of ~90px-wide tiles. */
    .life__foot { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .life__thumbs li { min-height: clamp(150px, 40vw, 200px); }
    /* The catalogue media keeps its 3:2 ratio here rather than flattening to 21/9: one
       consistent frame across all breakpoints is what holds the seven entries together as a
       set, and at seven cards (down from eight) the extra column height is affordable. */
    .ncard__media { aspect-ratio: 16 / 9; }
    .snapshot__media { aspect-ratio: 3 / 2; }
    .story__fig { min-height: 340px; }
    .catcard__body { padding: 17px; }
    .catcard__meta { padding-top: 14px; }
    .hero { min-height: min(82vh, 720px); }
    .hero h1 { max-width: 100%; }
    .hero__facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px 0; }
    .hero__facts > li:nth-child(3) { border-left: 0; padding-left: 0; }
    .hero__ctas .btn { width: 100%; }
    /* A shallower tuck on a phone. One variable — both the strip's pull and the hero's
       reserve follow it, so the clearance is unchanged and cannot go negative. */
    :root { --strip-pull: clamp(100px, 16vh, 130px); }
    /* The 0.9x desktop-only narrowing (rolled out from the Beaumaris pilot) is undone here —
       a phone's guidestrip already has minimal side margin; shrinking it further would waste
       width that matters more on a narrow screen. */
    .guidestrip__inner { width: var(--stage); grid-template-columns: repeat(2, minmax(0, 1fr)); padding: 18px; }
    .sig, .catgrid, .nextgrid { grid-template-columns: 1fr; }
    /* The overlay becomes a near-full-screen sheet. .cdlg__inner was already a stacked
       flex column (media then content, in DOM order) at every width — only the frame
       itself needs to go edge-to-edge and square off its corners here; the media keeps
       its one consistent ratio rather than switching to a different mobile crop. */
    .cdlg { width: 100%; max-width: 100%; max-height: 100%; height: 100%; margin: 0; }
    .cdlg__inner { max-height: 100%; height: 100%; border-radius: 0; }
    .cdlg__col { padding: 22px 20px 30px; }
    /* Why stop here / Good to know stack instead of sitting side by side once a column
       would be too narrow to read comfortably. */
    .cdlg__split, .cdlg__split[data-count="1"] { grid-template-columns: 1fr; }
    /* A clean 2-up grid at every count rather than the desktop's exact N columns — and,
       specifically for 3 visible facts, the odd one out spans both columns instead of
       leaving a half-empty row (2 facts and 4 facts both divide evenly already). Targeting
       `.cdlg__fact--last-visible` here, not `:last-child`: the dl always keeps the four
       legacy route-fact slots as its final DOM children too (just permanently hidden for
       these seven entries), so `:last-child` matches one of THOSE, not whichever visible
       fact is actually last — the class is stamped onto the true last visible row by JS. */
    .cdlg__facts, .cdlg__facts[data-count="2"], .cdlg__facts[data-count="3"], .cdlg__facts[data-count="4"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .cdlg__facts[data-count="3"] .cdlg__fact--last-visible { grid-column: 1 / -1; }
    .ways3, .ways3[data-count="2"], .ways3[data-count="1"] { grid-template-columns: 1fr; max-width: none; }
    .ways3 .way3 { min-height: 0; }
    /* The decorative skyline has no room at phone width — kept, it squeezed the sentence
       into a ~150px column wrapping over six lines. */
    .planv__arrive { grid-template-columns: 26px minmax(0, 1fr); }
    .planv__arrive .skyline { display: none; }
    /* Stack left (identity/summary/link-or-note) above right (facts) — a 35/65 split has
       no room to breathe at phone width. The desktop min-height (260px) is a two-column
       device; once the halves stack there's no shared row left to stretch, so it's
       replaced with a small content-driven floor instead of a matched fixed height — each
       topic is simply as tall as its own stacked content, and small differences between
       topics here are expected, normal mobile behaviour rather than a bug to hide. */
    .ptab-panels { min-height: 170px; }
    .ptab-panel { grid-template-columns: 1fr; gap: 18px; }
    .ptab-panel__right { padding-left: 0; padding-top: 16px; border-left: 0; border-top: 1px solid rgba(118, 94, 50, 0.16); }
    /* The fact cards collapse behind an explicit disclosure so a phone shows the summary (and
       any warning / verification link, which stay in __left above and are never hidden here)
       first, not a tall stack. Default collapsed; the [data-facts-open] attr only exists once
       JS has injected the button, so no-JS keeps the facts visible. */
    .ptab-panel__disclosure { display: inline-flex; align-items: center; gap: 8px; width: fit-content; margin-bottom: 16px; padding: 10px 16px; border: 1px solid var(--bronze-soft); border-radius: 999px; background: rgba(255, 255, 255, 0.42); color: var(--bronze); font: inherit; font-size: 0.86rem; font-weight: 700; cursor: pointer; }
    .ptab-panel__disclosure:focus-visible { outline: 2px solid var(--bronze); outline-offset: 2px; }
    .ptab-panel__disclosure svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1); }
    .ptab-panel__disclosure[aria-expanded="true"] svg { transform: rotate(180deg); }
    .ptab-panel__right[data-facts-open="false"] .pfacts { display: none; }
    .ptab-panel__right[data-facts-open="false"] { padding-bottom: 0; }
    @media (prefers-reduced-motion: reduce) { .ptab-panel__disclosure svg { transition: none; } }
    /* 272px left the photograph almost entirely behind the scrim and the copy; 340px restored
       a band of image; 400px is where it landed once the copy itself was fixed (2026-07-27).
       A defining-experience card is one photograph and one promise (town-village-contract.md
       §13c-iv), so the binding constraint is no longer "fit the copy" but "let the photograph
       lead". On a migrated page this puts the whole text block at ~41-43% of the card.
       CENTRALISED AFTER TESTING UNMIGRATED PAGES, not assumed safe: measured at 360px on
       llanrwst, colwyn-bay (Conwy) and beaumaris-town, moelfre (Anglesey), whose long
       pre-migration copy is still in place. Every one improved — text block 94%->80% and
       87%->74% — with no card overflowing and no text reaching the top edge. It does not
       *fix* them (only shorter copy does that), but it makes them strictly better meanwhile. */
    .sigcard { min-height: 400px; }
    /* NO LINE-CLAMP HERE — deliberately. A `-webkit-line-clamp: 2` rule used to sit on this
       selector, on the reasoning that "the full text is one tap away in the detail dialog".
       That reasoning was false: a .sigcard opens the CATALOGUE card's dialog, and the dialog
       summary is read from the .catcard (`fromCard(card, 'summary')` in town-guide.js), never
       from the sigcard — so the clamped-away half of a teaser existed nowhere else in the DOM
       and could not be recovered by any interaction. Measured 2026-07-27: it hid 49-66% of
       EVERY teaser (103/103 cards, 26/26 shared-layer pages) at 390px and 360px.
       It also bought essentially nothing: with the clamp removed, cards grow 0-3px, because
       the .sigcard min-height floor below already reserves more room than the full teaser
       needs. Half the copy was being hidden to save at most three pixels.
       Visible destination copy ends naturally at every supported width — see
       town-village-contract.md §13c-ii. Do not reintroduce a clamp here; if a teaser does not
       fit, write it shorter.
       SCRIM: two jobs in tension — keep the photograph clear, and keep the caption legible over
       whatever happens to be under it. A single gentle ramp cannot do both. This one is
       flat-and-clear across the top ~42% and then steep, so the text band sits on a reliably
       dark base whatever the frame behind it. An earlier, lighter attempt (0.16 at 44%) made
       the images read beautifully and left the copy barely legible over pale subjects — that
       failure is only visible in a render, never in a computed style.
       Also tested against unmigrated long copy at 360px before centralising: llanrwst went
       3.89:1 (failing) -> 5.65:1, beaumaris-town 6.17 -> 7.15. colwyn-bay is 3.67 -> 3.59, i.e.
       it was ALREADY failing AA before this change and stays marginally worse until its copy is
       migrated — a pre-existing defect this ramp neither causes nor fixes. */
    .sigcard::after { background: linear-gradient(180deg, rgba(10, 22, 16, 0) 0%, rgba(10, 22, 16, 0.04) 42%, rgba(9, 20, 15, 0.66) 58%, rgba(9, 20, 15, 0.95) 100%); }
    .sigcard__cue { margin-top: 10px; }
    .next { padding-top: clamp(60px, 8vw, 96px); padding-bottom: clamp(44px, 4.4vw, 62px); }
    .facts { grid-template-columns: 1fr; }
    /* Map above copy: a 260px map column beside text is unreadable at phone width. The
       vertical divider becomes a horizontal one, matching the canonical preview. */
    .map-banner { grid-template-columns: 1fr; width: 100%; max-width: 100%; }
    /* Taller on a phone so the wider mobile crop isn't squeezed into a letterbox — the pin
       and its label need vertical room to sit clear of the frame edges. */
    .map-banner__visual { min-height: 300px; border-right: 0; border-bottom: 1px solid rgba(228, 196, 122, 0.18); }
    /* Same fix as the desktop rule, at phone/tablet's own top-padding size: top stays generous,
       bottom drops to a proportionate end-of-page amount instead of matching it. */
    .close { padding: clamp(58px, 8vw, 96px) var(--gutter) clamp(32px, 6vw, 48px); }
    /* Two full-width CTAs stacked — matching .hero__ctas .btn at this width. */
    .close__ctas .btn { width: 100%; }
    .story__quote p { font-size: 1.24rem; }
}
@media (max-width: 420px) {
    .hero__facts { grid-template-columns: 1fr; }
    .hero__facts > li { border-left: 0; padding-left: 0; }
    .guidestrip__inner { grid-template-columns: 1fr; }
    .gcard__img { aspect-ratio: 21 / 9; }
    /* Below ~430px the feature's side-by-side image and copy leave the text in a
       ~150px column; stacking gives the description a full-width measure. */
    .orient-feature__media { height: 170px; }
    .orient-card { gap: 9px; padding: 7px 13px 7px 8px; }
}

/* =====================================================================================
   MOBILE MOMENTS SELECTOR  (<= 760px)

   Rolled out to the shared layer 2026-08-03 from the approved Llandudno and Beaumaris
   pilots, which were byte-identical apart from their headers. It replaces both — no page
   carries a copy of any of this, and none needs page-specific wiring: assets/town-guide.js
   finds `#experiences .sig` on its own and does nothing where that markup is absent.

   WHAT IT DOES. Below 760px — the breakpoint at which .sig is already a single column —
   the three or four tall stacked Moment cards become ONE visible card plus a compact
   selector panel beneath it. Every town/village page carries 3 or 4 Moments, so every one
   of them gets a selector; the row count is read from the DOM and is never a fixed number.

   WHAT IT DOES NOT DO. It authors no copy, image, number or link, and duplicates no Moment
   content: the visible card IS one of the page's own .sigcard elements, so it keeps the
   .sigcard__trigger that initCatalogue() injects and opens the same catalogue dialog it
   always did. The selector rows are real buttons OUTSIDE the cards, so activating one
   cannot reach that trigger.

   Above 760px none of these rules exist, so the authored Moment grid is untouched at every
   desktop and tablet width. With JavaScript off neither html class is stamped, so the
   original stacked cards remain.
   ===================================================================================== */

/* Mobile-only, declared outside the query so it is hidden at every width the block below
   does not explicitly cover. */
.momsel { display: none; }

@media (max-width: 760px) {
    /* `tg-moments` is stamped by the pre-paint bootstrap, before this section can paint, so
       the mobile default is card 1 alone and the stack is never briefly visible (a
       reduced-motion visitor gets an opaque .reveal immediately, so it really would be).
       `tg-moments-on` arrives with the selector and hands "which card" from CSS
       (:first-child) to the [data-moment-on] attribute. */
    html.tg-moments .sig > .sigcard:not(:first-child) { display: none; }
    html.tg-moments-on .sig > .sigcard { display: none; }
    html.tg-moments-on .sig > .sigcard[data-moment-on] { display: flex; animation: momsel-in 0.26s ease-out; }
    @keyframes momsel-in { from { opacity: 0.3; } to { opacity: 1; } }
    /* ONE panel with the rows inside it — an editorial contents index, not a stack of cards.
       The panel owns the border, radius and clipping, so first/last rows need no radius. */
    html.tg-moments-on .sig > .momsel { display: grid; gap: 0; overflow: hidden; border: 1px solid var(--green-rule); border-radius: 14px; background: var(--green-card); }
    /* One row = thumb, number, title (the only flexible track, so a long title wraps rather
       than pushing the chevron off-screen), chevron. No clamp: nothing here is truncated.
       Rows are flush and borderless; a hairline divider separates them. */
    .momsel__row { display: grid; grid-template-columns: 42px auto minmax(0, 1fr) 15px; align-items: center; gap: 10px; width: 100%; padding: 8px 12px 8px 8px; border: 0; border-top: 1px solid var(--green-rule); background: transparent; color: rgba(248, 239, 216, 0.82); font: inherit; text-align: left; cursor: pointer; transition: background 0.25s ease, color 0.25s ease; }
    .momsel__row:first-child { border-top: 0; }
    /* Active: flat cream row, dark ink-green title, bronze numeral (5.2:1 on cream), slim gold
       rule at the left edge. INSET, so selecting a row never moves its content or changes its
       height; the two dividers touching it are hidden so the cream reads as one plate, not a pill.

       --ink (#1a2c21) rather than --paper-heading (#2b3022), rolled out from the approved
       Colwyn Bay contrast pilot 2026-08-03. The row was never failing on contrast — it measured
       11.84:1 — but it read pale, because Playfair Display is a high-contrast didone whose
       hairlines are genuinely thin at 15.2px on a light ground. The fix is mostly stroke mass
       (the 600 below); this is the smaller colour move on top, and it takes the title to 12.85:1.

       ONE declaration for two elements: .momsel__c is stroke: currentColor, so the chevron
       takes this colour with the title and needs no rule of its own. (The pilot used two
       separate overrides only because it was winning from a later stylesheet.) */
    .momsel__row[aria-pressed="true"] { background: var(--cream); color: var(--ink); box-shadow: inset 3px 0 0 var(--bronze); }
    .momsel__row[aria-pressed="true"], .momsel__row[aria-pressed="true"] + .momsel__row { border-top-color: transparent; }
    /* Inset ring: the panel clips overflow, so an outward offset would be cut off. */
    .momsel__row:focus-visible { outline: 2px solid var(--gold-bright); outline-offset: -2px; }
    .momsel__thumb { width: 42px; height: 42px; border-radius: 8px; overflow: hidden; background: var(--surface-deep); }
    .momsel__thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
    .momsel__n { font-family: var(--serif); font-size: 0.9rem; font-weight: 700; color: var(--gold); letter-spacing: 0.04em; }
    /* opacity pinned, not reduced: the numeral was already solid --bronze at 5.18:1 and there
       was never a dimming rule to remove. Stated so the active row's three foreground elements
       are declared together and a later dimming rule cannot quietly reach it. Deliberately left
       at the token value rather than darkened — a bespoke bronze is how a palette drifts. */
    .momsel__row[aria-pressed="true"] .momsel__n { color: var(--bronze); opacity: 1; }
    .momsel__t { font-family: var(--serif); font-size: 0.95rem; line-height: 1.24; }
    /* 600, not 400 — a real Playfair cut (every town page requests 400/500/600/700), so never a
       synthesised faux-bold, which would thicken unevenly and widen the line more than the true
       weight does. This is the change that actually stopped the active row reading pale. */
    .momsel__row[aria-pressed="true"] .momsel__t { font-weight: 600; }
    .momsel__c { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; opacity: 0.5; }
    /* Full opacity and a little more stroke mass, so a 15px glyph holds its own beside a
       600-weight line of type. The colour arrives from the row via currentColor, and the SVG box
       stays 15x15, so the fourth grid track is unchanged. */
    .momsel__row[aria-pressed="true"] .momsel__c { opacity: 1; stroke-width: 2.4; }
    @media (prefers-reduced-motion: reduce) { html.tg-moments-on .sig > .sigcard[data-moment-on] { animation: none; } .momsel__row { transition: none; } }
}

/* =====================================================================================
   MOBILE CATALOGUE PROGRESSIVE DISCLOSURE  (<= 760px)

   Rolled out to the shared layer 2026-08-03 from the approved Llandudno and Beaumaris
   pilots. Entirely separate from the Moments block above — different namespace, no shared
   rule, no shared state; neither knows the other exists.

   WHAT IT DOES. Below 760px — the breakpoint at which .catgrid is already a single column —
   a catalogue of 5 or more cards opens on its first three, with the remainder and the
   trailing .catfoot guidance behind one toggle. A catalogue of 4 or fewer is left alone
   entirely: no toggle, every card visible. Which pages fall which side is decided by
   assets/town-guide.js from the real card count; no count is written here or on any page.

   WHAT IT DOES NOT DO. It restyles no card: no image, crop, dimension, title, tag, summary,
   meta, cue, border, type, spacing or hover rule is touched, and no card is cloned. The
   only thing under this block's control is whether a card is in the layout.

   TWO GATES, BOTH REQUIRED, so nothing can leak:
     html.tg-catmore-on                 added only AFTER the toggle is in the DOM, so a
                                        script that never ran leaves every card visible.
     [data-catalogue-collapsed="true"]  the current state.
   Above 760px NONE of these rules exist, so desktop cannot inherit a stale collapsed state
   however the visitor got there — no resize handler is needed to protect it.
   ===================================================================================== */

/* Mobile-only, for the same reason as .momsel above. */
.catmore { display: none; }

@media (max-width: 760px) {
    /* Collapsed: the remainder cards and the trailing guidance leave the layout, the
       accessibility tree and the tab order together. display:none, not [hidden] on the
       elements, so the state lives in ONE attribute that simply stops applying above
       this width. */
    html.tg-catmore-on #catalogue[data-catalogue-collapsed="true"] .catmore-extra { display: none; }

    /* Full catalogue width, rectangular (never a pill), and built from the catalogue's own
       label vocabulary: .catcard__cue's uppercase bronze sans, on .btn--onpaper's restrained
       paper surface. No gradient, no shadow, no sticky or floating position. */
    html.tg-catmore-on .catmore { display: flex; align-items: center; justify-content: center; gap: 10px; width: 100%; min-height: 52px; margin: clamp(20px, 2.1vw, 30px) 0 0; padding: 15px 20px; border: 1px solid var(--bronze-soft); border-radius: 12px; background: rgba(255, 255, 255, 0.4); color: var(--bronze); font-family: var(--sans); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; text-align: center; cursor: pointer; transition: background 0.25s ease, border-color 0.25s ease; }
    .catmore:hover { background: rgba(255, 255, 255, 0.72); border-color: var(--bronze); }
    .catmore:focus-visible { outline: 2px solid var(--bronze); outline-offset: 2px; }
    /* Small directional chevron; one icon, rotated, so the two states cannot disagree. */
    .catmore svg { flex: none; width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; transition: transform 0.25s ease; }
    .catmore[aria-expanded="true"] svg { transform: rotate(180deg); }

    /* Newly revealed content settles in on opacity alone. The catalogue's HEIGHT is never
       animated — a five-card slide would be several screens of movement. */
    html.tg-catmore-on #catalogue[data-catalogue-collapsed="false"] .catmore-extra { animation: catmore-in 0.22s ease-out; }
    @keyframes catmore-in { from { opacity: 0; } to { opacity: 1; } }

    @media (prefers-reduced-motion: reduce) {
        /* Matching the specificity of the rule that SET the transition — a bare .catmore
           here loses to html.tg-catmore-on .catmore above and silently does nothing. */
        html.tg-catmore-on .catmore, .catmore svg { transition: none; }
        html.tg-catmore-on #catalogue[data-catalogue-collapsed="false"] .catmore-extra { animation: none; }
    }
}
