/* =========================================================
   Static site template
   styles.css
   ========================================================= */

/* ---- Webfonts (self-hosted) -------------------------------------------
   Variable woff2 files served from our own origin — no Google Fonts request
   chain in the critical path, no third-party connection (EU-friendly). One
   file per family+subset covers every weight the site uses (Inter 400–600,
   Oswald 500–600); the browser only fetches the subsets the text needs
   (Slovak pulls latin + latin-ext). index.html preloads all four. */
@font-face {
    font-family: "Inter";
    font-style: normal;
    font-weight: 400 600;
    font-display: swap;
    src: url("assets/fonts/inter-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: "Inter";
    font-style: normal;
    font-weight: 400 600;
    font-display: swap;
    src: url("assets/fonts/inter-latin-ext.woff2") format("woff2");
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
    font-family: "Oswald";
    font-style: normal;
    font-weight: 500 600;
    font-display: swap;
    src: url("assets/fonts/oswald-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: "Oswald";
    font-style: normal;
    font-weight: 500 600;
    font-display: swap;
    src: url("assets/fonts/oswald-latin-ext.woff2") format("woff2");
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---- Design tokens ---------------------------------------------------- */
:root {
    --font-sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    /* Headings: bold/industrial condensed face. */
    --font-serif: "Oswald", "Arial Narrow", system-ui, sans-serif;
    /* Brand wordmark only. The header brand renders as an image (spec-level
       brandImage), so this only shows on the error screen's text fallback —
       no webfont is loaded for it, system serif is enough. */
    --font-brand: Georgia, "Times New Roman", serif;

    /* Single content column width. Blocks fill it; the page gutter keeps them
       off the screen edges on small viewports. */
    --content-width: 56rem;

    /* Uniform vertical rhythm between consecutive blocks in a section. */
    --block-gap: 1.5rem;

    /* GLOBAL SIZE DIAL. Everything below is in rem / rem-based clamp(), so it
       all scales off the root font-size. Change this one value to make the
       whole site (text, headings, gutters, gaps, content widths) bigger or
       smaller proportionally. 100% = browser default (usually 16px);
       106.25% ≈ 17px; 112.5% ≈ 18px. Applied on `html` below. */
    --root-scale: 99.25%;

    --gutter: clamp(1.25rem, 4vw, 2.5rem);
    --radius: 6px;
    --radius-sm: 6px;

    --transition: 220ms cubic-bezier(0.4, 0, 0.2, 1);

    /* Interaction tokens — theme-independent (same in light + dark).
       Keyboard focus ring (only shown after real keyboard nav) and the
       subtle "press" shrink shared by every button-like control. */
    --focus-ring: 2px solid var(--accent);
    --focus-offset: 3px;

    --unicode-arrow: "\2192"; /* Unicode for → */

    /* Theme-independent tokens — identical in light + dark, so defined once
       here instead of duplicated in both [data-theme] blocks.

       --brand-* : per-platform social/contact icon tints. renderTheme() can
                   override these from SITE.theme.socialColors.
       --on-image-* / --dot-* / --carousel-chip-* : controls that sit ON TOP of
                   photos (lightbox, carousel dots/counter), so they stay
                   dark-on-light in BOTH themes for legibility over any image. */
    --panel-blur: 8px; /* frosted-glass radius; kept modest to ease scroll repaint cost */

    --brand-instagram: #e1306c;
    --brand-youtube: #ff0000;
    --brand-spotify: #1db954;
    --brand-phone: #2e9e4f;
    --brand-email: #3b5bdb;
    --brand-linkedin: #0a66c2;

    --on-image-text: #fff;
    --on-image-btn-bg: rgba(0, 0, 0, 0.45);
    --on-image-btn-bg-hover: rgba(0, 0, 0, 0.7);
    --on-image-hairline: rgba(255, 255, 255, 0.25);
    --on-image-veil: rgba(0, 0, 0, 0.88);

    --dot-veil-bg: rgba(0, 0, 0, 0.35);
    --dot-ring: rgba(255, 255, 255, 0.75);
    --dot-fill-hover: rgba(255, 255, 255, 0.35);

    --carousel-chip-text: #f4f5f8;
}

/* Dark (default) — only tokens that DIFFER from light live here. Shared,
   theme-independent tokens (brand/on-image/dot/chip/panel-blur) are in :root. */
[data-theme="dark"] {
    /* — Backgrounds — */
    --bg-base: #0e0f13; /* page background */
    --bg-panel: rgba(22, 24, 30, 0.62); /* cards / link rows (translucent) */
    --bg-panel-solid: #16181e; /* mobile drawer, buttons (opaque) */

    /* — Text — */
    --text: #ecedf1; /* headings + body */
    --text-soft: #d1d1d1; /* secondary / lead text */
    --text-faint: #ffffff; /* footer / captions */

    /* — Menu / nav — */
    --menu-text: #a6a9b4; /* idle nav link */
    --menu-text-active: #ecedf1; /* hovered / active nav link */
    --menu-hover-bg: var(--accent-soft); /* hover pill behind link */

    /* — Accents & lines — */
    --accent: #4de0c2;
    /* Derived from --accent so changing the brand color updates the soft
       accent (hover pills, focus glow) automatically — one value, many uses. */
    --accent-soft: color-mix(in srgb, var(--accent) 14%, transparent);
    --border: rgba(255, 255, 255, 0.10);

    /* — Background-image overlay & header — */
    --overlay-top: rgba(14, 15, 19, 0.79);
    --overlay-bottom: rgba(14, 15, 19, 0.98);
    --header-bg: rgba(14, 15, 19, 0.82);

    /* — Monochrome icon/logo inversion —
       Card icons and the optional header logo are black PNGs on transparent
      .  Black art is invisible on the dark
       theme, so dark inverts it to white; light leaves it untouched. */
    --mono-icon-filter: invert(1);
}

/* Light — only tokens that DIFFER from dark; shared tokens are in :root. */
[data-theme="light"] {
    /* — Backgrounds — */
    --bg-base: #ffffff; /* page background */
    /* Cards/rows are more opaque in light mode so text sits on a near-solid
       surface instead of letting the background photo bleed through. */
    --bg-panel: rgba(248, 248, 250, 0.90); /* cards / link rows */
    --bg-panel-solid: #f1f1f4; /* mobile drawer, buttons (opaque) */

    /* — Text — */
    --text: #15161a; /* headings + body */
    --text-soft: #131415; /* secondary / lead text */
    --text-faint: #212123; /* footer / captions */

    /* — Menu / nav — */
    --menu-text: #4b4d55; /* idle nav link */
    --menu-text-active: #15161a; /* hovered / active nav link */
    --menu-hover-bg: var(--accent-soft); /* hover pill behind link */

    /* — Accents & lines — */
    --accent: #0b7d6c;
    /* Derived from --accent. Light mode uses a slightly lower 10% tint. */
    --accent-soft: color-mix(in srgb, var(--accent) 10%, transparent);
    --border: rgba(43, 43, 43, 0.49);

    /* — Background-image overlay & header —
       Heavier than dark: pushes the background photo back so cards/text read
       cleanly in light mode while still showing the image. */
    --overlay-top: rgba(255, 255, 255, 0.47);
    --overlay-bottom: rgba(255, 255, 255, 0.62);
    --header-bg: rgba(255, 255, 255, 0.90);

    /* — Monochrome icon/logo inversion —
       Black art already reads on the white theme — no filter. */
    --mono-icon-filter: none;
}

/* ---- Reset-ish -------------------------------------------------------- */
*,
*::before,
*::after {
    box-sizing: border-box;
}

/* No text I-beam anywhere — mouse stays a normal arrow over text.
   Interactive elements still get the pointer hand below. */
* {
    cursor: default;
}

a, button, [role="tab"], label, summary,
input[type="checkbox"], input[type="radio"],
.menu-toggle, .theme-toggle {
    cursor: pointer;
}

/* No blinking text caret on non-editable content. Clicking text focuses the
   <main> container (it's focusable so the skip-link can target it), which would
   otherwise show an editing caret. Real form fields keep a normal caret + cursor. */
* {
    caret-color: transparent;
}

input, textarea, [contenteditable] {
    caret-color: auto;
    cursor: text;
}

html {
    /* Root font-size = the global size dial (--root-scale in :root). All rem
       units resolve against this, so changing the token rescales the whole
       site. Must live here on `html`, not body — rem is anchored to the root. */
    font-size: var(--root-scale);

    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;

    /* Reserve vertical scrollbar space even on tabs that do not need scrolling.
       Without this, switching from a short tab to a tall tab makes the browser
       add the right scrollbar, shrink the layout viewport, and visually squish
       / shift the centered content by the scrollbar width. */
    scrollbar-gutter: stable;
}

html, body {
    overscroll-behavior-y: none;
}

html.site-booting body {
    opacity: 0;
}

html.site-ready body {
    opacity: 1;
    transition: opacity 100ms ease;
}

/* Language swap (switchLocale): only the regions whose text actually changes
   fade out and back in — the page copy and the desktop tabs. The background, the
   brand, the header controls, the theme and the scroll position all stay put, so
   switching language reads as a soft crossfade of the copy rather than the
   full-page blink a reload gives.
   Deliberately NOT applied to #navMobile: the drawer owns its own transform
   transition for opening/closing, and it is closed before a swap anyway.
   The duration must match SWAP_FADE_MS in engine.js — the fade-out has to have
   finished before the DOM underneath it is rebuilt.

   #siteFooter is deliberately NOT in either rule. It's one short line pinned
   under the content, so fading it doesn't read as a crossfade — it reads as the
   footer blinking out of the page and back. Its text is simply replaced in
   place, under the cover of the fade happening above it.

   NOTE for sites that give `.section` a visible surface (a card: background,
   border, blur): fading `#main` fades that surface too, so the page background
   flashes through where the card was. Those builds should fade the card's
   CHILDREN instead — `#main > .section > *` — which is what m-remis.github.io
   does. The plain template has no card, so `#main` is correct here. */
#main,
#navDesktop {
    transition: opacity 90ms ease;
}

html.site-swapping #main,
html.site-swapping #navDesktop {
    opacity: 0;
}

/* Fallback for older browsers that do not support scrollbar-gutter.
   Keep this on the root scroll container, not body, so the page layout width
   stays consistent between short and long tabs. */
@supports not (scrollbar-gutter: stable) {
    html {
        overflow-y: scroll;
    }
}

body {
    margin: 0;
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    font-family: var(--font-sans);
    font-size: clamp(0.94rem, 0.9rem + 0.15vw, 1rem);
    line-height: 1.58;
    color: var(--text);
    background: var(--bg-base);
    overflow-x: hidden;
    /* Don't let late-loading content (background image, map iframe) pin the
       viewport to a nearby element and drag the scroll position on refresh. */
    overflow-anchor: none;
    transition: background-color var(--transition), color var(--transition);
}

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

    *, *::before, *::after {
        animation-duration: 0.001ms !important;
        transition-duration: 0.001ms !important;
    }
}

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

