@font-face {
  font-family: 'Stencil Std';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  /* The actual font the old Wix site used ("Stencil", i.e. Adobe's
     Stencil Std) -- not Google's "Allerta Stencil", which looked
     visibly different. Self-hosted from an .otf the user sourced
     directly (fontpalace.com), not Adobe Fonts -- avoids depending on
     an ongoing Creative Cloud subscription for a font used on a
     self-hosted site. */
  src: url('../fonts/StencilStd.otf') format('opentype');
}

@font-face {
  font-family: 'Space Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  /* Self-hosted (SIL OFL, from the official googlefonts/spacemono repo)
     rather than a Google Fonts CDN link -- same reasoning as Stencil Std
     above: no third-party request, and no dependency on an external
     service staying up. Replaces Consolas/Courier New as the small-label
     mono font -- those are Windows-only names, so phones (no Consolas
     installed) were silently falling through to whatever thin generic
     monospace the OS ships, which read as "typewritery" and low-weight
     against the warm-paper background. Tried JetBrains Mono here first --
     its narrow, code-optimized proportions read as *more* condensed on a
     real phone screen, not less, so swapped for Space Mono, which is
     deliberately wide-set/display-weight rather than density-optimized. */
  src: url('../fonts/SpaceMono-Regular.ttf') format('truetype');
}

/* ---------- Tokens: light is the deliberate, primary identity ---------- */
:root {
  --paper: #FDF4DC;
  --paper-dim: #F7E9C8;
  --paper-line: #E6D3A1;
  --ink: #442211;
  --ink-soft: #7A5233;
  --accent: #94A487;
  --accent-soft: #E9E6C9;
  --accent-text: #5C6E52;

  /* Fixed (not overridden in dark mode): the user wants ONE consistent sage
     and ONE consistent warm-white regardless of theme -- e.g. the "Plan je
     bezoek" card should look identical in light/dark, not shift with
     --accent's per-theme value. --ink-fixed is for text sitting on top of
     --accent-solid specifically, since dark ink has the best contrast
     against that sage in both themes (dark mode's --ink is light, so it
     can't be reused there). */
  --accent-solid: #94A487;
  --warm-white: #FDF4DC;
  --ink-fixed: #442211;
  --danger: #a33;
  /* Medium-tier attention, between --ink-soft (plain info) and --danger
     (a real problem) -- e.g. "Geen foto's" in the admin entries list is
     worth noticing but isn't broken/hidden the way the danger-red notes
     are. A warm amber sits tonally next to this palette's existing cream/
     gold family rather than introducing an unrelated hue. */
  --warning: #946200;
  --focus: var(--ink);

  --font-stencil: 'Stencil Std', Impact, 'Arial Narrow', sans-serif;
  --font-display: 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, serif;
  --font-body: 'Trebuchet MS', Verdana, system-ui, sans-serif;
  --font-mono: 'Space Mono', Consolas, 'Courier New', monospace;
}
/* Dark is an explicit, opt-in choice via the header toggle (not auto
   system-preference) — a genuine neutral-dark, not the brand brown used
   as a giant field. */
:root[data-theme="dark"] {
  --paper: #211D18;
  --paper-dim: #2B2621;
  --paper-line: #3D362C;
  --ink: #F3ECD9;
  --ink-soft: #C7B99C;
  --accent: #A9BC9C;
  --accent-soft: #56604C;
  --accent-text: #A9BC9C;
  --warning: #e0a940;
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
/* Sticky footer, site-wide: body is a full-viewport-height flex column
   (header / .site-main / footer), and .site-main (wrapping {% block
   content %} in layout.html) is the one item that grows via flex:1 --
   so a short page's footer still sits at the bottom of the viewport
   instead of floating up right under the content (most visible on the
   entry detail page, which has little content and no fixed-height hero
   to pad it out). Doesn't interact with Home's position:fixed hero image
   (Gotcha #6) -- fixed positioning cares about transform/filter/
   will-change on an ancestor, not display:flex, and .site-main sets
   neither. */
body {
  display: flex; flex-direction: column; min-height: 100vh;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
.site-main { flex: 1 0 auto; }
a { color: inherit; }
img { max-width: 100%; display: block; }
::selection { background: var(--accent); color: var(--paper); }
/* --focus is just var(--ink) (defined once in :root) rather than its own
   hardcoded hex per theme -- a plain CSS variable reference re-resolves
   against whichever theme is active without needing a duplicate dark-mode
   override. Used to be a hardcoded blue that didn't fit the warm-paper/
   sage palette anywhere it showed up (default focus rings site-wide, e.g.
   the Dieren & planten search field). */
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* min-width:0 matters wherever .wrap sits inside a flex container (e.g.
   .controls) -- without it, a flex item won't shrink below its content's
   natural min-width, which forced real page-level horizontal overflow on
   narrow screens (mobile browsers respond to that by zooming the whole
   page out to fit, instead of reflowing). Harmless everywhere .wrap is a
   plain block (the vast majority of its uses). */
.wrap { max-width: 1280px; margin: 0 auto; padding: 0 clamp(1rem, 4vw, 2.5rem); min-width: 0; }

/* A single, deliberately plain hairline -- an earlier version used a 9px
   repeating-dash pattern that (combined with header's own border-bottom)
   read as a "double line" with visible gap before whatever came next.
   One thin line, same hairline color used for every other border on the
   site, sitting flush against its neighbors with zero extra spacing. */
.barred { height: 2px; background: var(--paper-line); }

/* ---------- header ---------- */
header.site { position: relative; padding: .85rem 0; background: var(--paper); }
/* auto/1fr/auto so the nav truly centers in the space between the
   wordmark and the action icons, regardless of either's width -- not a
   plain space-between, which would push nav off-center whenever those
   two ends differ in width. */
header.site .wrap { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 1rem; }
.wordmark { display: flex; align-items: center; gap: .85rem; text-decoration: none; justify-self: start; }

/* The egg logo is a fixed-color badge (sage/brown, like --accent-solid
   elsewhere) so it doesn't need a separate light/dark recolor -- one
   image works unchanged in both themes. */
.logo-mark { display: block; position: relative; width: 42px; height: 42px; flex: none; }
.logo-mark img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }

.wordmark .wordmark-text { font-family: var(--font-stencil); font-weight: 400; font-size: 1.2rem; letter-spacing: .02em; color: var(--ink); text-transform: uppercase; white-space: nowrap; }
nav.main { justify-self: center; }
/* Fixed 1rem at the low end (unchanged right at the header's nav
   breakpoint, so this doesn't force the hamburger to kick in any
   earlier), growing on wider screens to use the extra real estate
   instead of leaving the centered nav looking cramped in a mostly-empty
   wide header. Tightened from an earlier 1.6rem floor (and the link
   font from 1.08rem to 1rem below) specifically so the six nav labels
   still fit single-row at common "snap browser to half a 1920px
   screen" widths (~960px) -- see the breakpoint comment near the
   @media rule for the actual numbers. */
nav.main ul { list-style: none; display: flex; gap: clamp(1rem, 2vw, 3rem); margin: 0; padding: 0; }
nav.main a {
  font: inherit; text-decoration: none; white-space: nowrap;
  font-size: 1rem; color: var(--ink-soft); border-bottom: 2px solid transparent; padding: 0 0 3px;
}
nav.main a:hover, nav.main a.current { color: var(--ink); border-color: var(--accent); }
.header-actions { display: flex; align-items: center; gap: .6rem; flex: none; justify-self: end; }

/* Shared circular icon-button treatment for theme/search/hamburger toggles. */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 999px; border: 1px solid var(--paper-line);
  background: var(--paper-dim); cursor: pointer; color: var(--ink); flex: none;
}
.icon-btn svg { width: 17px; height: 17px; }
/* Hidden here (desktop, and mobile before the drawer redesign below) --
   shown only inside the @media (max-width: 930px) block, where nav.main
   becomes a fixed full-height panel that can end up covering the header's
   own #nav-toggle if the page has been scrolled (header isn't
   position:fixed/sticky, so it can scroll out of view entirely) -- this
   button is the drawer's own always-reachable close affordance,
   independent of wherever the header itself currently sits. */
.nav-close { display: none; }

/* Dims the rest of the page behind the mobile nav drawer and the header
   search popover -- see main.js's syncBackdrop(), which toggles this
   class whenever either one opens/closes. Plain opacity/pointer-events
   transition, not display:none, so it can fade instead of snapping. */
.page-backdrop {
  position: fixed; inset: 0; z-index: 28; background: rgba(0,0,0,.45);
  opacity: 0; pointer-events: none; transition: opacity .2s ease;
}
.page-backdrop.open { opacity: 1; pointer-events: auto; }

.theme-toggle .moon { display: none; }
:root[data-theme="dark"] .theme-toggle .sun { display: none; }
:root[data-theme="dark"] .theme-toggle .moon { display: block; }

/* Site-wide search -- searches the catalog, Actueel posts, and a small
   static index of the other pages (see STATIC_PAGES in routes/public.py),
   plus a live/instant suggestions dropdown (main.js fetches /zoeken.json,
   debounced, and renders results here) with a fallback full results page
   at /zoeken on Enter.
   Desktop: position:relative here on purpose, so .nav-search-form's
   right:0 anchors against THIS icon-sized box -- the popout should hang
   directly under the search icon, not the far edge of the whole header
   (an earlier version removed this to fix the mobile bug below, which
   instead anchored the popout to the header's right edge, well past the
   theme toggle -- confirmed by the user, not just a guess).
   Mobile (see the 1050px query): switched to position:static there
   instead, so header.site (the next ancestor up, also position:relative)
   becomes the containing block, which is what the mobile left+right
   full-width anchoring actually needs. */
.nav-search { position: relative; display: flex; align-items: center; }
/* Elevation via shadow only, no border -- an outer card border plus the
   input's own pill border, close together, was reading as a faint extra
   line under the search field.
   Animates via opacity/transform, NOT max-height + overflow:hidden --
   that combination (a clipped, rounded-corner box with an active height
   transition) is a known Chromium compositing trouble spot: a faint
   seam could appear on focus and persist until the browser was forced to
   repaint (e.g. on window blur), rather than a real line drawn by any
   rule here. Dropping the clipping transition avoids relying on a
   repaint to clear a rendering glitch in the first place. */
.nav-search-form {
  position: absolute; top: 100%; right: 0; z-index: 30;
  width: min(320px, calc(100vw - 2rem));
  background: var(--paper); border-radius: 10px; padding: .6rem;
  box-shadow: 0 8px 24px rgba(0,0,0,.15);
  opacity: 0; visibility: hidden; transform: translateY(-6px); pointer-events: none;
  transition: opacity .15s ease, transform .15s ease, visibility 0s linear .15s;
}
.nav-search-form.open {
  opacity: 1; visibility: visible; transform: translateY(0); pointer-events: auto;
  transition: opacity .15s ease, transform .15s ease;
}

/* The pill's visual chrome (background/border/radius) lives on this
   wrapper, not the input itself -- the input is transparent on top of it
   so the ghost autocomplete text (a sibling behind it) can show through
   in the not-yet-typed portion. */
.search-field {
  position: relative; display: flex; align-items: center;
  border: 1px solid var(--paper-line); border-radius: 999px; background: var(--paper-dim);
}
/* Own focus style (border + soft halo) instead of the sitewide plain
   outline -- a rectangular outline sitting on a pill-shaped field read as
   the other part of the original "corners" complaint. Already used
   var(--ink), same token --focus above now points at too. */
.search-field:focus-within {
  border-color: var(--ink);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--ink) 25%, transparent);
}
.search-field-icon { position: absolute; left: .8rem; width: 15px; height: 15px; color: var(--ink-soft); pointer-events: none; z-index: 2; }
/* Ghost autocomplete -- shows the typed text invisibly (so its width
   reserves the exact space the real characters occupy) followed by a
   faded suggested completion, sitting behind the real (transparent)
   input. Same font/size/padding/line-height as the input so the two line
   up exactly -- this element used flex + align-items:center to vertically
   center its text, but a plain <input> centers its text via its own
   line-height/UA box model instead, and those two centering methods don't
   necessarily agree on the same pixel, which read as the ghost text
   sitting a little higher than the real typed text. Matching line-height
   on both, with neither using flex centering, keeps them identical. */
.search-ghost {
  position: absolute; inset: 0; z-index: 1;
  padding: .55rem 2.1rem; font-family: var(--font-body); font-size: 1rem; line-height: 1.3;
  white-space: pre; overflow: hidden; pointer-events: none;
}
.search-ghost .ghost-typed { color: transparent; }
.search-ghost .ghost-suffix { color: var(--ink-soft); opacity: .7; }
.nav-search-form input {
  position: relative; z-index: 2;
  width: 100%; font-family: var(--font-body); font-size: 1rem; line-height: 1.3; padding: .55rem 2.1rem;
  border: none; border-radius: 999px; background: transparent; color: var(--ink);
  /* Hand rendering fully to our own CSS -- WebKit/Blink's native
     type="search" chrome (its own cancel-button/decoration internals)
     doesn't respect border-radius, which was the actual source of the
     original "weird white corners" on the pill shape, not just the focus
     ring color. */
  appearance: none; -webkit-appearance: none;
}
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-results-button,
input[type="search"]::-webkit-search-results-decoration { -webkit-appearance: none; appearance: none; }
.nav-search-form input:focus-visible { outline: none; }
.search-clear {
  position: absolute; right: .45rem; z-index: 3; width: 22px; height: 22px; display: flex; align-items: center; justify-content: center;
  border: none; background: none; color: var(--ink-soft); cursor: pointer; padding: 0;
}
.search-clear svg { width: 13px; height: 13px; }
.search-clear[hidden] { display: none; }

.search-suggestions { max-height: 60vh; overflow-y: auto; }
.sugg-group { margin-top: .9rem; }
.sugg-group h4 {
  font-family: var(--font-mono); font-size: .76rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-soft); margin: 0 .3rem .5rem;
}
.sugg-item { display: flex; align-items: center; gap: .7rem; padding: .4rem .3rem; border-radius: 6px; text-decoration: none; color: var(--ink); }
.sugg-item:hover { background: var(--paper-dim); }
.sugg-item img, .sugg-thumb-fallback { width: 40px; height: 40px; object-fit: cover; border-radius: 6px; flex: none; background: var(--accent-soft); }
.sugg-post-text { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.sugg-post-text strong { font-weight: 600; }
.sugg-post-text small { color: var(--ink-soft); font-size: .88rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sugg-submit {
  display: flex; align-items: center; gap: .5rem; margin-top: .9rem; padding: .6rem .3rem 0;
  border-top: 1px solid var(--paper-line); text-decoration: none; color: var(--ink-soft); font-size: .95rem;
}
.sugg-submit svg { width: 14px; height: 14px; }
.sugg-submit:hover { color: var(--ink); }
.sugg-empty { color: var(--ink-soft); font-size: 1rem; margin: .9rem .3rem 0; }

/* Hamburger toggle -- hidden on desktop, shown at the same 800px
   breakpoint used site-wide (see the shared @media block at the bottom). */
.nav-toggle { display: none; }
.nav-toggle .close { display: none; }
.nav-toggle[aria-expanded="true"] .burger { display: none; }
.nav-toggle[aria-expanded="true"] .close { display: block; }

/* ---------- hero (home) ---------- */
.hero { position: relative; min-height: min(80vh, 700px); display: flex; align-items: center; padding: clamp(2rem, 6vw, 3.6rem) 0; overflow: hidden; }
/* Fixed (not absolute) so the photo stays put in the viewport while the
   page scrolls -- a "window into the garden" effect, matching the old
   site (confirmed there via a real scroll-position test: the hero image's
   bounding rect didn't move at all until the page had scrolled almost a
   full viewport height). z-index:-1 puts it behind normal page content,
   so the next section's own opaque background naturally covers it once
   scrolled up over it -- unlike CSS background-attachment:fixed, a fixed
   position on a real element doesn't have the classic iOS Safari bug. */
.hero-bg { position: fixed; inset: 0; z-index: -1; }
.hero-bg img { width: 100%; height: 100%; object-fit: cover; object-position: center 100%; }
.hero-bg::after {
  content: ""; position: absolute; inset: 0;
  /* Lighter than before -- the photo itself (the user's real ShortPixel-
     graded export) is already properly exposed/darkened, so this only
     needs to add enough of a scrim for text legibility, not set the mood
     on its own (that was double-darkening the previous raw source). */
  background: linear-gradient(180deg, rgba(10,16,26,.15) 0%, rgba(10,16,26,.12) 40%, rgba(10,16,26,.5) 100%);
}
.hero .wrap { position: relative; z-index: 1; }
.hero-content { max-width: 960px; margin: 0 auto; text-align: center; }
/* margin-bottom kept equal to the default UA <p> margin it replaces
   (1em at this font-size) so spacing looks unchanged everywhere else --
   only margin-top is zeroed, since the unreset default 1em top margin
   was the actual cause of the map/text misalignment on the bezoek page:
   .contact-cols aligns both columns to the same grid-row start, but the
   text column's first child (this eyebrow) was quietly pushing itself
   down below that line while the map column's first child wasn't. */
.eyebrow { font-family: var(--font-mono); text-transform: uppercase; letter-spacing: .14em; font-size: 1rem; color: var(--accent-text); margin: 0 0 .74rem; }
.hero-kicker { font-family: var(--font-display); font-size: clamp(1.3rem, 2.4vw, 1.6rem); color: var(--warm-white); margin: 0; text-shadow: 0 1px 6px rgba(0,0,0,.4); }
.hero h1 {
  font-family: var(--font-stencil); font-weight: 400; color: var(--warm-white); text-transform: uppercase;
  font-size: clamp(2.6rem, 8vw, 6rem); line-height: 1; margin: .7rem 0 .7rem;
  text-wrap: balance; letter-spacing: .01em; text-shadow: 0 2px 10px rgba(0,0,0,.35);
}
.hero-egg-logo { width: clamp(95px, 10vw, 155px); height: auto; margin: 0 auto .9rem; display: block; }
.hero p.lede { max-width: 46ch; margin: 0 auto 1.5rem; color: var(--warm-white); text-shadow: 0 1px 5px rgba(0,0,0,.4); font-size: 1.15rem; }
.hero-ctas { display: flex; gap: .7rem; flex-wrap: wrap; justify-content: center; margin: 0; }
.hero-ctas .btn { min-width: 190px; justify-content: center; }
.btn {
  display: inline-flex; align-items: center; gap: .5rem;
  background: var(--ink); color: var(--paper);
  font-family: var(--font-body); font-size: 1rem;
  padding: .7rem 1.25rem; border-radius: 3px; text-decoration: none;
  border: 1px solid var(--ink); cursor: pointer;
  transition: background-color .18s ease, border-color .18s ease, color .18s ease, transform .15s ease, box-shadow .15s ease;
}
.btn.ghost { background: transparent; color: var(--ink); border-color: var(--ink); }
.hero .btn.ghost { color: var(--warm-white); border-color: rgba(253,244,220,.75); }
/* Hover, exact spec: light theme -- every button (filled or ghost) turns
   warm-white with brown text, same as the primary button. Dark theme --
   every button instead turns the one fixed sage, always with the dark
   brown (--ink-fixed) text -- the user tried the "invert relative to
   resting color" scheme (light text on filled buttons' sage hover) and
   preferred dark brown text on sage unconditionally, regardless of the
   button's resting color -- except inside "Plan je bezoek", which is
   already that exact sage, so its buttons keep the warm-white hover in
   both themes (otherwise they'd visually vanish into the card). */
.btn:hover, .btn.ghost:hover { background: var(--warm-white); border-color: var(--warm-white); color: var(--ink-fixed); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,.18); }
:root[data-theme="dark"] .btn:hover,
:root[data-theme="dark"] .btn.ghost:hover { background: var(--accent-solid); border-color: var(--accent-solid); color: var(--ink-fixed); }
/* Needs BOTH an unconditional version (to beat .visit's own resting-color
   override in light mode, where no dark-theme rule is competing) AND a
   :root[data-theme="dark"]-prefixed version with even higher specificity
   (to beat the dark-theme .btn:hover rule above, which otherwise wins the
   specificity comparison and turns these buttons green after all --
   the exact bug reported: this exception looked fixed in light mode
   testing, but was never actually specific enough to win in dark mode). */
.visit .btn:hover, .visit .btn.ghost:hover { background: var(--warm-white); border-color: var(--warm-white); color: var(--ink-fixed); }
:root[data-theme="dark"] .visit .btn:hover,
:root[data-theme="dark"] .visit .btn.ghost:hover { background: var(--warm-white); border-color: var(--warm-white); color: var(--ink-fixed); }

/* The opposite problem on the Plan je bezoek / Contact page: --warm-white
   and --paper are the literal same hex in light mode, so the sitewide
   light-mode hover above (which turns every button warm-white) makes
   buttons vanish into this page's plain --paper section background.
   Force the same sage the dark theme already defaults to, in both
   themes here -- dark mode's own default already happens to be this
   exact value, so this only actually changes light mode's result. */
.contact .btn:hover,
.contact .btn.ghost:hover { background: var(--accent-solid); border-color: var(--accent-solid); color: var(--ink-fixed); }

/* ---------- quick links (home) ---------- */
/* Explicit background (not just relying on body's) -- necessary now that
   .hero-bg is a viewport-fixed, negative-z-index layer that would
   otherwise show through any section without its own opaque fill. */
.quicklinks { padding: clamp(1.4rem, 4vw, 2.2rem) 0 1.3rem; background: var(--paper); }
.quicklinks .grid3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.1rem; }
.qcard {
  background: var(--paper-dim); border: 1px solid var(--paper-line); border-radius: 8px;
  overflow: hidden; text-decoration: none; color: var(--ink); display: flex; flex-direction: column;
  transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.qcard:hover { border-color: var(--accent); transform: translateY(-2px); box-shadow: 0 6px 18px rgba(0,0,0,.08); }
.qcard .qphoto { aspect-ratio: 4/3; overflow: hidden; }
.qcard .qphoto img { width: 100%; height: 100%; object-fit: cover; }
.qcard .qbody { padding: 1rem 1.1rem 1.2rem; display: flex; flex-direction: column; gap: .4rem; flex: 1; }
.qcard h3 { font-family: var(--font-display); font-size: 1.3rem; margin: 0; }
.qcard p { margin: 0; font-size: 1rem; color: var(--ink-soft); }
.qcard .arrow { font-size: .84rem; color: var(--accent-text); margin-top: auto; }

/* ---------- plan your visit (home) ---------- */
.visit { padding: 1.3rem 0 clamp(2.2rem, 5vw, 3.4rem); background: var(--paper); }
.visit .box {
  /* Fixed sage/ink (not the theme-relative --accent/--ink tokens) so this
     block looks identical in light and dark mode -- the user explicitly
     wants ONE consistent sage here, not a per-theme variant. */
  background: var(--accent-solid); border-radius: 8px;
  padding: clamp(1.3rem, 4vw, 2rem); display: grid; grid-template-columns: 1.2fr .8fr; gap: 1.5rem; align-items: center;
}
.visit h2 { font-family: var(--font-display); font-size: 1.4rem; margin: 0 0 .5rem; color: var(--ink-fixed); }
.visit ul { list-style: none; margin: 0; padding: 0; font-size: 1rem; color: var(--ink-fixed); display: flex; flex-direction: column; gap: .35rem; }
.visit ul li strong { color: var(--ink-fixed); }
/* width: fit-content shrink-wraps this column to its widest child's natural
   size; align-items: stretch then makes the OTHER (narrower) button match
   that same width -- avoids hand-picking a min-width that can silently
   drift out of sync if either label changes. justify-self: end keeps the
   (now content-sized) column pinned to the box's right edge. */
.visit .cta { display: flex; flex-direction: column; gap: .6rem; align-items: stretch; width: fit-content; justify-self: end; }
.visit .cta .btn { justify-content: center; }
/* Fixed resting colors (not the theme-relative --ink/--paper) so these
   buttons -- not just the card background -- look identical in both
   themes; hover is already handled by the dark-mode .visit exception above. */
.visit .btn { background: var(--ink-fixed); border-color: var(--ink-fixed); color: var(--warm-white); }
.visit .btn.ghost { background: transparent; color: var(--ink-fixed); border-color: var(--ink-fixed); }

/* ---------- page header (ontdek / contact / actueel / sfeer / over ons) ---------- */
.page-head { padding: clamp(1.8rem, 5vw, 2.6rem) 0 1.3rem; }
.page-head h1 { font-family: var(--font-stencil); font-weight: 400; font-size: clamp(1.9rem, 4.4vw, 2.7rem); margin: .4rem 0 .5rem; text-wrap: balance; }
/* :not(.eyebrow) -- otherwise this (higher-specificity than .eyebrow's
   own single-class rule) silently overrode every eyebrow that happens to
   sit inside a .page-head with --ink-soft, so most eyebrows on the site
   rendered brown (or pale tan in dark mode) instead of the intended
   --accent-text green -- only the one eyebrow living outside any
   .page-head (over-ons's "In beeld") ever showed the real color. */
.page-head p:not(.eyebrow) { color: var(--ink-soft); max-width: 60ch; margin: 0; }

/* ---------- controls (ontdek) ---------- */
.controls { padding: 0 0 1.4rem; }
/* A fixed, uniform 3-row structure at every width: search, then Toon, then
   Sorteer+view-toggle sharing the final row -- deliberately the SAME
   grouping from full desktop down to the smallest phone, rather than
   reflowing which controls share a line at different breakpoints. Chosen
   over a more "space-efficient" desktop layout (e.g. search+Toon sharing
   a row when there's room) specifically for simplicity: one layout to
   reason about instead of several tier-dependent ones, matching this
   project's standing preference for boring/low-abstraction code over
   anything clever. The only things that still change by width are Toon's
   own internal rendering (single-line pills vs. a grid, ~540px) and
   Sorteer's label (text vs. icon, ~420px) -- both genuine content-fit
   necessities, not layout choices.
   All four controls are siblings in one flex-wrap row, ordered via `order`
   rather than split across separate row wrappers. .row-break-1/-2 are
   zero-height dummy items whose only job is forcing a line-wrap via
   flex-basis:100% wherever they're ordered to sit -- a plain flex-wrap
   decides which line an item lands on using its *hypothetical* (flex-
   basis) size, before flex-grow is applied, so e.g. two small-basis items
   could still both land on a line whose *grown* content wouldn't actually
   fit together (confirmed happening earlier). Putting flex-basis:100%
   directly on an item "fixes" that but also makes it claim the *entire*
   line for itself, blocking anything from sharing that line with it --
   these break markers force the line-wrap without consuming any width of
   their own, leaving Toon and Sorteer+view free to lay out normally on
   the lines that follow. */
.controls-bar { display: flex; flex-wrap: wrap; align-items: center; column-gap: .8rem; row-gap: 1.1rem; }
.row-break { display: block; flex-basis: 100%; width: 0; height: 0; }
/* A fixed comfortable basis (not flex-grow) so search stays a normal-
   looking search box instead of stretching to fill the whole row now
   that it's always alone on one -- flex-shrink still lets it compress
   down to fit on narrow phones, where it ends up full-width anyway simply
   because there's no room left over once shrunk. */
.search-row { order: 1; display: flex; gap: .6rem; align-items: center; flex: 0 1 450px; min-width: 0; }
.search-row input[type="search"] {
  font-family: var(--font-body); font-size: 1rem; padding: .55rem .8rem;
  border: 1px solid var(--paper-line); border-radius: 5px; background: var(--paper-dim); color: var(--ink);
  min-width: 0; width: 100%;
}
.hint { font-family: var(--font-mono); font-size: .76rem; color: var(--ink-soft); text-transform: uppercase; letter-spacing: .06em; white-space: nowrap; }

.row-break-2 { order: 2; }
/* flex-grow matters even though Toon is always alone on its row: without
   it, .filters-scroll's auto-fit grid was starved of width and collapsed
   to a single column (confirmed -- both .filters and .filters-scroll
   measured exactly 130px instead of filling the row). */
.filters { order: 3; display: flex; gap: .55rem; align-items: center; flex: 1 1 auto; min-width: 0; }
.filters-scroll { display: flex; gap: .55rem; flex-wrap: wrap; min-width: 0; }
.filters button {
  font-family: var(--font-body); font-size: 1rem; background: transparent; flex: none;
  border: 1px solid var(--paper-line); color: var(--ink-soft); padding: .45rem 1rem; border-radius: 999px; cursor: pointer;
}
.filters button svg { width: 14px; height: 14px; vertical-align: -2px; margin-right: .4rem; }
.filters button:hover { border-color: var(--accent); color: var(--ink); }
.filters button[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--paper); }
.filters button.reset { border-style: dashed; }