a {
    color: inherit;
    text-decoration: none;
}

/* Kill fake mobile/touch focus rings.
   Focus styling appears only after real keyboard navigation. */
:focus {
    outline: none;
}

body.keyboard-nav :focus-visible {
    outline: var(--focus-ring);
    outline-offset: var(--focus-offset);
    border-radius: 4px;
}

body:not(.keyboard-nav) :focus-visible {
    outline: none;
}

/* Extra protection for mobile Firefox / touch browsers. */
@media (hover: none), (pointer: coarse) {
    a:focus,
    button:focus,
    [role="tab"]:focus,
    a:focus-visible,
    button:focus-visible,
    [role="tab"]:focus-visible {
        outline: none;
    }
}

/* Remove the grey/blue tap-highlight rectangle mobile browsers draw on tap. */
a, button, [role="tab"], .menu-toggle, .theme-toggle {
    -webkit-tap-highlight-color: transparent;
}

/* ---- Hover / press policy ---------------------------------------------
   Hover styling lives in per-component `@media (hover: hover)` blocks, so
   touch devices never get sticky hover states — no per-component "undo on
   touch" rules needed. Press feedback (:active) stays unconditional so taps
   still respond. The bordered controls share one press rule: */
.theme-toggle:active,
.menu-toggle:active,
.lang-dd__current:active,
.nav-mobile__close:active,
.map-open-row a:active,
.carousel__nav:active,
.gallery__item:active {
    border-color: var(--accent);
}

/* ---- Background ------------------------------------------------------- */
.bg,
.bg-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    width: 100%;
    height: 100vh; /* fallback */
    height: 100lvh; /* LARGE viewport height: the tallest state (URL bar hidden), and it does NOT change as the bar slides — so no resize on scroll */
    z-index: -2;
    pointer-events: none;
}

/* Two stacked background layers so we can crossfade between images without a
   flash. JS toggles .is-active to fade one in while the other fades out, and
   each active layer runs a slow Ken Burns drift/zoom. */
.bg {
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    opacity: 0;
    transition: opacity 1200ms ease;
    /* Continuous, seamless drift — zoom in and back out forever, no snap.
       Runs on both layers always; opacity (.is-active) decides which shows. */
    /* Ken Burns drift — toggle by commenting line below. */
    animation: bg-drift 15s ease-in-out infinite alternate;
}

.bg.is-active {
    opacity: 1;
}

@keyframes bg-drift {
    from {
        transform: scale(1.02) translate(0, 0);
    }
    to {
        transform: scale(1.12) translate(-1.5%, -1.5%);
    }
}

@media (prefers-reduced-motion: reduce) {
    .bg {
        animation: none;
    }
}

.bg-overlay {
    z-index: -1;
    background: linear-gradient(to bottom, var(--overlay-top), var(--overlay-bottom));
    backdrop-filter: saturate(112%);
    transition: background var(--transition);
}

/* ---- Skip link -------------------------------------------------------- */
.skip-link {
    position: absolute;
    left: 50%;
    top: -3rem;
    transform: translateX(-50%);
    background: var(--accent);
    color: #fff;
    padding: 0.5rem 1rem;
    border-radius: var(--radius-sm);
    z-index: 100;
    transition: top var(--transition);
}

.skip-link:focus {
    top: 0.75rem;
}

/* ---- Header ----------------------------------------------------------- */
.site-header {
    position: sticky;
    top: 0;
    z-index: 40;
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0.9rem var(--gutter);
    background: var(--header-bg);
    -webkit-backdrop-filter: blur(var(--panel-blur));
    backdrop-filter: blur(var(--panel-blur));
    border-bottom: 1px solid var(--border);
}

.brand {
    font-family: var(--font-brand);
    font-size: 1.6rem;
    letter-spacing: 0.01em;
    font-style: normal;
    margin-right: auto;
    transition: opacity var(--transition);
}

@media (hover: hover) {
    .brand:hover {
        opacity: 0.7;
    }
}

/* Brand as a PNG logo — set by renderNav() when the spec has `brandImage`.
   The image is height-bound to roughly the text brand's visual size so the
   header layout and header-fit math stay unchanged; width follows the asset's
   aspect ratio. Monochrome black logos get the same dark-theme inversion as
   card icons via --mono-icon-filter. */
.brand--image {
    display: flex;
    align-items: center;
}

.brand__img {
    display: block;
    height: 2.1rem;
    width: auto;
    max-width: 8rem;
    object-fit: contain;
    filter: var(--mono-icon-filter);
    user-select: none;
    -webkit-user-select: none;
}

.nav-desktop {
    display: flex;
    gap: 0.35rem;
    flex: none;
}

.nav-desktop a {
    position: relative;
    padding: 0.45rem 0.85rem;
    font-size: 0.95rem;
    color: var(--menu-text);
    border-radius: var(--radius-sm);
    transition: color var(--transition), background var(--transition);
}

@media (hover: hover) {
    .nav-desktop a:hover {
        color: var(--menu-text-active);
        background: var(--menu-hover-bg);
    }
}

.nav-desktop a.is-active {
    color: var(--menu-text-active);
}

.nav-desktop a.is-active::after {
    content: "";
    position: absolute;
    left: 0.85rem;
    right: 0.85rem;
    bottom: 0.15rem;
    height: 2px;
    background: var(--accent);
    border-radius: 2px;
}

.header-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex: none;
}

/* Theme toggle */
.theme-toggle,
.menu-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    border: 1px solid var(--border);
    background: var(--bg-panel);
    backdrop-filter: blur(var(--panel-blur));
    color: var(--text);
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: border-color var(--transition), background var(--transition), transform var(--transition);
}

@media (hover: hover) {
    .theme-toggle:hover,
    .menu-toggle:hover,
    .lang-dd__current:hover {
        border-color: var(--accent);
    }
}

/* Language switcher — a custom <details> dropdown (built by initLang). The
   summary is the frosted "button" (same chrome as the theme toggle); the list
   is a fully site-styled panel, since native <select> option lists can't be
   themed. Ships hidden; the container is revealed only for a multi-locale site. */
.lang-switcher[hidden] {
    display: none;
}

.lang-dd {
    position: relative;
}

/* Trigger button (the <summary>) — the frosted control the site already uses. */
.lang-dd__current {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    height: 2.5rem;
    padding: 0 0.7rem;
    border: 1px solid var(--border);
    background-color: var(--bg-panel);
    -webkit-backdrop-filter: blur(var(--panel-blur));
    backdrop-filter: blur(var(--panel-blur));
    color: var(--text);
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-family: var(--font-serif);
    font-weight: 700;
    font-size: 0.8rem;
    letter-spacing: 0.04em;
    user-select: none;
    list-style: none; /* drop the default disclosure triangle */
    transition: border-color var(--transition), background-color var(--transition);
}

/* Remove the WebKit/Safari disclosure marker too. */
.lang-dd__current::-webkit-details-marker {
    display: none;
}

.lang-dd__caret {
    width: 0.7rem;
    height: 0.7rem;
    transition: transform var(--transition);
}

.lang-dd[open] .lang-dd__caret {
    transform: rotate(180deg);
}

/* Option list — a frosted panel dropped below the button, matching the cards
   and mobile drawer. Sits above the sticky header. */
.lang-dd__list {
    position: absolute;
    top: calc(100% + 0.4rem);
    right: 0;
    z-index: 60;
    min-width: 100%;
    margin: 0;
    padding: 0.3rem;
    list-style: none;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background-color: var(--bg-panel-solid);
    -webkit-backdrop-filter: blur(var(--panel-blur));
    backdrop-filter: blur(var(--panel-blur));
    box-shadow: 0 14px 32px -14px rgba(0, 0, 0, 0.55);
}