.row-break-1 { order: 4; }
.sort-select { order: 5; display: flex; align-items: center; gap: .5rem; flex: none; margin-left: 0; }
.sort-select .sort-hint-icon { display: none; width: 16px; height: 16px; color: var(--ink-soft); flex: none; }
.sort-select select {
  font-family: var(--font-body); font-size: 1rem; color: var(--ink); background: var(--paper-dim);
  border: 1px solid var(--paper-line); border-radius: 6px; padding: .5rem .65rem; cursor: pointer;
}
.view-toggle { order: 6; display: inline-flex; border: 1px solid var(--paper-line); border-radius: 6px; overflow: hidden; flex: none; margin-left: auto; }
.view-toggle button { background: var(--paper-dim); border: none; padding: .5rem .65rem; cursor: pointer; color: var(--ink-soft); display: flex; }
.view-toggle button + button { border-left: 1px solid var(--paper-line); }
.view-toggle button svg { width: 16px; height: 16px; }
.view-toggle button[aria-pressed="true"] { background: var(--ink); color: var(--paper); }

.empty-state {
  display: none; text-align: center; padding: 2.5rem 1rem; color: var(--ink-soft);
  border: 1px dashed var(--paper-line); border-radius: 8px; font-size: 1rem;
}
.empty-state.show { display: block; }

/* ---------- search results (/zoeken) ---------- */
.search-section { padding-bottom: 2.4rem; }
.search-section h2 { font-family: var(--font-display); font-size: 1.2rem; margin: 0 0 1rem; }
.search-section .grid, .search-section .posts { padding-bottom: 0; }
.search-pages { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: .7rem; }
.search-pages a {
  display: inline-block; padding: .5rem 1.1rem; border: 1px solid var(--paper-line); border-radius: 999px;
  text-decoration: none; color: var(--ink); background: var(--paper-dim);
}
.search-pages a:hover { border-color: var(--accent); }

/* grid view */
/* minmax raised from 220px to 260px: at this page's ~1200px content width
   that's the difference between 5 columns and 4 -- long unbroken spec
   values (e.g. "Belgie / Frankrijk") were bleeding past the card edge at
   5-up. auto-fill means this single number also governs every narrower
   breakpoint proportionally (4 columns become 3, 2, 1 sooner too) without
   needing separate per-breakpoint rules. If specs still overflow at 4-up,
   the next step is making them break inside (e.g. "Belgie / Frankrijk"
   with real spaces) rather than shrinking the grid further -- deliberately
   not done yet, per the user. */
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 1.1rem; padding-bottom: 3rem; }
.grid .card {
  background: var(--paper-dim); border: 1px solid var(--paper-line); border-radius: 8px;
  overflow: hidden; text-decoration: none; color: var(--ink); display: block;
  transition: transform .15s ease, box-shadow .15s ease;
}
.grid .card:hover { transform: translateY(-2px); box-shadow: 0 6px 18px rgba(0,0,0,.08); }
.grid .card .thumb { position: relative; aspect-ratio: 4/3; background: var(--accent-soft); display: flex; align-items: center; justify-content: center; overflow: hidden; }
.grid .card .thumb img { width: 100%; height: 100%; object-fit: cover; }
.grid .card .thumb svg { width: 46%; height: 46%; color: var(--ink); opacity: .6; }
/* .body is a <span> (inline by default); its former .card-top child was
   display:flex and h3 is a block element, so without an explicit block
   display here the padding never applied around those block-level children
   (a "block-in-inline" quirk) -- confirmed via computed styles, text was
   landing <1px from the card edge despite this padding computing correctly. */
.grid .card .body { display: block; padding: .9rem 1rem 1.1rem; }
/* Floats over the photo instead of sitting in the body below it -- shortens
   the card and reads more like a normal catalog-card badge. Fixed (not
   theme-relative) ink/warm-white so it stays legible over any real photo's
   brightness, not just the light placeholder background entries without a
   photo yet use. Same "outline, solid on hover" language as the entry-
   detail gallery's nav arrows, but lighter (a thin, partly-translucent
   border rather than the arrows' bold 2px) and without dropping the rest
   state's own legibility much -- this pill is conveying real information
   (which category), not just a discoverable nav affordance, so unlike the
   arrows it can't afford to fade very far before it's actually read. */
.card .tag {
  font-family: var(--font-mono); font-size: .7rem; text-transform: uppercase; letter-spacing: .08em;
  color: var(--warm-white); background: rgba(68, 34, 17, .75); border: 1px solid rgba(253, 244, 220, .4);
  border-radius: 999px; padding: .2rem .6rem; white-space: nowrap;
  transition: background-color .18s ease, border-color .18s ease;
}
.grid .card:hover .tag { background: rgba(68, 34, 17, .95); border-color: var(--warm-white); }
.grid .card .thumb .tag { position: absolute; top: .6rem; left: .6rem; }
/* 1.3rem, not the more modest ~1.15rem this used to be -- Space Mono
   below (dl.spec, also 1rem) reads visually *bigger* than an equal-rem
   serif size, thanks to its blocky, high-x-height, all-caps letterforms.
   At 1.15rem the card title was actually losing that comparison (confirmed
   via a zoomed screenshot: "Amrock" read smaller than "HERKOMST" right
   below it, despite being the larger number). dl.spec itself deliberately
   stays at 1rem -- it was already raised there once for its own
   readability -- so the fix is raising the title to properly outrank it,
   not shrinking the spec data back down. */
.card h3 { font-family: var(--font-display); font-size: 1.3rem; margin: 0 0 .25rem; }
dl.spec { margin: .5rem 0 0; font-family: var(--font-mono); font-size: 1rem; color: var(--ink-soft); }
/* Solid, matching the card's own 1px border below (.grid .card) -- a
   dashed rule here read as a second, weaker "almost a border" competing
   with the card outline rather than reading as one consistent line style. */
dl.spec div { display: flex; justify-content: space-between; gap: .5rem; padding: .16rem 0; border-top: 1px solid var(--paper-line); }
dl.spec div:first-child { border-top: none; }
dl.spec dt { text-transform: uppercase; letter-spacing: .04em; opacity: .8; }
/* flex:1/min-width:0 so long values (herkomst especially, e.g. "Klein-Azië
   (Turkije/Griekenland)") actually wrap inside the card instead of forcing
   the row wider -- a flex item's default min-width is auto (its content's
   min-content size), not 0, same underlying bug as this file's other
   min-width:0 notes, just on a flex row here instead of a grid. Paired
   with the break_slashes template filter (adds a <wbr> after every "/"),
   which gives values like "België/Frankrijk" a break point to use even
   though they have no space to wrap at naturally. */