.lang-dd__option {
    display: block;
    width: 100%;
    padding: 0.45rem 0.95rem;
    border: none;
    background: transparent;
    color: var(--menu-text);
    font-family: var(--font-serif);
    font-weight: 500;
    font-size: 0.8rem;
    letter-spacing: 0.06em;
    text-align: left;
    border-radius: calc(var(--radius-sm) - 2px);
    cursor: pointer;
    transition: color var(--transition), background var(--transition);
}

@media (hover: hover) {
    .lang-dd__option:hover {
        color: var(--menu-text-active);
        background: var(--menu-hover-bg);
    }
}

.lang-dd__option.is-active {
    color: var(--accent);
}

/* Theme toggle icon — one SVG that morphs between a crescent moon (dark mode,
   default) and a rayed sun (light mode). The "bite" is a mask circle that
   slides off-shape in light mode; the rays fade + scale in. Pure CSS, driven
   by [data-theme="light"] on <body>. The global prefers-reduced-motion rule
   collapses these transitions to an instant swap. */
.theme-toggle__icon {
    width: 1.25rem;
    height: 1.25rem;
    color: var(--text);
    overflow: visible;
}

.theme-toggle__cut {
    /* Slides between biting into the core (crescent) and fully clear (disc). */
    transition: transform var(--transition);
    transform: translateX(0);
}

.theme-toggle__rays {
    opacity: 0;
    transform: scale(0.4);
    transform-origin: center;
    transition: opacity var(--transition), transform var(--transition);
}

/* Light mode → sun: the mask bite slides fully clear (disc), rays bloom in.
   The core radius is intentionally NOT animated: animating the SVG `r`
   property isn't a valid CSS length without a unit and isn't supported in
   Firefox, and the bite-slide + rays carry the morph on their own. */
[data-theme="light"] .theme-toggle__cut {
    transform: translateX(20px);
}

[data-theme="light"] .theme-toggle__rays {
    opacity: 1;
    transform: scale(1);
}

/* Mobile menu toggle (hidden on desktop) */
.menu-toggle {
    display: none;
    flex-direction: column;
    gap: 4px;
}

.menu-toggle__bar {
    width: 1.1rem;
    height: 2px;
    background: var(--text);
    border-radius: 2px;
    transition: transform var(--transition), opacity var(--transition);
}

.menu-toggle[aria-expanded="true"] .menu-toggle__bar:nth-child(1) {
    transform: translateY(6px) rotate(45deg);
}

.menu-toggle[aria-expanded="true"] .menu-toggle__bar:nth-child(2) {
    opacity: 0;
}

.menu-toggle[aria-expanded="true"] .menu-toggle__bar:nth-child(3) {
    transform: translateY(-6px) rotate(-45deg);
}

/* ---- Brand + socials (header) ----------------------------------------- */
/* The brand anchor is wrapped in .brand-wrap by JS so social icons can sit
   right beside it. The wrapper takes over the brand's auto-margin so the
   header layout (nav + actions pushed right) is unchanged. */
.brand-wrap {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-right: auto;
    /* Hold natural width (don't shrink or clip): the socials keep their real
       size so they genuinely approach the nav as the window narrows. JS
       (initHeaderFit) watches that gap and switches to the hamburger before
       they touch, hiding both the nav and these socials. */
    flex: none;
}

.brand-wrap .brand {
    margin-right: 0;
    flex: none;
}

/* ---- Socials ---------------------------------------------------------- */
.socials {
    display: flex;
    align-items: center;
    gap: 0.25rem;
}

.socials__link {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.4rem 0.55rem;
    border-radius: var(--radius-sm);
    font-size: 0.85rem;
    color: var(--menu-text);
    transition: color var(--transition), background var(--transition);
}

@media (hover: hover) {
    .socials__link:hover {
        color: var(--menu-text-active);
        background: var(--menu-hover-bg);
    }
}

.socials__label {
    line-height: 1;
}

/* Icon takes its brand color; the label stays in the readable menu color.
   The svg uses fill="currentColor", so we set color only on the svg here. */
.socials__link svg {
    display: block;
    flex: none;
}

.socials__link--instagram svg {
    color: var(--brand-instagram);
}

.socials__link--youtube svg {
    color: var(--brand-youtube);
}

.socials__link--spotify svg {
    color: var(--brand-spotify);
}

.socials__link--phone svg {
    color: var(--brand-phone);
}

.socials__link--email svg {
    color: var(--brand-email);
}

/* GitHub is a monochrome mark — it follows the text color so it stays legible
   in both themes; LinkedIn keeps its brand blue (a mid blue readable on both). */
.socials__link--github svg {
    color: var(--text);
}

.socials__link--linkedin svg {
    color: var(--brand-linkedin);
}

/* ---- Mobile nav ------------------------------------------------------- */
.nav-mobile {
    position: fixed;
    top: 0;
    right: 0;
    z-index: 50;
    width: min(78vw, 20rem);
    height: 100vh;
    height: 100dvh;
    padding: 5rem 1.5rem 2rem;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    /* Frosted, like the header — translucent panel + blur instead of solid. */
    background: var(--header-bg);
    -webkit-backdrop-filter: blur(var(--panel-blur));
    backdrop-filter: blur(var(--panel-blur));
    border-left: 1px solid var(--border);
    transform: translateX(105%);
    transition: transform var(--transition);
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* Close button pinned top-right inside the drawer. */
.nav-mobile__close {
    position: absolute;
    top: 1rem;
    right: 1rem;
    width: 2.5rem;
    height: 2.5rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-panel);
    -webkit-backdrop-filter: blur(var(--panel-blur));
    backdrop-filter: blur(var(--panel-blur));
    color: var(--text);
    font-size: 1.4rem;
    line-height: 1;
    transition: border-color var(--transition), transform var(--transition);
}

@media (hover: hover) {
    .nav-mobile__close:hover {
        border-color: var(--accent);
    }
}

.nav-mobile.is-open {
    transform: translateX(0);
}

.nav-mobile a {
    padding: 0.9rem 1rem;
    font-size: 1.1rem;
    color: var(--menu-text);
    border-radius: var(--radius-sm);
    transition: color var(--transition), background var(--transition);
}

.nav-mobile a.is-active {
    color: var(--menu-text-active);
    background: var(--menu-hover-bg);
}

@media (hover: hover) {
    .nav-mobile a:hover {
        color: var(--menu-text-active);
        background: var(--menu-hover-bg);
    }
}

/* Social links sit at the bottom of the drawer, stacked one per row, styled
   exactly like the header version (colored icon + visible label). The generic
   ".nav-mobile a" rule above sets the row padding/size; these just lay them out
   vertically and restore the inline icon+label gap. */
.nav-mobile .socials {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0.25rem;
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--border);
}

.nav-mobile .socials .socials__link {
    gap: 0.6rem;
    font-size: 1.1rem;
}

.nav-mobile__scrim {
    position: fixed;
    inset: 0;
    z-index: 45;
    background: rgba(0, 0, 0, 0.45);
    opacity: 0;
    transition: opacity var(--transition);
    /* When visible, swallow scroll/touch so the page behind can't be scrolled. */
    touch-action: none;
    overscroll-behavior: contain;
}

/* ---- Content ---------------------------------------------------------- */
/* The content column is the single site width; blocks fill it and the gutter
   keeps them off the edges on small screens. */
.content {
    width: 100%;
    max-width: var(--content-width);
    margin: 0 auto;
    padding: clamp(2rem, 6vw, 3rem) var(--gutter) 3rem;
    outline: none;
    flex: 1 0 auto;
}

.section {
    margin-bottom: clamp(3rem, 8vw, 5rem);
}

.section:last-child {
    margin-bottom: 0;
}

/* Tab panels: hidden ones are removed; active one fades in gently */
.content > .section[hidden] {
    display: none;
}

/* Off-screen embed warm-up — driven by warmUpIframes() in engine.js, which
   pre-loads a hidden tab's map/video iframes so opening that tab is instant.
   A display:none panel gives its iframes no box, and a zero-size YouTube
   player locks onto its smallest (cropped, pixelated) poster rendition, so
   for the few seconds the embeds are loading the panel is laid out at its
   real width instead — parked outside the viewport.

   Two properties are load-bearing, and both are the way they are because the
   obvious alternatives failed:
   - `left: -200vw` (not `visibility: hidden`) is what keeps the panel from
     being seen. `visibility` is inherited but a DESCENDANT CAN TURN IT BACK
     ON, and the carousel does exactly that (`.carousel__slide.is-active,
     .carousel--single .carousel__slide { visibility: visible }`) — a warming
     panel's slideshow painted straight over the live page. Being genuinely
     off-screen can't be overridden from the inside. `opacity: 0` backs it up
     for the same reason: it applies to the subtree as a group.
   - `position: fixed` keeps the panel out of the scrollable area, so warming
     can never add a scrollbar or shift the centered layout. Fixed elements
     don't contribute to scroll overflow, so the negative offset is free.
   The [hidden] in the selector means opening the tab mid-warm cancels all of
   this by itself. */
.content > .section[hidden].is-warming {
    display: block;
    position: fixed;
    top: 0;
    left: -200vw;
    width: var(--warm-width, 100%);
    opacity: 0;
    pointer-events: none;
}

.section--active {
    animation: panel-in 150ms cubic-bezier(0.4, 0, 0.2, 1);
}

@keyframes panel-in {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

@media (prefers-reduced-motion: reduce) {
    .section--active {
        animation: none;
    }
}

/* ---- Block layout: uniform spacing ----------------------------------- */
/* Every rendered block is wrapped by JS in a .block element. This is the
   single source of truth for the vertical rhythm between blocks. Adding a new
   block type needs no new spacing CSS — it inherits .block + .block. Blocks
   fill the content column; control a block's own width inside its builder/CSS
   if it ever needs to be narrower. */
.block {
    width: 100%;
    margin-inline: auto;
}

/* Uniform gap between consecutive blocks. Replaces the old per-pair adjacency
   rules (.section__text + .card-grid, etc.) with one rule that covers every
   combination, present and future. */
.block + .block {
    margin-top: var(--block-gap);
}

/* A section title sits above the first block; give the first block air below
   the title without needing the block to know about it. */
.section__title + .block {
    margin-top: 0;
}

/* Shared block heading + intro line. Seven block types (table, faq, slideshow,
   gallery, hours, review, text) open with an optional serif name and a prose blurb;
   these were six identical rule pairs. Builders now emit `block__name` /
   `block__blurb` alongside the type-specific class (e.g. "block__name
   table__name"), so the common look lives here once and a new block's heading
   is styled for free. Override per type by adding rules to the specific class. */
.block__name {
    font-family: var(--font-serif);
    font-weight: 500;
    font-size: 1.25rem;
    margin: 0 0 0.6rem;
    color: var(--text);
}

.block__blurb {
    margin: 0 0 0.75rem;
}

/* Intro / hero */
.intro__eyebrow {
    font-size: 0.85rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--text-faint);
    margin: 0 0 0.75rem;
}

.intro__title {
    font-family: var(--font-serif);
    font-weight: 600;
    font-size: clamp(2.4rem, 7vw, 3.6rem);
    line-height: 1.05;
    margin: 0 0 1rem;
    letter-spacing: -0.01em;
    text-transform: uppercase;
}

.intro__title em {
    font-style: normal;
    color: var(--accent);
}

.intro__lead {
    font-size: clamp(1.1rem, 1rem + 0.6vw, 1.3rem);
    color: var(--text-soft);
    margin: 0;
}

/* Section headings */
.section__title {
    font-family: var(--font-serif);
    font-weight: 600;
    font-size: clamp(1.6rem, 4vw, 2rem);
    margin: 0 0 1.25rem;
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
    text-transform: uppercase;
    letter-spacing: 0.01em;
}

/* Accent via <em> in the spec's section `title` — same treatment as
   .intro__title em: accent color, no italics. NOTE: .section__title is a
   flex container, so spec titles should wrap the WHOLE text in one <em>
   (a partial <em> would split the title into gap-separated flex items). */
.section__title em {
    font-style: normal;
    color: var(--accent);
}

/* Generic prose */
.prose p {
    color: var(--text-soft);
    margin: 0 0 1rem;
}

.prose p:last-child {
    margin-bottom: 0;
}
/* Card grid (services / projects) */
.card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
    gap: 1rem;
}

.card {
    padding: 1.35rem 1.4rem;
    background: var(--bg-panel);
    backdrop-filter: blur(var(--panel-blur));
    border: 2px solid var(--border);
    border-radius: var(--radius);
    /* transform uses a short ease-OUT (fast start, gentle settle) so the 3px
       lift feels crisp, not draggy; border-color keeps the shared curve. The
       lift is composited (see will-change on a.card__link) so moving the
       blurred panel doesn't repaint the backdrop-filter every frame — that
       repaint was the stutter. */
    transition: transform 140ms cubic-bezier(0.2, 0.7, 0.3, 1), border-color var(--transition);
    display: flex;
    flex-direction: column;
}

/* Only clickable cards react to the cursor. A plain (non-linked) .card stays
   put — reacting like a button would be a false affordance (user hovers, it
   lifts and glows, click does nothing). The lift/accent is reserved for
   a.card__link, which is what buildCards adds only when the card is a link. */
@media (hover: hover) {
    a.card__link:hover {
        transform: translateY(-3px);
        border-color: var(--accent);
    }
}

/* Card icon — set by buildCards() when an item has an `icon`: a monochrome
   black-on-transparent PNG, sat to the LEFT of the text
   and vertically centered against it (.card--icon switches the card from the
   default column to a row). The --mono-icon-filter token inverts it to white
   in dark theme and leaves it alone in light, so one asset works on both
   themes. */
.card--icon {
    flex-direction: row;
    align-items: center;
    gap: 1.1rem;
}

.card__icon {
    display: block;
    width: 4rem;
    height: 4rem;
    flex: none;
}

.card__icon img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    filter: var(--mono-icon-filter);
    user-select: none;
    -webkit-user-select: none;
}

/* The text column inside an icon card. Mirrors the plain card's column flow
   so .card__meta's margin-top:auto (stick to bottom) keeps working; min-width
   lets long titles wrap instead of pushing the icon out of the card. */
.card__body {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-width: 0;
}

.card h3 {
    margin: 0 0 0.4rem;
    font-size: 1.1rem;
    font-weight: 600;
}

.card p {
    margin: 0;
    color: var(--text-soft);
    font-size: 0.95rem;
}

a.card__link {
    display: flex;
    cursor: pointer;
    position: relative;
    will-change: transform;
}

/* Persistent "go" cue on clickable cards, so the affordance survives on touch
   (no hover on mobile) — not just on desktop hover. The arrow rides on the
   accent meta line and nudges right on hover. */
a.card__link .card__meta::after {
    /* content: var(--unicode-arrow); */ /* arrow cue intentionally disabled */
    margin-left: 0.4rem;
    display: inline-block;
    font-size: 2.5em;
    line-height: 0.5;
    vertical-align: -0.09em;
    transition: transform var(--transition);
}

@media (hover: hover) {
    a.card__link:hover .card__meta::after {
        transform: translateX(3px);
    }
}

/* Fallback for clickable cards with no meta line: a corner arrow on the card
   itself, so the cue is guaranteed regardless of content. (The card is
   position:relative above to anchor this.) */
a.card__link:not(:has(.card__meta))::after {
    /* content: var(--unicode-arrow); */
    position: absolute;
    top: 1.1rem;
    right: 1.2rem;
    font-size: 1.2rem;
    line-height: 1;
    color: var(--accent);
    transition: transform var(--transition);
}

@media (hover: hover) {
    a.card__link:not(:has(.card__meta)):hover::after {
        transform: translateX(3px);
    }
}

.card__meta {
    display: inline-block;
    margin-top: auto;
    padding-top: 0.85rem;
    font-size: 0.8rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--accent);
}

/* The accent meta line doubles as the "this card is a link" cue (paired with
   the hover lift on a.card__link). On a plain, non-clickable card the same
   accent would be a false affordance — mute it to the caption color so the
   accent keeps meaning "clickable" everywhere. */
article.card .card__meta {
    color: var(--text-faint);
}

/* Link list (where to find me / contact) */
.link-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 0.4rem;
}

.link-list a {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.85rem 1.1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-panel);
    backdrop-filter: blur(var(--panel-blur));
    color: var(--text);
    transition: border-color var(--transition), transform var(--transition);
}

@media (hover: hover) {
    .link-list a:hover {
        border-color: var(--accent);
        transform: translateX(3px);
    }
}

/* Kind + value stack in a column next to the icon */
.link-list__text {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    min-width: 0;
}

/* Small muted "what is this" line ("Phone", "Instagram", …) */
.link-list__kind {
    font-size: 0.72rem;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--text-faint);
}

/* Label hugs the icon on the left */
.link-list .label {
    font-weight: 500;
    overflow-wrap: anywhere;
}

/* Persistent clickability cue — same arrow language as redirect cards, so
   contact rows never need a "these are clickable" sentence in the copy. */
.link-list__arrow {
    margin-left: auto;
    flex: none;
    display: inline-flex;
    align-items: center;
    color: var(--accent);
}

.link-list__arrow::after {
    /* content: var(--unicode-arrow); */
    font-size: 1.5rem;
    line-height: 1;
}

/* When a handle is present it takes the auto-push; the arrow just trails it. */
.link-list a:has(.handle) .link-list__arrow {
    margin-left: 0.75rem;
}

/* layout: "grid" — rows become side-by-side tiles (auto-stacks when narrow). */
.link-list--grid {
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 0.6rem;
}

.link-list--grid a {
    padding: 1.05rem 1.15rem;
}

@media (hover: hover) {
    .link-list--grid a:hover {
        transform: translateY(-2px);
    }
}

/* Handle pushed to the right end. margin-left:auto does the "space-between"
   job without forcing a wrap. */
.link-list .handle {
    margin-left: auto;
    color: var(--text-faint);
    font-size: 0.9rem;
    text-align: right;
    overflow-wrap: anywhere;
}

/* Below this width, EVERY button stacks the handle under the label, so they
   all stay consistent (not one wrapped, others not). Tune the 26rem to taste. */
@media (max-width: 26rem) {
    .link-list a {
        flex-wrap: wrap;
    }

    .link-list .handle {
        margin-left: 0; /* drop to its own line, align under label */
        flex-basis: 100%;
        text-align: left;
        padding-left: calc(2rem + 0.6rem); /* indent under the label, past the icon */
        order: 10; /* wrap below; the arrow stays up on the value line */
    }

    /* With the handle on its own line, the arrow takes the auto-push again. */
    .link-list a:has(.handle) .link-list__arrow {
        margin-left: auto;
    }
}

.link-list__icon {
    display: inline-flex;
    align-items: center;
    flex: none;
    margin-right: 0.75rem;
    color: var(--text-soft);
}