dl.spec dd { margin: 0; text-align: right; flex: 1; min-width: 0; }
.card p.note { font-size: 1rem; color: var(--ink-soft); margin: .5rem 0 0; }
.card.pending .body::after {
  content: "gegevens volgen"; display: inline-block; margin-top: .5rem;
  font-family: var(--font-mono); font-size: .74rem; color: var(--ink-soft); font-style: italic;
}

/* list view */
.grid.view-list { grid-template-columns: 1fr; gap: .6rem; }
.grid.view-list .card { display: flex; align-items: center; gap: 1rem; }
/* A bit bigger than the grid-view thumb math would suggest -- list rows
   have the vertical room to spare, so the photo can carry more weight
   than the 84px it was before. Dialed back down again at the narrow
   (480px) tier below, where that room disappears. */
.grid.view-list .card .thumb { width: 106px; height: 106px; aspect-ratio: auto; flex: none; border-radius: 6px; }
.grid.view-list .card .body { flex: 1; padding: .7rem 1rem .7rem 0; }
/* A real grid (not flex-wrap) so each field lands in a fixed column instead
   of wrapping wherever the previous line happened to run out of room --
   flex-wrap let a long "Herkomst" value shove "Eieren" onto its own
   ragged line while other cards' fields stayed put, so nothing lined up
   row to row. */
.grid.view-list dl.spec {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  column-gap: 1.1rem;
  row-gap: .15rem;
  font-size: 1rem;
}
/* 1fr grid tracks default to a *minimum* size of their content's
   min-content width, not 0 -- so a long unbroken value like "Verenigde
   Staten" could force its whole track wider than its 1fr share and bleed
   the grid past the card edge instead of wrapping, at any viewport width
   (the same class of bug as this file's .wrap-inside-flex min-width:0
   note above). min-width:0 on the grid item lets it actually shrink to
   its assigned track width so long values wrap inside their column
   instead of overflowing it. */
.grid.view-list dl.spec div { min-width: 0; }
/* dt/dd read as "HERKOMST: Verenigde Staten" -- left-aligned and colon-
   joined -- rather than the grid-view rule's space-between/right-align:
   right-aligning here pushed the value flush against the next column's
   label with barely a gap between them. A solid vertical rule (matching
   the card's own border and the grid view's now-solid horizontal ones)
   marks the column boundary instead, since the dt/dd pair itself no
   longer spans full-width to make that boundary visually obvious on its
   own. */
.grid.view-list dl.spec div {
  border-top: none; padding: 0;
  justify-content: flex-start; gap: .3rem;
  border-left: 1px solid var(--paper-line); padding-left: .85rem;
}
.grid.view-list dl.spec div:first-child { border-left: none; padding-left: 0; }
.grid.view-list dl.spec dt::after { content: ":"; }
.grid.view-list dl.spec dd { text-align: left; min-width: 0; }
.grid.view-list h3 { margin: .1rem 0 .15rem; }

/* Medium list-view rows: 4 columns is too tight once the card itself has
   shrunk, so drop to 2 columns -- but keep it 2 columns x 2 rows (not a
   4-row single column yet) by filling column-first instead of the grid
   default row-first. That puts herkomst+gewicht in column 1 and
   eieren+eikleur in column 2, matching the fields' natural pairing
   (origin/size, then egg stats) instead of splitting each pair across
   columns the way row-first fill would. */
@media (max-width: 960px) {
  .grid.view-list dl.spec {
    grid-template-columns: repeat(2, 1fr);
    grid-template-rows: repeat(2, auto);
    grid-auto-flow: column;
  }
  /* The wide tier's "border on everything but the first child" no longer
     matches column boundaries once fields fill column-first -- item 2
     (gewicht) sits directly under item 1 (herkomst) in column 1, not
     beside it, so it shouldn't get column 1's divider. Only items 3-4
     (column 2) do. */
  .grid.view-list dl.spec div { border-left: none; padding-left: 0; }
  /* :where() zeroes this selector's specificity down to match the plain
     rule above, so the narrow tier below -- which needs to override this
     divider back off again -- can win on source order alone rather than
     having to out-specify an :nth-child selector it otherwise has no
     reason to know about. */
  .grid.view-list dl.spec div:where(:nth-child(n+3)) { border-left: 1px solid var(--paper-line); padding-left: .85rem; }
}

/* Narrow phones: even 2 columns crowds "Verenigde Staten"-length values
   against their neighbour, so drop to a single column of 4 rows. Must
   reset grid-auto-flow back to row (the 960px tier's column-fill would
   otherwise still apply here too, since both max-width queries match at
   once and this one loads later in the cascade -- but being explicit
   here rather than relying on load order is what actually keeps this
   correct). */
@media (max-width: 620px) {
  .grid.view-list dl.spec {
    grid-template-columns: 1fr;
    grid-auto-flow: row;
  }
  /* One column means there are no side-by-side fields left to divide
     vertically -- switch the divider to a horizontal rule between stacked
     rows instead, the same solid-border-between-rows treatment the
     grid-view card uses for its own single-column spec list. */
  .grid.view-list dl.spec div {
    border-left: none; padding-left: 0;
    border-top: 1px solid var(--paper-line); padding-top: .15rem;
  }
  .grid.view-list dl.spec div:first-child { border-top: none; padding-top: 0; }
  .grid.view-list .card .thumb { width: 84px; height: 84px; }
}

.load-more { display: flex; justify-content: center; padding: 1.6rem 0 0; }

/* ---------- detail page ---------- */
.breadcrumb { display: inline-flex; align-items: center; gap: .4rem; font-size: 1rem; color: var(--ink-soft); text-decoration: none; }
.breadcrumb:hover { color: var(--ink); }
.breadcrumb svg { width: 14px; height: 14px; }
.detail { padding: 1.2rem 0 3rem; }
.detail-cols { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(1.5rem, 4vw, 3rem); align-items: start; }
/* 1fr grid tracks default to a *minimum* size of their content's
   min-content width, not 0 (same underlying bug as this file's other
   min-width:0 notes). .gallery-thumbs is a non-wrapping flex row of
   fixed-width thumbnails -- entries with many photos/variants (e.g.
   zijdehoen's 9+ thumbnails across 4 color variants) pushed that row's
   min-content width past its fair share of the column, forcing the whole
   gallery column -- and the photo inside it, which is width:100% of that
   column -- wider than on other entries, and squeezing/overflowing the
   specs column next to it. min-width:0 lets both columns actually shrink
   to their assigned 1fr share, so .gallery-thumbs's own overflow-x:auto
   scrolls instead of stretching the column. */
.detail-cols > div { min-width: 0; }
.detail-gallery > .thumb { border-radius: 8px; overflow: hidden; border: 1px solid var(--paper-line); }
/* Same bordered-pill treatment as the admin's .admin-tag (a real badge,
   unlike .eyebrow -- which is a page-level kicker meant to appear once
   above a page's own H1, and reads wrong repeated per-item above every
   entry's own H1: no border/background means no "this is a tagged type"
   signal). Sized up from admin's dense list-row context (.64rem) to this
   page's own prominent placement -- 16px minimum, same as everywhere else. */
.category-tag {
  font-family: var(--font-mono); font-size: 1rem; text-transform: uppercase; letter-spacing: .08em;
  color: var(--accent-text); border: 1px solid var(--accent); border-radius: 999px; padding: .25rem .8rem;
  white-space: nowrap; display: inline-block; margin-bottom: .74rem;
}
.detail-info h1 { font-family: var(--font-display); font-size: 2rem; margin: 0 0 .8rem; }
.detail-info dl.spec { font-size: 1rem; margin-bottom: 1rem; }
.detail-info dl.spec div { padding: .4rem 0; }
/* dd's flex:1/min-width:0 (so long values like Kenmerken wrap instead of
   overflowing) now lives on the base dl.spec dd rule above, shared by
   every context that uses the plain flex-row spec layout. */
.detail-info .desc { color: var(--ink-soft); font-size: 1.05rem; }
.detail-info time { font-family: var(--font-mono); font-size: .74rem; color: var(--accent-text); text-transform: uppercase; letter-spacing: .06em; display: block; margin-bottom: .3rem; }

/* ---------- entry detail: photo gallery (main image + thumbnail strip) ---------- */
.detail-gallery { display: flex; flex-direction: column; gap: .8rem; }
.gallery-main {
  position: relative; border-radius: 8px; overflow: hidden; border: 1px solid var(--paper-line);
  background: var(--paper-dim);
}
.gallery-main img { width: 100%; display: block; aspect-ratio: 4/3; object-fit: cover; cursor: zoom-in; }
/* object-fit:contain (not cover, unlike the photo above) -- a portrait
   phone video shouldn't get cropped to a 4/3 landscape box the way a photo
   can safely be. Letterboxed on black rather than the paper background so
   the letterbox itself doesn't read as a design choice. No zoom-in cursor:
   there's no lightbox for video, native <video controls> is the whole
   interaction. */