/* Per-platform brand colors for contact-row icons. Brand colors, not theme
   tokens — intentionally identical in light and dark mode, and kept on hover. */
.link-list--phone .link-list__icon {
    color: var(--brand-phone);
}

.link-list--email .link-list__icon {
    color: var(--brand-email);
}

.link-list--instagram .link-list__icon {
    color: var(--brand-instagram);
}

.link-list--youtube .link-list__icon {
    color: var(--brand-youtube);
}

.link-list--spotify .link-list__icon {
    color: var(--brand-spotify);
}

.link-list--github .link-list__icon {
    color: var(--text);
}

.link-list--linkedin .link-list__icon {
    color: var(--brand-linkedin);
}

/* ---- Table (price list / structured rows) ----------------------------- */
/* A structured table rendered by buildTable() in engine.js from a `table`
   block: an optional name + blurb, then `headings` (the columns) and `rows`
   (arrays of cells in heading order). Colors come from the same theme tokens
   as cards and the link list, so it re-themes automatically. The .table-scroll
   wrapper lets a wide table scroll sideways on narrow screens instead of
   squashing columns or forcing the whole page wider.

   On mobile (<= 640px) the table is restructured into stacked row-cards — see
   the stacking block inside the responsive section near the bottom. The base
   rules here are the DESKTOP table; do not put display:block / card styling
   here or it leaks onto desktop. */

/* Scroll container: holds the panel frame and clips the corners. On desktop a
   very wide table can scroll sideways here; on mobile this is neutralized and
   the rows stack instead (see responsive section). */
.table-scroll {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow-x: auto;
    background: var(--bg-panel);
    backdrop-filter: blur(var(--panel-blur));
}

/* Collapsible table (opt-in via `collapsible: true` in the spec): styled like
   a FAQ item — the block carries the frame, the name renders as a flat toggle
   row inside it, and the inner .table-scroll frame is stripped so the open
   state reads as ONE table continuing under the toggle row. The +/- icon is
   drawn by the shared .faq__icon rules (they also target .table__toggle-icon);
   the panel collapses via [hidden] set in JS. */
.table-block--collapsible {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-panel);
    -webkit-backdrop-filter: blur(var(--panel-blur));
    backdrop-filter: blur(var(--panel-blur));
    overflow: hidden;
}

/* Consecutive collapsible tables fuse into one accordion list with the FAQ's
   tight gap. Progressive enhancement: no :has() support = normal block gap. */
.block:has(> .table-block--collapsible) + .block:has(> .table-block--collapsible) {
    margin-top: 0.5rem;
}

.table__name--toggle {
    margin: 0;
}

/* The toggle row + its hover live in the shared .faq__q/.table__toggle rules
   (FAQ section) — like the +/- icon, the accordion look is styled once. */

/* Open panel: divider under the toggle row, like .faq__a. */
.table__panel {
    border-top: 1px solid var(--border);
}

.table__panel .block__blurb {
    margin: 0;
    padding: 0.85rem 1.1rem 0;
}

/* The outer block already carries the frame — strip the inner one, and the
   last row's divider so it doesn't double with the frame's bottom edge. */
.table-block--collapsible .table-scroll {
    border: 0;
    border-radius: 0;
    background: transparent;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
}

.table-block--collapsible .data-table tbody tr:last-child td {
    border-bottom: 0;
}

.data-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.8rem;
}

.data-table th,
.data-table td {
    padding: 0.85rem 1.1rem;
    text-align: left;
    vertical-align: top;
    border-bottom: 1px solid var(--border);
}

/* Header row: serif heading style, consistent with section/slideshow names. */
.data-table thead th {
    font-family: var(--font-serif);
    font-weight: 500;
    font-size: 1.05rem;
    color: var(--text);
    white-space: nowrap;
}

/* Body cells default to the softer body text color, like card/prose copy. */
.data-table tbody td {
    color: var(--text-soft);
}

/* Last column reads as a price/value: right-aligned, accented, won't wrap.
   width:1% + nowrap = shrink-to-fit, so the column sits at the panel's right
   edge in EVERY table instead of drifting with each table's longest cell. */
.data-table__value {
    text-align: right;
    white-space: nowrap;
    width: 1%;
}

.data-table td.data-table__value {
    color: var(--accent);
    font-weight: 600;
}

/* Gentle row highlight on hover, matching the link-list/card feel. */
.data-table tbody tr {
    transition: background var(--transition);
}

.data-table tbody tr:hover {
    /* background: var(--menu-hover-bg); */
}

/* ---- Wide-table mobile accordion (.table-cards) ----------------------- */
/* The mobile mirror of a 3+ column table, built by buildTable alongside the
   <table>. Hidden by default; the mobile @media swaps the two. Each item is a
   framed disclosure (shared .table-cards__toggle / __icon rules with the FAQ)
   whose panel lists the row's other columns as label → value pairs. */
.table-cards {
    display: none;
}

.table-cards__item {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-panel);
    -webkit-backdrop-filter: blur(var(--panel-blur));
    backdrop-filter: blur(var(--panel-blur));
    overflow: hidden;
}

/* The clicked row: the package name in the serif heading style, like a table
   name/th, so a collapsed list reads as a column of titles. */
.table-cards__name {
    font-family: var(--font-serif);
    font-size: 1.05rem;
    color: var(--text);
}

/* Detail panel: a thin divider from the toggle, then one line per column. */
.table-cards__panel {
    padding: 0.3rem 1.1rem 0.85rem;
    border-top: 1px solid var(--border);
}

.table-cards__row {
    padding: 0.55rem 0;
    border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
}

.table-cards__row:last-child {
    border-bottom: none;
}

/* Column heading as a small eyebrow above its value. */
.table-cards__label {
    display: block;
    margin-bottom: 0.15rem;
    font-family: var(--font-serif);
    font-size: 0.72rem;
    font-weight: 500;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-soft);
    opacity: 0.7;
}

.table-cards__value {
    display: block;
    color: var(--text-soft);
    overflow-wrap: anywhere;
}

/* Price column keeps the accent treatment it has in the desktop table. */
.table-cards__row--value .table-cards__value {
    color: var(--accent);
    font-weight: 600;
}

/* ---- FAQ / accordion -------------------------------------------------- */
/* Rendered by buildFaq() in engine.js from a `faq` block: optional name +
   blurb, then `items` of { q, a }. Each row is a native <button> (the
   question) toggling a `.faq__a` panel (the answer). Uses the same panel
   tokens as cards/links/table so it re-themes in light/dark for free. The
   +/- icon is drawn with pseudo-elements (no icon font / SVG asset). */

.faq__list {
    display: grid;
    gap: 0.5rem;
}

.faq__item {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-panel);
    -webkit-backdrop-filter: blur(var(--panel-blur));
    backdrop-filter: blur(var(--panel-blur));
    overflow: hidden;
}

/* Shared accordion toggle button: the FAQ question row and the collapsible
   table's name row (same deduplication as the +/- icon rules below). Fills
   its row, sits flat — the .faq__item / .table-block--collapsible frames it;
   the 1.1rem inline padding lines the table name up with its cells. */
.faq__q,
.table__toggle,
.table-cards__toggle {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.95rem 1.1rem;
    border: 0;
    background: transparent;
    cursor: pointer;
    text-align: left;
    font: inherit;
    color: inherit;
    transition: background var(--transition), color var(--transition);
}

/* Only the type treatment diverges: FAQ questions are sans semibold; the
   table toggle keeps color/font inherited from its serif .block__name h3. */
.faq__q {
    font-weight: 600;
    color: var(--text);
}

@media (hover: hover) {
    .faq__q:hover,
    .table__toggle:hover,
    .table-cards__toggle:hover {
        background: var(--menu-hover-bg);
        color: var(--accent);
    }
}

.faq__q-text,
.table-cards__name {
    flex: 1;
}

/* +/- toggle drawn from two bars; the vertical one hides when expanded.
   Shared with the collapsible table's .table__toggle-icon. */
.faq__icon,
.table__toggle-icon,
.table-cards__icon {
    position: relative;
    flex: none;
    width: 14px;
    height: 14px;
}

.faq__icon::before,
.faq__icon::after,
.table__toggle-icon::before,
.table__toggle-icon::after,
.table-cards__icon::before,
.table-cards__icon::after {
    content: "";
    position: absolute;
    background: var(--accent);
    transition: transform var(--transition), opacity var(--transition);
}

.faq__icon::before,
.table__toggle-icon::before,
.table-cards__icon::before { /* horizontal bar */
    top: 50%;
    left: 0;
    width: 100%;
    height: 2px;
    transform: translateY(-50%);
}

.faq__icon::after,
.table__toggle-icon::after,
.table-cards__icon::after { /* vertical bar */
    left: 50%;
    top: 0;
    width: 2px;
    height: 100%;
    transform: translateX(-50%);
}

/* Expanded: rotate the set and collapse the vertical bar into a minus. */
.faq__q[aria-expanded="true"] .faq__icon::after,
.table__toggle[aria-expanded="true"] .table__toggle-icon::after,
.table-cards__toggle[aria-expanded="true"] .table-cards__icon::after {
    opacity: 0;
    transform: translateX(-50%) rotate(90deg);
}

/* The answer panel. `hidden` (set by JS) collapses it; when shown it gets a
   thin divider from the question and the standard prose treatment inside. */
.faq__a {
    padding: 0 1.1rem 1rem;
    border-top: 1px solid var(--border);
    color: var(--text-soft);
}