.gallery-main video { width: 100%; display: block; aspect-ratio: 4/3; object-fit: contain; background: #000; }
/* post-detail.js toggles the main img/video pair via the `hidden` attribute
   when switching between a photo and a video gallery item -- without this,
   the `display: block` rules above (needed so the *visible* one fills the
   box) outrank the browser's own default `[hidden]{display:none}` rule by
   specificity, so the "hidden" element stayed visible (a broken-image box
   sitting on top of the video, caught by actually loading the page rather
   than just reading the JS). */
.gallery-main img[hidden], .gallery-main video[hidden] { display: none; }

/* ---------- Site-wide: custom play button for paused <video> ---------- */
/* Native browser controls only show a small play icon tucked into the
   bottom bar -- fine when a video autoplays, but once a video opens paused
   (Over Ons's popups, deliberately not autoplaying -- see over-ons.js) a
   video with no motion of its own (e.g. the Radio 2 interview, a static
   photo) reads as broken/stuck rather than "tap to play". This one button,
   injected by main.js next to every <video> on the page, covers all of
   them: Over Ons's inline + popup videos, the Actueel gallery's main video,
   and any [video] BB-tag embed in a bericht body -- all three already sit
   inside a position:relative box (.video-box / .gallery-main /
   .video-lightbox-box). Same circle + CSS-triangle shape as
   .gallery-thumb.has-video's play badge above, scaled up into an actual
   clickable/focusable button instead of a decorative ::before/::after pair. */
.video-play-btn {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 1;
  width: 4.4rem; height: 4.4rem; border-radius: 50%; border: none; cursor: pointer;
  background: rgba(0,0,0,.55); display: flex; align-items: center; justify-content: center;
  transition: opacity .15s ease, background .15s ease;
}
.video-play-btn:hover { background: rgba(0,0,0,.75); }
.video-play-btn .tri {
  width: 0; height: 0; margin-left: .3rem;
  border-style: solid; border-width: .8rem 0 .8rem 1.25rem;
  border-color: transparent transparent transparent var(--warm-white);
}
.video-play-btn.is-hidden { opacity: 0; pointer-events: none; }
/* Only shown next to whichever <video> is actually visible -- .gallery-main
   toggles [hidden] between its photo and video when the user switches
   gallery items, and the button must hide right along with the video
   underneath it instead of floating over the photo. */
video[hidden] + .video-play-btn { display: none; }

/* Dark-brown body / warm-white border+icon / drop shadow so the arrow
   reads clearly against any photo regardless of its own colors -- a sage
   body (tried first) is still a mid-tone pulled from the same garden
   palette (soil/leaves/feathers) as the actual photos, so hue contrast
   alone wasn't reliable; dark-fill-plus-light-icon is lightness contrast,
   which holds up against a much wider range of real photos (the standard
   pattern most photo galleries use for exactly this reason). Fixed (not
   theme-relative) tokens throughout, same reasoning as "Plan je bezoek":
   this sits on top of a photo, so it needs to look the same regardless of
   site theme. Translucent at rest, a bit more visible on hovering the
   picture itself, fully solid when the button itself is hovered/about to
   be clicked. */
.gallery-nav {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
  width: 2.4rem; height: 2.4rem; border-radius: 50%; padding: 0; cursor: pointer;
  background: var(--ink-fixed); border: 2px solid var(--warm-white); color: var(--warm-white);
  box-shadow: 0 2px 10px rgba(0,0,0,.35);
  opacity: .6;
  display: flex; align-items: center; justify-content: center;
  transition: opacity .18s ease, box-shadow .18s ease;
}
.gallery-main:hover .gallery-nav { opacity: .9; }
.gallery-nav:hover { opacity: 1; box-shadow: 0 3px 14px rgba(0,0,0,.5); }
.gallery-nav svg { width: 20px; height: 20px; }
.gallery-nav.prev { left: .7rem; }
/* Reuses ic-arrow-left rather than a second icon -- mirrored for "next". */
.gallery-nav.next { right: .7rem; transform: translateY(-50%) scaleX(-1); }
.gallery-thumbs { display: flex; gap: .6rem; overflow-x: auto; padding-bottom: .2rem; }
/* Desktop-only (mouse/trackpad) scrollbar -- touch scrolling on mobile
   doesn't render a persistent bar at all, so this only matters here.
   scrollbar-width/-color covers Firefox; the ::-webkit- pseudo-elements
   cover Chrome/Edge/Safari. Thin + palette-matched instead of the
   browser-default thick gray bar. */
.gallery-thumbs { scrollbar-width: thin; scrollbar-color: var(--accent) var(--paper-dim); }
.gallery-thumbs::-webkit-scrollbar { height: 7px; }
.gallery-thumbs::-webkit-scrollbar-track { background: var(--paper-dim); border-radius: 999px; }
.gallery-thumbs::-webkit-scrollbar-thumb { background: var(--accent); border-radius: 999px; }
.gallery-thumbs::-webkit-scrollbar-thumb:hover { background: var(--accent-solid); }
.gallery-thumb {
  flex: 0 0 auto; width: 4.5rem; height: 4.5rem; padding: 0; border-radius: 6px; overflow: hidden;
  border: 2px solid transparent; cursor: pointer; background: var(--paper-dim); position: relative;
}
.gallery-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gallery-thumb.current { border-color: var(--ink); }
/* Same play-badge shape as the admin photo-manager's .pthumb-video and the
   Actueel feed's .post-thumb.has-video, sized down for this small strip. */
.gallery-thumb.has-video::before {
  content: ""; position: absolute; top: 50%; left: 50%; width: 1.6rem; height: 1.6rem;
  transform: translate(-50%, -50%); border-radius: 50%; background: rgba(0,0,0,.55);
}
.gallery-thumb.has-video::after {
  /* width/height MUST be 0 for the border-triangle trick below to form a
     clean triangle -- an earlier version left them at the triangle's own
     rough size, which instead drew the triangle around a phantom content
     box, widening the real element past its intended bounds and getting
     silently clipped by the parent's overflow:hidden (looked exactly like
     "the triangle is cut off" in a screenshot, not a rendering glitch). */
  content: ""; position: absolute; top: 50%; left: calc(50% + .1rem); width: 0; height: 0;
  transform: translate(-50%, -50%);
  border-style: solid; border-width: .35rem 0 .35rem .55rem;
  border-color: transparent transparent transparent var(--warm-white);
}

/* ---------- entry detail: fullscreen lightbox, opened by tapping the main photo ---------- */
/* Reuses .gallery-nav for its own prev/next (a position:fixed ancestor is
   still a valid containing block for their position:absolute, so left/
   right/top:50% resolve against the full viewport here instead of
   .gallery-main) -- just bumped up from the inline gallery's hover-to-
   reveal opacity (.6 resting), since there's no separate "picture" to
   hover over in a fullscreen view -- the whole screen already is the
   picture, so the controls need to read clearly without that cue. */
.lightbox {
  position: fixed; inset: 0; z-index: 2000; background: rgba(0,0,0,.92);
  display: flex; align-items: center; justify-content: center;
}
.lightbox[hidden] { display: none; }
/* touch-action: pan-y alone (the first attempt) also silently disables the
   browser's native pinch-zoom recognition on this element -- touch-action
   values are mutually exclusive in what gestures they let through, not
   additive by default. "pan-y pinch-zoom" keeps native pinch-to-zoom (and
   vertical pan) working while still leaving single-finger horizontal drag
   un-owned by the browser, which is what the swipe-to-browse JS below
   needs -- it was never blocked by touch-action to begin with (its
   listeners are { passive: true }, so they never fought the browser for
   the gesture), only pinch-zoom was the casualty here. */
.lightbox img { max-width: 92vw; max-height: 88vh; object-fit: contain; touch-action: pan-y pinch-zoom; user-select: none; -webkit-user-drag: none; }
.lightbox .gallery-nav { opacity: .85; }
/* Same white-outline treatment as .gallery-nav's arrows, so the two read
   as one family of overlay controls instead of the close button looking
   like a different, unrelated component -- but always full opacity, not
   the arrows' hover-to-reveal fade: finding the way out shouldn't ever
   need a hover to discover it. */
.lightbox-close {
  position: absolute; top: 1rem; right: 1rem; z-index: 2;
  width: 2.6rem; height: 2.6rem; border-radius: 50%; border: 2px solid var(--warm-white); cursor: pointer;
  background: var(--ink-fixed); color: var(--warm-white); box-shadow: 0 2px 10px rgba(0,0,0,.35);
  display: flex; align-items: center; justify-content: center;
}
.lightbox-close svg { width: 20px; height: 20px; }

/* ---------- entry detail: variant picker (filters the gallery above) ---------- */
.variant-picker { margin: 0 0 1rem; }
/* .hint's base size (.76rem) is fine for a small inline label next to a
   control elsewhere (Ontdek's Toon:/Sorteer:) but reads too small stacked
   above the pills below it on its own line -- 16px minimum, scoped here
   rather than raising the shared base rule everywhere it's used. */
.variant-picker .hint { display: block; margin-bottom: .45rem; font-size: 1rem; }
.variant-pills { display: flex; flex-wrap: wrap; gap: .5rem; }
.variant-pill {
  font-family: var(--font-body); font-size: 1rem; background: transparent;
  border: 1px solid var(--paper-line); color: var(--ink-soft); padding: .4rem .9rem; border-radius: 999px; cursor: pointer;
}
.variant-pill:hover { border-color: var(--accent); color: var(--ink); }
.variant-pill.current { background: var(--ink); border-color: var(--ink); color: var(--paper); }

/* ---------- contact page ---------- */
/* Top/bottom rhythm matched to the rest of the site: same top offset as
   page-head (this page has no separate page-head anymore -- the h1 lives
   in the grid instead, see .contact-cols h1 below), equal padding on
   both sides of the hairline between the two rows, and the same
   clamp(2.2rem,5vw,3.4rem)-style closing space before the footer that
   Home's .visit section uses, instead of the earlier flat 1rem that
   read as cramped next to the footer. */
.contact { padding: clamp(1.8rem, 5vw, 2.6rem) 0 clamp(2rem, 4vw, 2.8rem); }
.contact-secondary { border-top: 1px solid var(--paper-line); padding: clamp(2rem, 4vw, 2.8rem) 0 clamp(2.2rem, 5vw, 3.4rem); }
.contact-cols { display: grid; grid-template-columns: .8fr 1.2fr; gap: clamp(1.5rem, 4vw, 3rem); align-items: start; }
.contact-cols h1 { font-family: var(--font-stencil); font-weight: 400; font-size: clamp(1.7rem, 3.2vw, 2.3rem); margin: .3rem 0 .9rem; text-wrap: balance; }
.contact-photo { border-radius: 8px; overflow: hidden; border: 1px solid var(--paper-line); }
.contact-photo img { width: 100%; display: block; }
.visit-map .map-caption { margin-top: .9rem; }
.section-h2 { font-family: var(--font-display); font-size: 1.4rem; margin: 0 0 .8rem; }
.contact-info p.line { margin: 0 0 .5rem; color: var(--ink-soft); }
.contact-info p.line strong { color: var(--ink); }
.contact-info p.line a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--paper-line); }
.contact-form { margin-top: 1.6rem; }
.contact-form h3 { font-family: var(--font-display); font-size: 1.15rem; margin: 0 0 1rem; }
.field { margin-bottom: 1rem; }
.field label { display: block; font-size: 1rem; color: var(--ink-soft); margin-bottom: .3rem; }
.field input, .field textarea {
  width: 100%; font-family: var(--font-body); font-size: 1rem; padding: .6rem .7rem;
  border: 1px solid var(--paper-line); border-radius: 5px; background: var(--paper-dim); color: var(--ink);
}
.field textarea { min-height: 110px; resize: vertical; }
.field.invalid input, .field.invalid textarea { border-color: var(--danger); }
.bb-toolbar { display: flex; gap: .4rem; margin-bottom: .4rem; }
.bb-toolbar button {
  font: inherit; font-size: .85rem; padding: .3rem .6rem; border-radius: 6px; cursor: pointer;
  border: 1px solid var(--paper-line); background: var(--paper-dim); color: var(--ink);
}
.bb-toolbar button:hover { border-color: var(--accent); color: var(--accent-text); }
.bb-toolbar button svg { width: 18px; height: 18px; display: block; }
.bb-toolbar button.active { background: var(--accent-solid); border-color: var(--accent-solid); color: var(--ink-fixed); }
.bb-editor {
  min-height: 160px; font-family: var(--font-body); font-size: 1rem; padding: .6rem .7rem;
  border: 1px solid var(--paper-line); border-radius: 5px; background: var(--paper-dim); color: var(--ink);
}
.bb-editor:focus { outline: 2px solid var(--focus); outline-offset: 1px; }
/* Full editor width, not a narrower max-width box -- an embed capped
   narrower than the editor itself left genuine blank space to its side
   that belongs to no element (just the editor's own background peeking
   through), which is exactly the kind of ambiguous ground text-caret
   hit-testing can resolve unpredictably on. Full-width removes that dead
   zone outright: every point at that vertical band unambiguously belongs
   either to the embed (non-editable) or to real editable content
   above/below it. */
.bb-editor .video-box, .bb-editor .bb-image-box { position: relative; }
/* .video-box already caps its own height (the base .video-box rule below
   sets aspect-ratio:16/9), but .bb-image-box deliberately doesn't -- the
   public site wants a tall image (e.g. a newspaper-article screenshot,
   the actual motivating case for this feature) to show at its full,
   undistorted height, "stacked... almost exactly like the original
   article." Inside the *editor* specifically, that same unconstrained
   height turned into a real bug: a portrait-oriented image could render
   hundreds of pixels tall with no cap. Centered (flex) in its now-full-
   width box rather than left-aligned, so a narrower image doesn't itself
   recreate the same "blank space beside it" dead zone one level in. */
.bb-editor .bb-image-box { display: flex; justify-content: center; background: var(--paper-dim); }
.bb-editor .bb-image-box img { display: block; max-width: 100%; max-height: 22rem; width: auto; height: auto; }
/* Hidden on the public site (the button renders there too, since it's part
   of the same server-rendered markup the editor's initial content reuses)
   -- shown only inside the admin editor, where it's an actual affordance. */
.bb-embed-remove { display: none; }
.bb-editor .bb-embed-remove {
  display: flex; align-items: center; justify-content: center; position: absolute; top: .4rem; right: .4rem; z-index: 1;
  width: 26px; height: 26px; border-radius: 999px; border: none; cursor: pointer;
  background: rgba(0,0,0,.65); color: var(--warm-white); font-size: 1.1rem; line-height: 1;
}
.bb-editor .bb-embed-remove:hover { background: rgba(0,0,0,.85); }
/* No space reserved in advance -- collapsed entirely until a field is
   actually invalid, so the form only grows when a message appears. */
.field-error { display: none; align-items: center; gap: .35rem; margin: .35rem 0 0; font-size: .95rem; color: var(--danger); }
.field-error svg { width: 14px; height: 14px; flex: none; }
.field.invalid .field-error { display: flex; }
.hp-field { position: absolute; left: -9999px; top: -9999px; height: 0; width: 0; overflow: hidden; }

.map-section { padding: 1.2rem 0 3rem; }
.map-section h2 { font-family: var(--font-display); font-size: 1.2rem; margin: 0 0 .8rem; }
.map-box { position: relative; border: 1px solid var(--paper-line); border-radius: 8px; overflow: hidden; max-width: 520px; }
.map-box iframe { width: 100%; height: 320px; display: block; border: 0; }
.map-caption { display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; margin-top: .7rem; font-size: 1rem; color: var(--ink-soft); max-width: 520px; }

/* ---------- Sfeer gallery ---------- */
/* CSS multi-column masonry rather than a cropped uniform grid: each photo
   keeps its own natural aspect ratio (no forced 4:3 crop), so the layout
   gets organic variety straight from the real photos' own shapes instead
   of the old site's randomly-sized tiles -- calmer, still no two rows
   ever line up identically. Plain CSS columns, no JS masonry library,
   matching this project's "no build step" bias. */
.sfeer-grid { column-count: 3; column-gap: 1.1rem; padding-bottom: 3rem; }
.sfeer-item {
  display: block; width: 100%; margin: 0 0 1.1rem; padding: 0; border: none;
  background: var(--paper-dim); border-radius: 8px; overflow: hidden; cursor: zoom-in;
  break-inside: avoid;
}
.sfeer-item img { width: 100%; display: block; border: 1px solid var(--paper-line); border-radius: 8px; }

@media (max-width: 800px) {
  .sfeer-grid { column-count: 2; }
}
@media (max-width: 480px) {
  .sfeer-grid { column-count: 1; }
}

/* ---------- Over Ons ---------- */
/* A few purpose-built rows (lede, photo+text, full-width video, photo+text)
   rather than one long vertical column or a single rigid two-column split
   -- same "bespoke per page" approach as Contact's 2x2 grid, chosen so a
   six-paragraph founding story reads with some horizontal variety and
   natural pacing instead of a wall of centered text. */
.story-section { padding: clamp(1.4rem, 3.6vw, 2.2rem) 0; }
.story-section + .story-section { border-top: 1px solid var(--paper-line); }
.story-section:last-child { padding-bottom: clamp(2.4rem, 5vw, 3.4rem); }
.story { max-width: 60ch; }
.story p { color: var(--ink-soft); margin: 0 0 1.1rem; }
.story p a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--paper-line); }
.story p a:hover { border-color: var(--accent); }
/* A one-off "name reveal" beat where the story names the place -- Stencil
   Std is otherwise reserved for the nav wordmark and page H1s (see the
   design system notes), but this is the one spot in running body copy
   where using it is the whole point: it's a deliberate callback to the
   real hand-stenciled wooden sign the story photo right next to it shows,
   not a general body-text treatment. */
.story p.reveal-name {
  display: flex; align-items: center; gap: .85rem;
  font-family: var(--font-stencil); font-weight: 400; color: var(--ink);
  font-size: clamp(1.6rem, 4.2vw, 2.2rem); letter-spacing: .02em;
  margin: .3rem 0 1.3rem;
}
/* Reuses the header/footer wordmark's own logo-mark image (see
   .logo-mark in the header rules) rather than a third copy of the icon,
   just sized up to sit next to this bigger stencil text. */
.reveal-name .logo-mark { width: clamp(2.2rem, 5vw, 2.7rem); height: clamp(2.2rem, 5vw, 2.7rem); }

.story-row { display: grid; grid-template-columns: 1fr 1.2fr; gap: clamp(1.5rem, 4vw, 3rem); align-items: start; }
.story-row.reverse .story-media { order: 2; }
.story-media { margin: 0; }
.story-media img { width: 100%; display: block; border-radius: 8px; border: 1px solid var(--paper-line); }
.story-media figcaption { margin-top: .6rem; font-size: .9rem; font-style: italic; color: var(--ink-soft); }

/* Reuses .map-box's aspect-ratio-box + bordered-rounded-corners treatment
   (Contact page), scaled down to a max-width so a 16:9 video doesn't span
   the full text measure on wide screens. */
.video-box { position: relative; border-radius: 8px; overflow: hidden; border: 1px solid var(--paper-line); aspect-ratio: 16/9; max-width: 900px; margin-top: .9rem; }
.video-box iframe { width: 100%; height: 100%; display: block; border: 0; }
/* object-fit:contain (not cover) for an uploaded video -- portrait phone
   footage shouldn't get cropped to fit this 16:9 box the way a YouTube
   embed (always 16:9 already) never needs to worry about. */