.faq__a .prose {
    margin-top: 0.85rem;
}

/* ---- Map: embed mode (live Google iframe) ----------------------------- */
.map-embed {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--bg-panel);
    aspect-ratio: 21 / 9; /* wider, shorter — more of a rectangle */
}

.map-embed iframe {
    width: 100%;
    height: 100%;
    border: 0;
    display: block;
    /* gently match the site mood without breaking legibility */
    filter: saturate(0.95);
    pointer-events: none; /* view-only on all browsers; open via the button */
}

/* ---- Map: static mode (no iframe, clean themed card) ------------------ */
/* Rendered when a `map` block sets mode: "static". A lightweight panel that
   matches cards/table — location name, optional address, and the Open-in-Maps
   button — instead of the noisier live iframe. */
.map-card {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-panel);
    backdrop-filter: blur(var(--panel-blur));
    padding: 1.35rem 1.4rem;
}

.map-card__name {
    margin: 0;
    font-family: var(--font-serif);
    font-weight: 500;
    font-size: 1.25rem;
    line-height: 1.2;
    color: var(--text);
}

.map-card__address {
    margin: 0.4rem 0 0;
    color: var(--text-soft);
    font-size: 0.95rem;
}

/* The Open-in-Maps button row is shared by both map modes. */
.map-open-row {
    margin: 0.75rem 0 0;
}

/* In embed mode the row sits just under the framed map. */
.map-embed + .map-open-row {
    margin-top: 0.75rem;
}

.map-open-row a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 2.6rem;
    padding: 0.65rem 1rem;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: var(--bg-panel);
    color: var(--text);
    font: inherit;
    line-height: 1;
    transition: border-color var(--transition), transform var(--transition);
}

@media (hover: hover) {
    .map-open-row a:hover {
        border-color: var(--accent);
    }
}

@media (max-width: 640px) {
    .map-embed {
        aspect-ratio: 4 / 5;
    }
}

/* ---- Video (YouTube embed) --------------------------------------------- */
/* Rendered by buildVideo() in engine.js from a `video` block. Same framed-
   panel treatment as .map-embed, but fixed 16:9 at every width (video content
   is 16:9 — a portrait crop would letterbox badly) and the iframe stays
   interactive (no pointer-events trick; the visitor must be able to press
   play). Solid panel behind it so any player letterboxing reads as intended,
   matching the carousel viewport. Heading + blurb come from the shared
   .block__name / .block__blurb rules — no CSS needed here. */
.video-embed {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--bg-panel-solid);
    aspect-ratio: 16 / 9;
}

.video-embed iframe {
    width: 100%;
    height: 100%;
    border: 0;
    display: block;
}

/* ---- Carousel / slideshow --------------------------------------------- */
/* Reusable image slideshow rendered by buildCarousel() in engine.js. Used by
   a `slideshow` block. One slide = a plain framed image (.carousel--single, no
   controls); two or more = manual carousel with prev/next buttons, dots, and an
   "n / total" chip. Colors come from the theme tokens above so it re-themes and
   matches cards/map automatically. */

.carousel {
    position: relative;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--bg-panel);
    backdrop-filter: blur(var(--panel-blur));

    /* One canonical IMAGE shape. The chin (caption) is no longer a hardcoded
       height: slides are grid-stacked in the viewport below, so the frame is
       image (fixed ratio) + the tallest caption in this carousel. Still no
       per-slide jumping — the container is sized by the tallest slide, once,
       and never resizes as slides change. */
    --carousel-ratio: 16 / 10;
}

.carousel__viewport {
    /* Grid-stacking replaces the old absolute/inset-0 stack. Every slide
       occupies the same 1/1 cell, so they overlap for the crossfade AND the
       container auto-sizes to the tallest slide. This is what lets captions
       be content-sized (no fixed --carousel-caption-height) without the
       slideshow resizing between slides. */
    display: grid;
    background: var(--bg-panel-solid);
    overflow: hidden;

    /* Horizontal belongs to initSwipe, vertical stays the browser's page
       scroll. Load-bearing — see the swipe gotcha in AGENTS.md: drop this and
       the browser can claim the whole gesture, so the swipe never fires. */
    touch-action: pan-y;
}

/* All slides stay in the stack; opacity shows/hides. A slide is a 2-row grid:
   row 1 = image at the canonical ratio, row 2 = chin, which flexes to fill
   whatever the tallest caption claimed — so chins match across slides. */
.carousel__slide {
    grid-area: 1 / 1;
    min-width: 0;
    margin: 0;

    display: grid;
    grid-template-rows: auto 1fr;

    /* Keep all slides in the stack.
       Opacity handles showing/hiding.
       This is what allows real overlap. */
    opacity: 0;

    transition: opacity 500ms ease-in-out;

    pointer-events: none;
}

.carousel__slide.is-active,
.carousel--single .carousel__slide {
    opacity: 1;
    pointer-events: auto;
    visibility: visible;
}

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

/* Expand button wraps the image so it's a real, focusable control that opens
   the lightbox. The canonical ratio now lives HERE, not on the viewport —
   the image area is the fixed part of the frame, the chin is the flexible
   part. zoom-in cursor hints it's clickable. */
.carousel__expand {
    grid-row: 1;
    width: 100%;
    aspect-ratio: var(--carousel-ratio);
    min-height: 0;
    padding: 0;
    margin: 0;
    border: 0;
    background: none;
    display: block;
    overflow: hidden;
    cursor: zoom-in;
}

.carousel__expand:focus-visible {
    outline: var(--focus-ring);
    outline-offset: -2px;
}

.carousel__img {
    width: 100%;
    height: 100%;
    min-height: 0;
    object-fit: cover;
    display: block;
}

/* If a slide image 404s, hide the broken-image glyph; the panel background
   shows through instead (silent fail, like the page background). */
.carousel__img.is-broken {
    visibility: hidden;
}

/* Number counter hidden: dots are the indicator. Keeping both is visual noise
   and increases the chance of overlapping real image content. */
.carousel__counter {
    display: none;
}

/* Caption / chin.
   Uses normal site theme tokens, not hardcoded photo-overlay colors.

   Content-sized: the line-clamps below are the ceiling, not a fixed pixel
   height. The old fixed height (clamp + vw) was smaller than 1 title line +
   2 text lines on phones, and justify-content: center clipped the overflow
   on BOTH edges — that was the sliced-title / mid-word-cut bug. */
.carousel__caption {
    z-index: 2;

    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 0.15rem;

    min-height: 3rem; /* small floor so a one-liner chin doesn't look pinched */

    padding: 0.7rem 0.85rem;
    overflow: hidden;

    background: var(--bg-panel);
    color: var(--text);
    border-top: 1px solid var(--border);

    -webkit-backdrop-filter: blur(var(--panel-blur));
    backdrop-filter: blur(var(--panel-blur));
}

.carousel__title {
    font-family: var(--font-serif);
    font-size: 1.15rem;
    line-height: 1.2;
    color: var(--text);
}

.carousel__text {
    font-size: 0.95rem;
    line-height: 1.35;
    color: var(--text-soft);
}

.carousel__subtext {
    font-size: 0.85rem;
    line-height: 1.35;
    color: var(--text-faint);
}

/* Line-clamps are now the ONLY cap on caption size. Overflow past the clamp
   ends on a line boundary with an ellipsis — never a sliced glyph. */
.carousel__title,
.carousel__text,
.carousel__subtext {
    overflow: hidden;
    display: -webkit-box;
    -webkit-box-orient: vertical;
}

.carousel__title {
    -webkit-line-clamp: 1;
    line-clamp: 1;
}

.carousel__text,
.carousel__subtext {
    -webkit-line-clamp: 2;
    line-clamp: 2;
}

/* Prev / next buttons, vertically centered on each edge. */
.carousel__nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 3;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.4rem;
    height: 2.4rem;
    border: 1px solid var(--border);
    border-radius: 50%;
    background: var(--bg-panel-solid);
    color: var(--text);
    font-size: 1rem;
    line-height: 1;
    transition: border-color var(--transition), background var(--transition), transform var(--transition);
}

.carousel__nav--prev {
    left: 0.6rem;
}

.carousel__nav--next {
    right: 0.6rem;
}

@media (hover: hover) {
    .carousel__nav:hover {
        border-color: var(--accent);
        background: var(--menu-hover-bg);
    }
}

/* Dots indicator, floating inside the image without sitting on captions. */
.carousel__dots {
    position: absolute;
    top: 0.75rem;
    right: 0.75rem;
    z-index: 4;

    display: flex;
    justify-content: center;
    align-items: center;
    gap: 0.45rem;

    padding: 0.35rem 0.5rem;
    border-radius: 999px;

    background: var(--dot-veil-bg);
    border: 1px solid rgba(255, 255, 255, 0.18);

    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
}

.carousel__dot {
    width: 0.55rem;
    height: 0.55rem;
    padding: 0;
    border: 1px solid var(--dot-ring);
    border-radius: 50%;
    background: transparent;
    transition: background var(--transition), transform var(--transition);
}

@media (hover: hover) {
    .carousel__dot:hover {
        background: var(--dot-fill-hover);
    }

    /* Keep the active dot solid even under the hover fill. */
    .carousel__dot.is-active:hover {
        background: var(--on-image-text);
    }
}

.carousel__dot.is-active {
    background: var(--on-image-text);
    transform: scale(1.15);
}

@media (max-width: 640px) {
    .carousel {
        --carousel-ratio: 4 / 3;
        /* --carousel-caption-height removed — the chin is content-sized now */
    }

    .carousel__nav {
        width: 2.1rem;
        height: 2.1rem;
    }
}

/* ---- Gallery (image grid) --------------------------------------------- */
/* Rendered by buildGallery() in engine.js. Sibling to the carousel: same theme
   tokens, same shared lightbox (getLightbox), but a scannable grid of tiles
   instead of one paged frame. A `gallery` block shows all images at once;
   clicking any tile opens the lightbox at that image. A `photo` block is a
   one-image gallery. */

.gallery__grid {
    display: grid;
    /* Auto-fit tiles; min tile ~13rem, never overflowing a narrow screen. */
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
    gap: 0.75rem;
}

/* Optional hard column hints (data-columns="2|3|4"); still collapse on narrow
   viewports via the media query below. */
.gallery__grid[data-columns="2"] {
    grid-template-columns: repeat(2, 1fr);
}

.gallery__grid[data-columns="3"] {
    grid-template-columns: repeat(3, 1fr);
}

.gallery__grid[data-columns="4"] {
    grid-template-columns: repeat(4, 1fr);
}

/* Each tile is a button so it's focusable/keyboard-openable. Strip button
   chrome; the image fills it. zoom-in cursor signals it opens the lightbox —
   same affordance language as .carousel__expand. */
.gallery__item {
    display: flex;
    flex-direction: column;
    padding: 0;
    margin: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--bg-panel);
    -webkit-backdrop-filter: blur(var(--panel-blur));
    backdrop-filter: blur(var(--panel-blur));
    cursor: zoom-in;
    transition: transform var(--transition), border-color var(--transition);
}

@media (hover: hover) {
    .gallery__item:hover {
        transform: translateY(-3px);
        border-color: var(--accent);
    }
}

.gallery__item:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 2px;
}

/* Square tile. object-fit: cover crops uniformly so a ragged set of photo
   dimensions still reads as a tidy grid (same trick as .carousel__img). */
.gallery__img {
    width: 100%;
    aspect-ratio: 1 / 1;
    object-fit: cover;
    display: block;
    min-height: 0;
}

/* Broken image: hide the glyph, let the panel show through (silent fail,
   matches .carousel__img.is-broken). */
.gallery__img.is-broken {
    visibility: hidden;
}

/* Pager: prev / "page / total" / next. Sits centered under the grid; only
   rendered when the set spans more than one page (engine.js). Uses the same
   token language as the rest of the UI. */
.gallery__pager {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
    margin-top: 0.9rem;
}

.gallery__pager-status {
    font-variant-numeric: tabular-nums;
    font-size: 0.95rem;
    color: var(--text-soft);
    min-width: 3.5rem;
    text-align: center;
}

.gallery__pager-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-panel);
    color: var(--text);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
    transition: transform var(--transition), border-color var(--transition);
}

@media (hover: hover) {
    .gallery__pager-btn:hover:not(:disabled) {
        transform: translateY(-2px);
        border-color: var(--accent);
    }
}

.gallery__pager-btn:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 2px;
}

.gallery__pager-btn:disabled {
    opacity: 0.4;
    cursor: default;
}

/* Narrow screens: relax forced column counts to two so tiles stay tappable. */
@media (max-width: 640px) {
    .gallery__grid[data-columns="3"],
    .gallery__grid[data-columns="4"] {
        grid-template-columns: repeat(2, 1fr);
    }
}

/* ----------------------------------------------------------------------
   Opening hours block. A definition list rendered as label/value rows;
   today's row is highlighted. Mirrors the panel/border token language used
   by the table and gallery blocks.
---------------------------------------------------------------------- */

.hours__list {
    margin: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--bg-panel);
    -webkit-backdrop-filter: blur(var(--panel-blur));
    backdrop-filter: blur(var(--panel-blur));
}

.hours__row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.6rem 0.85rem;
    border-top: 1px solid var(--border);
}

.hours__row:first-child {
    border-top: 0;
}

.hours__day {
    margin: 0;
    color: var(--text-soft);
}

.hours__value {
    margin: 0;
    text-align: right;
    font-variant-numeric: tabular-nums;
    color: var(--text);
}

/* Today's row: tinted background, accent edge, and a bolder day label so the
   current day reads at a glance without needing a separate badge. */
.hours__row.is-today {
    background: var(--accent-soft);
    box-shadow: inset 3px 0 0 var(--accent);
}

.hours__row.is-today .hours__day {
    color: var(--text);
    font-weight: 600;
}

/* ---- Review block ----------------------------------------------------- */
/* Rendered by buildReview() in engine.js from a `review` block. Outbound
   "leave a review" CTAs (Google, Facebook, …). No backend — each is a link to
   the platform's own review page. Surfaces match cards/links so it re-themes
   for free in light/dark and on the 404 page. */

.review__list {
    display: grid;
    gap: 0.6rem;
}

/* A CTA row: icon + label + trailing arrow, matching .link-list a treatment. */
.review__cta {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.85rem 1.1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-panel);
    -webkit-backdrop-filter: blur(var(--panel-blur));
    backdrop-filter: blur(var(--panel-blur));
    color: var(--text);
    text-decoration: none;
    transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease;
}

@media (hover: hover) {
    .review__cta:hover {
        border-color: var(--accent);
        background: var(--accent-soft);
        transform: translateY(-1px);
    }
}

.review__cta:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 2px;
}

.review__cta-icon {
    display: inline-flex;
    flex: 0 0 auto;
    line-height: 0;
}

.review__cta-label {
    flex: 1 1 auto;
    font-weight: 500;
}

/* Trailing arrow, mirroring .link-list__arrow. */
.review__cta-arrow {
    flex: 0 0 auto;
    width: 18px;
    height: 18px;
    background: currentColor;
    opacity: 0.55;
    -webkit-mask: no-repeat center / contain url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>');
    mask: no-repeat center / contain url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>');
    transition: transform 0.18s ease, opacity 0.18s ease;
}

@media (hover: hover) {
    .review__cta:hover .review__cta-arrow {
        opacity: 1;
        transform: translateX(2px);
    }
}

/* Touch layout tweak: center the Open-in-Maps button under the full-width
   embed. (The iframe itself is view-only via pointer-events: none in its base
   rule, so Firefox/Android can't hijack scroll taps into Maps.) Hover styling
   needs no touch overrides — it's gated behind @media (hover: hover) at each
   component, so touch never receives sticky hover states in the first place. */
@media (hover: none), (pointer: coarse) {
    .map-open-row {
        text-align: center;
    }
}

/* ---- Footer ----------------------------------------------------------- */
.site-footer {
    width: 100%;
    max-width: none;
    flex-shrink: 0;
    padding: 0.75rem clamp(1.5rem, 5vw, 4rem);
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 1rem;
    justify-content: space-between;
    align-items: center;
    color: var(--text-soft);
    font-size: 0.78rem;
    border-top: 1px solid var(--border);
}

.site-footer a {
    color: var(--text-soft);
}

@media (hover: hover) {
    .site-footer a:hover {
        color: var(--accent);
    }
}

/* Visitor count sits right after the © line. Empty until the count loads
   (or stays empty if analytics is off / unreachable), and the ::before dot
   only shows once there's text, so there's no stray leading dot. When it
   links to the public dashboard it's a plain footer link and inherits the
   shared `.site-footer a` style — same as the credit link — so both match.
   The faint color applies only to the non-link span variant. */
span.footer__count {
    color: var(--text-soft);
}

.footer__count:not(:empty)::before {
    content: "·";
    margin: 0 0.5rem 0 0;
    color: var(--text-soft);
    opacity: 0.6;
}

/* ---- Responsive ------------------------------------------------------- */

/* Mobile nav mode, triggered two ways that behave identically:
   (1) genuinely small screens via the media query below;
   (2) the .force-mobile-nav class, added by JS (initHeaderFit) the instant the
       desktop nav and the header socials would touch. In that mode the desktop
       nav AND the header socials are both hidden and the hamburger takes over —
       the socials still live in the mobile drawer, so nothing is lost.
   CSS can't @media on a class, so the two triggers are kept as parallel rules. */
.site-header.force-mobile-nav .nav-desktop {
    display: none;
}

.site-header.force-mobile-nav .brand-wrap .socials {
    display: none;
}

.site-header.force-mobile-nav .menu-toggle {
    display: inline-flex;
}