.video-box video { width: 100%; height: 100%; display: block; object-fit: contain; background: #000; }
/* An inline [img] insert (BB-editor "Afbeelding" button) -- no fixed
   aspect-ratio box like video, since a photo can be any shape; just a
   rounded, full-width block matching the same vertical rhythm as .video-box. */
.bb-image-box { border-radius: 8px; overflow: hidden; margin-top: .9rem; }
.bb-image-box img { width: 100%; display: block; }

.media-mention {
  display: inline-flex; align-items: center; gap: .5rem; margin-top: 1.3rem;
  font: inherit; font-size: .95rem; color: var(--ink-soft); text-decoration: none;
  text-align: left; background: none; border: none; border-bottom: 1px solid var(--paper-line);
  padding: 0 0 .15rem; cursor: pointer;
}
.media-mention svg { width: 18px; height: 18px; flex: none; color: var(--accent-text); }
.media-mention:hover { color: var(--ink); border-color: var(--accent); }
/* Three .media-mention triggers side by side (Tobias reportage, Radio 2,
   HLN artikel) -- wraps to a stacked column on narrow screens since each
   button's own margin-top already gives stacked spacing for free. */
.media-mentions { display: flex; flex-wrap: wrap; column-gap: 2rem; }

/* ---------- Over Ons: video + article popups ---------- */
/* Same fixed-overlay + reused .lightbox-close pattern as the photo
   lightbox (entry detail / Sfeer) -- a position:fixed ancestor is still a
   valid containing block for the close button's position:absolute, same
   as there. Self-hosted <video> now (site's own upload pipeline) instead
   of a YouTube iframe, so playback stays on-site. */
.video-lightbox { position: fixed; inset: 0; z-index: 2000; background: rgba(0,0,0,.92); display: flex; align-items: center; justify-content: center; padding: 2rem; }
.video-lightbox[hidden] { display: none; }
.video-lightbox-box { position: relative; width: 100%; max-width: 960px; aspect-ratio: 16/9; border-radius: 8px; overflow: hidden; }
.video-lightbox-box video { width: 100%; height: 100%; display: block; object-fit: contain; background: #000; }

/* Article popup (krantenartikel) -- reuses the generic .lightbox overlay +
   .lightbox-close button. The HLN piece itself is paywalled (HLN+), so
   rather than link out to a page most visitors can't read, this shows the
   same stacked page screenshots as Actueel post #131 -- a scrollable,
   read-it-here reproduction instead of a dead end. */
.article-lightbox-box {
  width: 100%; max-width: 640px; max-height: 88vh; overflow-y: auto;
  background: var(--paper); border-radius: 8px;
}
.article-lightbox-box img { width: 100%; display: block; }
.article-lightbox-box img + img { margin-top: 2px; }

/* ---------- Actueel ---------- */
/* No max-width of its own -- fills the page's .wrap (1280px) instead of the
   old 72ch text-column cap, so the thumbnail (below) has real desktop
   horizontal room instead of being squeezed into a narrow reading column. */
.posts { padding: 0 0 3rem; display: flex; flex-direction: column; gap: 1.6rem; }
.posts article { border-top: 1px solid var(--paper-line); padding-top: 1.6rem; display: flex; gap: 1.8rem; align-items: flex-start; }
.posts article:first-child { border-top: none; padding-top: 0; }
.posts .post-thumb {
  width: 40%; max-width: 440px; flex: none; aspect-ratio: 4/3; border-radius: 8px; overflow: hidden;
  border: 1px solid var(--paper-line); background: var(--accent-soft); display: flex; align-items: center; justify-content: center;
  position: relative;
}
.posts .post-thumb img { width: 100%; height: 100%; object-fit: cover; }
.posts .post-thumb svg { width: 42%; height: 42%; color: var(--ink); opacity: .6; }
/* Feed/search thumbnail for a post whose first gallery item is a video --
   thumb itself is a real ffmpeg-extracted poster JPEG (see thumb_filename,
   app/routes/admin.py's save_video_upload()), this badge is just the visual
   cue that it plays. Same circle+triangle shape as the admin photo-manager's
   .pthumb-video badge (style.css, photo-list rules), sized up for the
   bigger feed thumbnail. */
.posts .post-thumb.has-video::before {
  content: ""; position: absolute; top: 50%; left: 50%; width: 3rem; height: 3rem;
  transform: translate(-50%, -50%); border-radius: 50%; background: rgba(0,0,0,.55);
}
.posts .post-thumb.has-video::after {
  /* width/height: 0 -- see .gallery-thumb.has-video::after's comment
     (style.css) for why a non-zero size here breaks the border-triangle
     shape and gets it clipped by the parent's overflow:hidden. */
  content: ""; position: absolute; top: 50%; left: calc(50% + .2rem); width: 0; height: 0;
  transform: translate(-50%, -50%);
  border-style: solid; border-width: .65rem 0 .65rem 1.05rem;
  border-color: transparent transparent transparent var(--warm-white);
}
.posts .post-body { flex: 1; min-width: 0; }
.posts time { font-family: var(--font-mono); font-size: .74rem; color: var(--accent-text); text-transform: uppercase; letter-spacing: .06em; }
.posts h2 { font-family: var(--font-display); font-size: 1.3rem; margin: .3rem 0 .5rem; }
.posts h2 a { color: inherit; text-decoration: none; }
.posts h2 a:hover { color: var(--accent-text); }
.posts p { color: var(--ink-soft); margin: 0; max-width: 60ch; }

.pagination { display: flex; align-items: center; justify-content: center; gap: 1.2rem; margin: 2.4rem 0 1rem; }
.pagination-status { font-family: var(--font-mono); font-size: .8rem; color: var(--ink-soft); text-transform: uppercase; letter-spacing: .06em; }

@media (max-width: 560px) {
  .posts article { flex-direction: column; }
  .posts .post-thumb { width: 100%; max-width: none; aspect-ratio: 16/9; }
}

/* ---------- Actueel post detail ---------- */
/* Reuses entry detail's .detail/.detail-cols/.detail-gallery/.detail-info
   grid wholesale (see the "entry detail: photo gallery" block below) --
   same wide two-column layout, same gallery+lightbox+thumbnail-strip
   markup and JS pattern (post-detail.js mirrors entry-detail.js, minus the
   variant-picker parts entries have and posts don't). Previously had its
   own narrow (640px) single-column CSS; removed in favor of the shared
   classes rather than maintaining two near-duplicate detail-page layouts.
   .bb-content is the one post-detail-specific addition, for the "bericht"
   field's rendered BB markup. */
.bb-content a, .bb-content .bb-link { color: var(--accent-text); text-decoration: underline; }
.bb-editor .bb-link { cursor: text; }
.bb-content .video-box, .bb-content .bb-image-box { margin-top: .9rem; margin-bottom: .9rem; }
.bb-content ul, .bb-content ol { margin: .6rem 0; padding-left: 1.4rem; }
.bb-content li { margin-bottom: .3rem; }

/* ---------- footer ---------- */
/* Paper-dim (the same "card ground" tint used everywhere else, e.g.
   .qcard/.card) rather than plain --paper -- gives the footer a touch of
   separation from the page above it instead of blending in flush, without
   introducing a new one-off color into the palette. */
footer.site { padding: 2.2rem 0 1.8rem; font-size: 1rem; color: var(--ink-soft); background: var(--paper-dim); }
footer.site .wrap.footer-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1.5rem; align-items: start; }
footer.site .fbrand-name, footer.site h3 { display: block; margin: 0 0 .9rem; }
/* Reuses the header's own .wordmark + .logo-mark treatment so the brand
   mark is consistent, just slightly smaller for the footer's scale. */
footer.site .fbrand-name { display: flex; align-items: center; gap: .85rem; }
footer.site .fbrand-name .logo-mark { width: 30px; height: 30px; }
footer.site .fbrand-name .wordmark-text { font-family: var(--font-stencil); font-weight: 400; font-size: 1.35rem; letter-spacing: .02em; color: var(--ink); text-transform: uppercase; }
footer.site h3 {
  font-family: var(--font-mono); font-size: 1.35rem; text-transform: uppercase; letter-spacing: .03em;
  color: var(--ink);
}
footer.site .fcol p { margin: 0 0 .4rem; }
/* Two columns of links instead of one tall list -- with six links this
   column was noticeably taller than the other two, which read as unbalanced. */
footer.site .fcol ul {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: 1fr 1fr; gap: .4rem 1rem;
}
footer.site a { color: inherit; text-decoration: none; border-bottom: 1px solid transparent; }
footer.site a:hover { color: var(--ink); border-color: var(--paper-line); }

/* ---------- Admin ---------- */
.admin-nav { padding: 0 0 1.4rem; }
.admin-nav nav { display: flex; gap: 1rem; flex-wrap: wrap; align-items: center; border-bottom: 1px solid var(--paper-line); padding-bottom: .9rem; }
.admin-nav nav a { font-size: .88rem; color: var(--ink-soft); text-decoration: none; padding: .3rem 0; border-bottom: 2px solid transparent; }
.admin-nav nav a:hover, .admin-nav nav a.current { color: var(--ink); border-color: var(--accent); }
.admin-nav nav a:last-child { margin-left: auto; }

.admin-body { padding-bottom: 3rem; }

.flash-list { margin-bottom: 1.2rem; }
.flash { padding: .7rem 1rem; border-radius: 6px; font-size: 1rem; margin: 0 0 .6rem; }
.flash-success { background: var(--accent-soft); color: var(--accent-text); border: 1px solid var(--accent); }
.flash-error { background: #f4d9d9; color: #7a2b2b; border: 1px solid #d99; }
:root[data-theme="dark"] .flash-error { background: #4a2626; color: #f0b8b8; border-color: #7a3b3b; }

.admin-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.2rem; }
.admin-toolbar-groups { display: flex; flex-direction: column; gap: .5rem; }
.admin-filters { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }
.admin-filters a {
  font-family: var(--font-mono); font-size: .75rem; text-transform: uppercase; letter-spacing: .04em;
  padding: .35rem .8rem; border: 1px solid var(--paper-line); border-radius: 999px; text-decoration: none; color: var(--ink-soft);
}
.admin-filters a.current { background: var(--ink); border-color: var(--ink); color: var(--paper); }

/* entries list: eye-toggle per entry + a per-color-variant sub-list, each
   with its own toggle -- replaces a plain table since a variant sub-list
   doesn't fit table rows cleanly. */
.admin-entries { list-style: none; margin: 0; padding: 0; }
.admin-entry { border-bottom: 1px solid var(--paper-line); padding: .7rem 0; transition: opacity .2s ease; }
.admin-entry.is-hidden { opacity: .55; }
.admin-entry-row { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; }
.admin-entry-name { font-weight: 600; color: var(--ink); text-decoration: none; }
.admin-entry-name:hover { text-decoration: underline; }
.admin-entry-thumb { width: 40px; height: 40px; object-fit: cover; border-radius: 4px; flex-shrink: 0; }
.admin-tag {
  font-family: var(--font-mono); font-size: .64rem; text-transform: uppercase; letter-spacing: .08em;
  color: var(--accent-text); border: 1px solid var(--accent); border-radius: 999px; padding: .12rem .5rem; white-space: nowrap;
}
.admin-status { font-size: .8rem; color: var(--danger); }
/* Plain photo count ("N foto's") -- muted ink, purely informational. */
.admin-status-muted { font-size: .8rem; color: var(--ink-soft); }
/* "Geen foto's" specifically -- worth noticing (unlike a normal count)
   but not a real problem like a hidden entry, so a medium-tier amber
   rather than the danger red the hidden-status notes use. */
.admin-status-warning { font-size: .8rem; color: var(--warning); font-weight: 600; }
.admin-entry-actions { margin-left: auto; display: flex; gap: .8rem; align-items: center; white-space: nowrap; }
.admin-entry-actions form { display: inline; }
.admin-entries-empty { padding: .7rem 0; color: var(--ink-soft); }

.visibility-btn {
  background: none; border: 1px solid var(--paper-line); border-radius: 6px; cursor: pointer;
  color: var(--ink-soft); padding: .4rem; display: flex; align-items: center; justify-content: center; flex: none;
}
.visibility-btn:hover { border-color: var(--accent); color: var(--ink); }
.visibility-btn svg { width: 18px; height: 18px; }
.visibility-btn.small { padding: .3rem; }
.visibility-btn.small svg { width: 14px; height: 14px; }

.admin-variants { list-style: none; margin: .6rem 0 0 2.4rem; padding: 0; display: flex; flex-direction: column; gap: .4rem; }
.admin-variant { display: flex; align-items: center; gap: .6rem; font-size: .88rem; transition: opacity .2s ease; }
.admin-variant.is-hidden { opacity: .55; }
.admin-variant-name { min-width: 8rem; }
.admin-variant-count { color: var(--ink-soft); font-size: .8rem; }

table.admin-table { width: 100%; border-collapse: collapse; font-size: .9rem; }
table.admin-table th, table.admin-table td { text-align: left; padding: .6rem .7rem; border-bottom: 1px solid var(--paper-line); vertical-align: middle; }
table.admin-table th { font-family: var(--font-mono); font-size: .7rem; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-soft); }
table.admin-table td.thumb-cell { width: 60px; }
table.admin-table td.thumb-cell img { width: 48px; height: 48px; object-fit: cover; border-radius: 4px; }
table.admin-table td.actions { white-space: nowrap; text-align: right; }
table.admin-table td.actions a { margin-right: .8rem; }
table.admin-table td.actions form { display: inline; }
.link-btn { background: none; border: none; font: inherit; color: var(--accent-text); cursor: pointer; padding: 0; text-decoration: underline; }
.link-btn.danger { color: #a33; }

.admin-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 1.1rem; margin-bottom: 1.4rem; }
.admin-cards .acard { background: var(--paper-dim); border: 1px solid var(--paper-line); border-radius: 8px; padding: 1.2rem; text-decoration: none; color: var(--ink); display: block; }
.admin-cards .acard:hover { border-color: var(--accent); }
.admin-cards .acard h3 { font-family: var(--font-display); margin: 0 0 .3rem; }
.admin-cards .acard p { margin: 0; font-size: .85rem; color: var(--ink-soft); }

.admin-form { max-width: 640px; }
.admin-form select {
  width: 100%; font-family: var(--font-body); font-size: .92rem; padding: .6rem .7rem;
  border: 1px solid var(--paper-line); border-radius: 5px; background: var(--paper-dim); color: var(--ink);
}
.admin-form .field input[type="file"] { padding: .4rem 0; border: none; background: none; }
.input-suffix { display: flex; align-items: center; gap: .6rem; }
.input-suffix input { width: 6rem; flex: none; }
.input-suffix span { font-size: .85rem; color: var(--ink-soft); white-space: nowrap; }
.admin-form .row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1rem; }
.actions-row { display: flex; gap: .8rem; align-items: center; margin-top: 1.4rem; }
.actions-row .delete-form { margin-left: auto; }
.btn.danger { background: var(--danger); border-color: var(--danger); }
.btn.danger:hover { background: transparent; color: var(--danger); }

.photo-manager { margin-top: 2rem; padding-top: 1.6rem; border-top: 1px solid var(--paper-line); }
.photo-manager h2 { font-family: var(--font-display); font-size: 1.15rem; margin: 0 0 1rem; }

/* drag-to-reorder photo grid: the first tile (the "main"/thumbnail photo)
   is rendered bigger via plain :first-child, so whatever the user drags
   to the front automatically gets the bigger treatment -- no JS needed
   for that part, only for the reordering itself. */
.photo-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1rem; margin: 0 0 1.4rem; }
.photo-list .pitem { background: var(--paper-dim); border: 1px solid var(--paper-line); border-radius: 8px; overflow: hidden; }
.photo-list.has-main .pitem:first-child { grid-column: span 2; grid-row: span 2; }
.photo-list .pthumb { aspect-ratio: 4/3; cursor: grab; touch-action: none; position: relative; }
.photo-list .pthumb img, .photo-list .pthumb video { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; }
.photo-list .pthumb-video::before {
  content: ""; position: absolute; top: 50%; left: 50%; width: 2.2rem; height: 2.2rem;
  transform: translate(-50%, -50%); border-radius: 50%; background: rgba(0,0,0,.55);
}
.photo-list .pthumb-video::after {
  /* width/height: 0 -- see .gallery-thumb.has-video::after's comment
     (style.css) for why a non-zero size here breaks the border-triangle
     shape and gets it clipped by the parent's overflow:hidden. */
  content: ""; position: absolute; top: 50%; left: calc(50% + .15rem); width: 0; height: 0;
  transform: translate(-50%, -50%);
  border-style: solid; border-width: .5rem 0 .5rem .8rem;
  border-color: transparent transparent transparent var(--warm-white);
}
.photo-list .pitem.dragging { opacity: .95; box-shadow: 0 10px 28px rgba(0,0,0,.28); cursor: grabbing; }
.photo-list .pitem.placeholder { background: transparent; border: 2px dashed var(--paper-line); }
.photo-list .pitem input[type="text"] {
  display: block; width: calc(100% - 1.2rem); box-sizing: border-box; margin: .5rem .6rem 0; padding: .35rem .5rem;
  font-family: var(--font-body); font-size: .8rem; border: 1px solid var(--paper-line); border-radius: 4px;
  background: var(--paper); color: var(--ink);
}
.photo-list .pitem .link-btn { display: block; width: 100%; text-align: left; padding: .4rem .6rem .6rem; }

/* per-photo variant tagging chips (a photo can carry more than one variant,
   e.g. a group shot) */
.pchips { display: flex; flex-wrap: wrap; gap: .4rem; margin: .6rem .6rem 0; }
.pchips .chip {
  display: flex; align-items: center; gap: .4rem; font-size: 1rem; line-height: 1.3;
  padding: .35rem .65rem; border: 1px solid var(--paper-line); border-radius: 999px;
  background: var(--paper); color: var(--ink-soft); cursor: pointer;
}
/* Custom box instead of accent-color: accent-color only lets us pick the
   fill, the browser still auto-picks the tick's color for contrast (plain
   white in every browser tested) -- to get the actual --warm-white token
   instead of literal white, the tick has to be our own inline SVG. Fixed
   (not theme-relative) ink-fixed/warm-white, matching how any other
   must-look-identical-in-both-themes chrome on this site is done. */
.pchips .chip input[type="checkbox"] {
  appearance: none; -webkit-appearance: none; margin: 0; flex-shrink: 0; cursor: pointer;
  width: 1.15em; height: 1.15em; border: 1.5px solid var(--ink-soft); border-radius: 3px; background: var(--paper);
}
.pchips .chip input[type="checkbox"]:checked {
  background-color: var(--ink-fixed); border-color: var(--ink-fixed);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.5l3 3l6-7' fill='none' stroke='%23FDF4DC' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-size: 75% 75%; background-position: center; background-repeat: no-repeat;
}

/* styled file-picker: hides the native input behind a .btn-styled label
   that opens the picker directly. */
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }

/* Skip-to-content link -- off-screen (same clip trick as .visually-hidden)
   until keyboard-focused, then pinned top-left above everything else so a
   keyboard/screen-reader user can jump straight past the header nav/search/
   theme-toggle controls instead of tabbing through all of them first. */