@media (max-width: 640px) {
    .nav-desktop {
        display: none;
    }

    /* On small screens the header socials are hidden too; they appear in the
       drawer instead. */
    .brand-wrap .socials {
        display: none;
    }

    .menu-toggle {
        display: inline-flex;
    }

    /* Let the brand shrink + ellipsis so the action buttons (lang/theme/menu)
       ALWAYS fit — a long brand otherwise overflows the header and clips the
       menu button off-screen (body is overflow-x:hidden). Safe here: the
       desktop nav is already hidden at this width, so header-fit's
       natural-width measurement is moot. header-actions stays flex:none so the
       buttons never shrink. */
    .site-header {
        gap: 0.5rem;
    }

    .brand-wrap {
        flex: 0 1 auto;
        min-width: 0;
    }

    .brand-wrap .brand,
    .brand {
        flex: 0 1 auto;
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        font-size: 1.35rem;
    }

    .link-list a {
        padding: 1rem 1.1rem;
    }

    /* ---- Stack data tables on mobile ---------------------------------- */
    /* Long-text tables (e.g. Podmienky servisu) are unreadable as a
       side-scrolling 2-column grid on a narrow screen. Instead of scrolling
       horizontally, turn each row into a self-contained card with cells
       stacked vertically. Works off source order, no JS change needed.

       This is the ONLY place the card/stacking treatment lives — the base
       .data-table rules above stay a normal desktop table. */
    /* Neutralize the scroll frame: the cards carry their own borders now. */
    .table-scroll {
        overflow-x: visible;
        border: none;
        background: transparent;
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        min-width: 0;
    }

    /* Collapse the table structure to blocks so rows/cells stack. min-width:0
       lets them shrink below their intrinsic content width — this is what stops
       a long sentence (e.g. the skladovanie row) from forcing a sideways scroll. */
    .data-table,
    .data-table thead,
    .data-table tbody,
    .data-table tr,
    .data-table td {
        display: block;
        width: 100%;
        min-width: 0;
    }

    /* Visually hide the header row — each stacked row is self-explanatory in
       source order (e.g. condition then detail, or task/description/price). Kept in
       the DOM for screen readers. */
    .data-table thead {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        clip-path: inset(50%);
        white-space: nowrap;
    }

    /* Stacked rows fuse into ONE continuous panel, like .hours__list: the
       tbody carries the frame (border/radius/background), rows carry only a
       divider — so there are no doubled borders and no gaps between rows. */
    .data-table tbody {
        border: 1px solid var(--border);
        border-radius: var(--radius);
        background: var(--bg-panel);
        backdrop-filter: blur(var(--panel-blur));
        -webkit-backdrop-filter: blur(var(--panel-blur));
        overflow: hidden;
    }

    .data-table tbody tr {
        padding: 0.5rem 1.25rem;
        border-top: 1px solid var(--border);
    }

    /* Stacked cards keep the original left-aligned, wrapping value cell —
       the desktop right-align/nowrap treatment is a column concept and the
       columns don't exist here. */
    .data-table td.data-table__value {
        text-align: left;
        white-space: normal;
    }

    /* Inside a collapsible table the outer block already carries the frame —
       strip the stacked-card tbody's own frame so it doesn't double up. */
    .table-block--collapsible .data-table tbody {
        border: none;
        border-radius: 0;
        background: transparent;
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }

    .data-table tbody tr:first-child {
        border-top: 0;
    }

    /* Cells stack inside the row; a divider sits between them, none after
       the last. The cell divider is a FADED version of the row divider
       (derived from --border like --accent-soft is from --accent), so row
       boundaries read stronger than cell boundaries inside the fused panel.
       overflow-wrap/word-break guarantee even an unbroken string can't push
       the row wider than the screen. */
    .data-table tbody td {
        padding: 0.55rem 0;
        border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
        white-space: normal;
        overflow-wrap: anywhere;
        word-break: break-word;
    }

    .data-table tbody tr td:last-child {
        border-bottom: none;
    }

    /* Price/value cell: drop the right-align + nowrap so it reads as a normal
       value inside the stacked row. Keeps the accent color + weight. */
    .data-table td.data-table__value {
        text-align: left;
        white-space: normal;
    }

    /* No sticky hover-fill on the stacked cards. */
    .data-table tbody tr:hover {
        background: var(--bg-panel);
    }

    /* ---- Wide tables become a row accordion on mobile ------------------ */
    /* A 3+ column table can't stack readably (see .table-cards below), so on a
       narrow screen swap the <table> out for its accordion mirror. Both are
       built by buildTable; hiding one with display:none also drops it from the
       a11y tree, so only the visible representation is ever announced. */
    .data-table--multicol {
        display: none;
    }

    .table-cards {
        display: grid;
        gap: 0.5rem;
    }
}

/* Mobile drawer is fixed/overlayed.
   Do NOT lock vertical body scroll here. On some mobile browsers / scrollbar
   setups, changing body overflow while the drawer is open changes the viewport
   calculation and makes the centered content look pushed or narrower.

   Keep the same horizontal overflow behavior as the normal body state so opening
   the menu does not change layout width. */
body.menu-open {
    overflow-x: hidden;
}

/* ---- Lightbox (expanded image view) ----------------------------------- */
/* A single shared overlay (created once in JS) reused by every carousel.
   Shows the full image letterboxed (object-fit: contain) so each image keeps
   its own aspect ratio inside one uniform frame — nothing is cropped. */
.lightbox {
    position: fixed;
    inset: 0;
    z-index: 200;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1rem;
    padding: clamp(1rem, 4vw, 3rem);
    background: var(--on-image-veil);
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
    opacity: 0;
    transition: opacity 200ms ease;
    cursor: zoom-out;

    /* The overlay owns every gesture (initSwipe: sideways = next/prev,
       down = close). Page scroll behind is already locked by .lightbox-open,
       so the only thing given up here is pinch-zoom on the expanded image. */
    touch-action: none;
}

.lightbox.is-open {
    opacity: 1;
}

/* The image: capped to the viewport, letterboxed, never cropped or upscaled
   past its natural size beyond the frame. All expanded images share this same
   bounding frame, so they read as a consistent set. */
.lightbox__img {
    max-width: 100%;
    max-height: 82vh;
    width: auto;
    height: auto;
    object-fit: contain;
    border-radius: var(--radius);
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
    cursor: default;
}

.lightbox__caption {
    margin: 0;
    max-width: min(100%, 48rem);
    text-align: center;
    color: var(--carousel-chip-text);
    font-size: 0.95rem;
    line-height: 1.4;
    cursor: default;
}

.lightbox__close {
    position: absolute;
    top: clamp(0.75rem, 3vw, 1.5rem);
    right: clamp(0.75rem, 3vw, 1.5rem);
    width: 2.75rem;
    height: 2.75rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--on-image-hairline);
    border-radius: 50%;
    background: var(--on-image-btn-bg);
    color: var(--on-image-text);
    font-size: 1.6rem;
    line-height: 1;
    cursor: pointer;
    transition: border-color var(--transition), background var(--transition), transform var(--transition);
}

@media (hover: hover) {
    .lightbox__close:hover {
        border-color: var(--on-image-text);
        background: var(--on-image-btn-bg-hover);
    }
}

/* Press feedback on the photo-overlay controls (their own tint, not accent). */
.lightbox__close:active,
.lightbox__nav:active {
    border-color: var(--on-image-text);
}

.lightbox__close:focus-visible {
    outline: var(--focus-ring);
    outline-offset: var(--focus-offset);
}

/* Prev / next arrows in the expanded view, vertically centered on each edge.
   Hidden by JS when the carousel has only one image. */
.lightbox__nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 3rem;
    height: 3rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--on-image-hairline);
    border-radius: 50%;
    background: var(--on-image-btn-bg);
    color: var(--on-image-text);
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
    transition: border-color var(--transition), background var(--transition), transform var(--transition);
}

.lightbox__nav--prev {
    left: clamp(0.5rem, 3vw, 1.5rem);
}

.lightbox__nav--next {
    right: clamp(0.5rem, 3vw, 1.5rem);
}

@media (hover: hover) {
    .lightbox__nav:hover {
        border-color: var(--on-image-text);
        background: var(--on-image-btn-bg-hover);
    }
}

.lightbox__nav:focus-visible {
    outline: var(--focus-ring);
    outline-offset: var(--focus-offset);
}

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

/* Lock page scroll behind the open lightbox. */
body.lightbox-open {
    overflow: hidden;
}

/* While fading out (is-open removed but not yet hidden), let the cursor and
   clicks fall through immediately instead of being held by the zoom-out
   overlay during the 200ms fade. */
.lightbox:not(.is-open) {
    pointer-events: none;
}

/* ----------------------------------------------------------------------
   ERROR STATE — shown when site-spec.json can't be loaded/parsed and there
   is no inline content to fall back to. Append to styles.css. Uses the same
   theme tokens as the rest of the site, so it re-themes for free.
---------------------------------------------------------------------- */

.error-state {
    /* Roomy, centered card so the message reads as intentional, not broken. */
    padding: 2.5rem 1.75rem;
    border: 1px solid var(--border, rgba(127, 127, 127, 0.25));
    border-radius: 14px;
    background: var(--bg-panel);
    text-align: center;
}

.error-state__title {
    margin: 0 0 0.75rem;
    font-size: clamp(1.4rem, 4vw, 2rem);
    line-height: 1.2;
}

.error-state__lead {
    margin: 0 auto 1.5rem;
    max-width: 38rem;
    color: var(--text-soft);
    line-height: 1.6;
}

/* Reuse the header .socials layout but center it here. */
.error-state .socials {
    justify-content: center;
    margin: 0 auto 1.5rem;
}

.error-state__details {
    margin-top: 1.5rem;
    text-align: left;
    max-width: 38rem;
    margin-left: auto;
    margin-right: auto;
    font-size: 0.875rem;
    color: var(--text-soft);
}

.error-state__details summary {
    cursor: pointer;
    user-select: none;
    opacity: 0.8;
}

@media (hover: hover) {
    .error-state__details summary:hover {
        opacity: 1;
    }
}

.error-state__details p {
    margin: 0.5rem 0 0;
    padding: 0.75rem;
    border-radius: 8px;
    background: var(--bg-base, rgba(127, 127, 127, 0.1));
    font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
    word-break: break-word;
}

/*
  Built from m-remis/static-web-template
  https://github.com/m-remis/static-web-template
*/