.skip-link {
  position: absolute; top: -3rem; left: .6rem; z-index: 200;
  background: var(--ink-fixed); color: var(--warm-white);
  padding: .6rem 1rem; border-radius: 6px; font-weight: 600;
  transition: top .15s ease;
}
.skip-link:focus-visible { top: .6rem; }
.file-picker { display: flex; align-items: center; gap: .9rem; flex-wrap: wrap; }

/* Kenmerken list-adder */
.list-adder { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .6rem; }
.list-adder-row { display: flex; align-items: center; gap: .6rem; }
.list-adder-row input { flex: 1; }
.list-adder-row .remove-row { font-size: 1.1rem; line-height: 1; padding: 0 .3rem; text-decoration: none; }

/* native date input: match field styling, follow the theme toggle for
   the browser-rendered popup's light/dark chrome (its internals can't
   be restyled further) */
.admin-form input[type="date"] { color-scheme: light; }
:root[data-theme="dark"] .admin-form input[type="date"] { color-scheme: dark; }

/* Themed confirm dialog, replacing the browser's native confirm() popup
   for destructive actions (entry/post delete) -- see admin.js. */
.modal-overlay {
  position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center;
  background: rgba(0,0,0,.45); padding: 1rem;
}
.modal-overlay[hidden] { display: none; }
.modal-box {
  background: var(--paper); border: 1px solid var(--paper-line); border-radius: 10px;
  padding: 1.5rem; max-width: 22rem; width: 100%; box-shadow: 0 12px 32px rgba(0,0,0,.3);
}
.modal-box p { margin: 0 0 1.3rem; color: var(--ink); font-size: 1rem; }
.modal-input {
  width: 100%; font-family: var(--font-body); font-size: 1rem; padding: .6rem .7rem; margin-bottom: 1.3rem;
  border: 1px solid var(--paper-line); border-radius: 5px; background: var(--paper-dim); color: var(--ink);
}
/* wrap: the unsaved-changes flow's button labels ("Verlaat zonder
   opslaan"/"Blijven op deze pagina") are longer than the original
   Verwijderen/Annuleren pair this modal was sized for. */
.modal-actions { display: flex; justify-content: flex-end; gap: .7rem; flex-wrap: wrap; }

/* Save-in-progress overlay (any .admin-form submit, admin.js) -- reuses
   .modal-overlay/.modal-box wholesale, just centered content instead of a
   left-aligned prompt/message. */
.saving-box { display: flex; flex-direction: column; align-items: center; text-align: center; }
.saving-box p { margin: 0; }
.spinner {
  width: 2.2rem; height: 2.2rem; margin-bottom: 1rem; border-radius: 50%;
  border: 3px solid var(--paper-line); border-top-color: var(--accent-solid);
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* The header nav needs a wider breakpoint than the rest of the site's
   800px cutoff: bigger + centered nav links (per user feedback) need more
   room than that, and between ~800-930px the nav ul (a flex row with no
   wrap) was squeezing individual link labels onto two lines rather than
   the row itself wrapping -- so this switches to the hamburger before
   that squeeze zone, not at the same point as everything else.
   930px, not a bare 960px: measured in-browser, the six nav labels need
   ~503px (link widths + the flex gap's 1rem floor), and the wordmark +
   header-actions columns + grid gaps eat a fixed ~332px, so the nav
   itself only just fits once total header width crosses ~908px. 930px
   keeps a real margin above that, while also staying *below* 960px --
   the width of half a 1920px display, a common "snap browser to half
   the screen" window size the user specifically wants to keep on the
   single-row desktop nav rather than dropping to the hamburger. main.js's
   resize listener uses this same value. */
@media (max-width: 930px) {
  .nav-toggle { display: inline-flex; }
  /* Slide-in panel from the right, fixed to the full viewport height
     (not just "under the header") so it reads as a real drawer instead of
     an expanding accordion -- the old max-height/opacity dropdown read as
     dated next to the rest of the site's own polish, and this matches the
     standard mobile-nav pattern instead. Sits above .page-backdrop (z-index
     28), which dims the rest of the page while it's open -- driven purely
     by this element's own .open class via main.js's syncBackdrop(), no
     extra wiring needed for the backdrop itself. */
  nav.main {
    position: fixed; top: 0; right: 0; bottom: 0; z-index: 30; justify-self: unset;
    width: min(80vw, 320px);
    background: var(--paper); border-left: 1px solid var(--paper-line);
    box-shadow: -8px 0 24px rgba(0,0,0,.25);
    overflow-y: auto; overscroll-behavior: contain;
    padding-top: 4rem;
    transform: translateX(100%); opacity: 1; pointer-events: none;
    transition: transform .28s ease;
  }
  nav.main.open { transform: translateX(0); pointer-events: auto; }
  .nav-close {
    display: inline-flex; position: absolute; top: 1rem; right: 1rem;
  }
  nav.main ul { flex-direction: column; gap: 0; padding: 0 clamp(1rem, 4vw, 2.5rem) 1rem; }
  nav.main ul li { border-top: 1px solid var(--paper-line); }
  nav.main ul li:first-child { border-top: none; }
  /* Bumped from the shared 1rem desktop size -- that was deliberately
     kept compact specifically so all six labels fit on one row in the
     ~930px-wide desktop bar (see the breakpoint comment above), a
     constraint that doesn't exist here: this is now a full-height,
     one-per-row stacked list with plenty of room, and it read too small
     once actually tried live on a phone. */
  nav.main a { display: block; font-size: 1.2rem; padding: .9rem 0; border-bottom: none; }
  nav.main a:hover, nav.main a.current { background: none; }
  /* Desktop's current-page indicator is a bottom border, dropped above for
     the drawer (a full-width underline under a stacked list item reads
     oddly). Left uncompensated, "current" was only the shared color change
     (ink vs ink-soft) from the base rule -- easy to miss at a glance,
     especially since it's the only signal. Left-border stripe + bold
     weight instead: two independent, color-agnostic differentiators
     (shape and weight) rather than relying on a subtle color shift alone. */
  nav.main a.current { border-left: 3px solid var(--accent); padding-left: .7rem; font-weight: 600; }

  /* Anchored to both edges (not a computed width) so it can't run past
     the visible screen -- `100vw` in the desktop rule's width calc is a
     known mobile-browser trouble spot (scrollbar-gutter/viewport quirks
     can make it wider than what's actually visible), which was pushing
     the popout out of view on some narrower phones.
     .nav-search must drop its position:relative here so header.site (not
     this small icon box) becomes the containing block these left/right
     values resolve against -- otherwise they overconstrain a ~34px-wide
     box into a nonsensical sliver. */
  .nav-search { position: static; }
  .nav-search-form { left: clamp(1rem, 4vw, 2.5rem); right: clamp(1rem, 4vw, 2.5rem); width: auto; }
}

@media (max-width: 800px) {
  .admin-form .row2 { grid-template-columns: 1fr; }
  .hero { min-height: min(56vh, 460px); }
  .quicklinks .grid3 { grid-template-columns: 1fr; }
  .visit .box { grid-template-columns: 1fr; }
  .detail-cols { grid-template-columns: 1fr; }
  .contact-cols { grid-template-columns: 1fr; }
  .story-row { grid-template-columns: 1fr; }
  /* Reset the desktop-only reverse order so every stacked row reads photo
     then text, top to bottom -- the alternating left/right placement is a
     wide-screen-only device, not something worth preserving as a stacking
     order once there's only one column. */
  .story-row.reverse .story-media { order: 0; }
  /* Both rows have the visual (map/photo) as the first DOM child and the
     text as the second, matching the desktop left/right column layout.
     Stacked on mobile that would read Map, Plan je bezoek, Photo,
     Contact -- flipping each row's order puts its own text first instead,
     giving Plan je bezoek, Map, Contact, Photo overall (rows keep their
     original top-to-bottom order; only the two children within each row
     swap), so each text block still leads the visual it's paired with
     rather than being separated from it by the other row. */
  .contact-cols > div:first-child { order: 2; }
  .contact-cols > div:last-child { order: 1; }
  .photo-list.has-main .pitem:first-child { grid-column: span 1; grid-row: span 1; }

  footer.site .wrap.footer-grid { grid-template-columns: 1fr; gap: 1.6rem; text-align: center; }
  /* inline-flex shrink-wraps the logo+text to its content width, so the
     column's own text-align:center (above) centers it as one inline unit
     -- more reliable here than justify-content on a full-width flex box. */
  footer.site .fbrand-name { display: inline-flex; }
  footer.site .fcol ul { grid-template-columns: 1fr; justify-items: center; }
}

/* Ontdek controls: Toon's own single-line content (~488px, hint + 4 pills)
   no longer fits even with the *entire* row to itself below here --
   collapse to a 2-column grid instead (auto-fit so it still widens back
   out to more columns if given more room). filters is always alone on its
   own line from the 850px tier down (never sharing with view-toggle
   anymore, now that sort permanently pairs with view instead), so an
   uncapped 1fr is safe everywhere -- no neighboring gap for stretched
   columns to eat into like the old tablet tier had to guard against. */
@media (max-width: 540px) {
  .filters { flex-direction: column; align-items: flex-start; gap: .5rem; }
  .filters-scroll { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); width: 100%; }
  /* width:100% so each pill actually fills its equal-width grid column --
     without it, a shorter-text button (e.g. "Alles tonen") only takes its
     own content width, leaving a ragged gap where the column still
     reserved full width for it (confirmed as a real bug, not intentional). */
  .filters-scroll button { display: flex; align-items: center; justify-content: center; width: 100%; }
}

/* Smallest phones: "Sorteer:" as a text label plus the dropdown no longer
   comfortably shares its row with the view-toggle -- swap to an icon-only
   label (same pattern as the theme toggle's sun/moon swap) so the pairing
   still holds instead of view-toggle being forced to wrap onto yet
   another line. */
@media (max-width: 420px) {
  .sort-select .sort-hint-text { display: none; }
  .sort-select .sort-hint-icon { display: block; }
  /* A real 375px phone (iPhone SE and similar) was still ~6px short of
     fitting sort-select + view-toggle on one line even with the icon swap
     above -- trimmed a bit more room from the select's own padding/gap and
     the row's column-gap (confirmed via direct measurement, not guessed). */
  .controls-bar { column-gap: .5rem; }
  .sort-select { gap: .35rem; }
  .sort-select select { padding: .5rem .45rem; }
}

/* Very small phones only (widened from 400px -- that was narrow enough
   that it likely never triggered on some real small-phone widths at all).
   Header: keep just the logo image, drop the wordmark text so it doesn't
   crowd the hamburger button -- unrelated to the footer rule below, this
   one's still genuinely about header-row width. */
@media (max-width: 480px) {
  /* This was `.wordmark span` (no class) -- .logo-mark is ALSO a <span>,
     so that selector matched it too and hid the whole logo (including
     both swapped <img>s) along with the text, which is the actual bug
     behind the logo being reported invisible on small screens. Verified
     directly via computed styles in a real Chrome tab: .logo-mark had
     display:none from this exact rule. Scoping to .wordmark-text fixes it. */
  header.site .wordmark .wordmark-text { display: none; }
}

/* Footer logo: used to drop at the same 480px as the header rule above,
   but 480px covers nearly every real phone in portrait mode (iPhone 14 is
   390px, Pixel 7 is 412px, even a Galaxy S22 is 360px) -- so in practice
   the icon was disappearing for almost all mobile visitors, not just
   "very small phones" as the original comment intended. Narrowed to a
   genuinely tiny cutoff instead. The footer's own wordmark text is never
   hidden by any rule (unlike the header's above), so this can't
   accidentally reintroduce the "bare icon, no text" look the original fix
   was for -- text+icon just keep showing together for longer now. */
@media (max-width: 360px) {
  footer.site .fbrand-name .logo-mark { display: none; }
}
