/*
 * blocks-shared.css — theme-agnostic block behavior CSS.
 *
 * Rationale (F4-585, prompted by "if we have 2000 starter templates in 2 years,
 * how does this scale?"): the F4-255 component kit was traditionally copy-
 * pasted into every theme's theme.css because there was no shared layer for
 * cross-theme block CSS. That works with 3-5 themes but rots at 20+, and any
 * fix (e.g. F4-583 hero entrance animations) has to be applied N times with
 * the risk of drift.
 *
 * This file is the shared layer: rules that describe *how a block behaves*
 * across themes, using the F4-247 token contract so per-theme colors/fonts
 * still land through --site-* variables. Each theme's own theme.css keeps
 * theme-specific overrides (typography, palette, per-theme decorative
 * touches).
 *
 * Loaded from each theme's page.php via
 *   <link rel="stylesheet" href="/assets/blocks-shared.css">
 * BEFORE theme.css, so a theme can override any of this by declaring the
 * same selector in its own theme.css.
 *
 * SitePublisher copies this file to /sites/<slug>/assets/blocks-shared.css
 * at publish time; PreviewRenderer inlines it for /cms/pages/*/preview and
 * the CMS editor canvas.
 */

/* ============================================================
 * F4-583: hero entrance animations.
 * F4-586: per-block entrance animations (heading, text, image, button, …).
 * Both share the same `@keyframes blokky-anim-*` set (defined once, below)
 * and the same `prefers-reduced-motion` guard.
 *
 * Hero-level: `.hero--anim-<type>` on the section shell; the animation is
 * applied to `.hero__inner` so the bg-band paints in immediately and only
 * the content animates. Timing via `--hero-anim-duration/-delay` on the
 * shell.
 *
 * Per-block: `.block.block--anim-<type>` on the block's own element; the
 * animation is applied to the block itself. Timing via `--block-anim-
 * duration/-delay` on the same element.
 *
 * F4-610: below-the-fold blocks now wait for IntersectionObserver — the
 * pending-state rules at the bottom of this section hold them at opacity: 0
 * until blocks-shared.js adds `.is-in-view` when the block enters the
 * viewport. Above-the-fold blocks fire immediately because the observer
 * reports them intersecting on first callback tick. No-JS visitors get
 * the content flat (see `html.no-js-anim` fallback) — worse than animated,
 * still readable.
 * ============================================================ */

.hero--anim-fade .hero__inner,
.hero--anim-slide-up .hero__inner,
.hero--anim-slide-down .hero__inner,
.hero--anim-slide-in-left .hero__inner,
.hero--anim-slide-in-right .hero__inner {
    animation-duration: var(--hero-anim-duration, 700ms);
    animation-delay: var(--hero-anim-delay, 0ms);
    animation-timing-function: cubic-bezier(.22, .61, .36, 1);
    animation-fill-mode: both;
}
.hero--anim-fade .hero__inner           { animation-name: blokky-anim-fade; }
.hero--anim-slide-up .hero__inner       { animation-name: blokky-anim-slide-up; }
.hero--anim-slide-down .hero__inner     { animation-name: blokky-anim-slide-down; }
.hero--anim-slide-in-left .hero__inner  { animation-name: blokky-anim-slide-in-left; }
.hero--anim-slide-in-right .hero__inner { animation-name: blokky-anim-slide-in-right; }

/* F4-586: per-block bindings. `.block.block--anim-*` uses two classes on the
   same element so the specificity beats stray descendant rules but stays
   below theme overrides at three-class weight.
   Easing: Material-style `cubic-bezier(.4,0,.2,1)` (F4-610 followup 2026-07-
   03). The hero-level snappy curve `(.22,.61,.36,1)` was too front-loaded
   for scroll-triggered per-block entries — user reported the animation
   "blinker inn" because ~73% of the transition completed in the first ~34%
   of duration, so setting duration to 8000ms still felt like a <1s fade.
   Material curve keeps the linear feel: ~44% done at 34% duration. */
.block.block--anim-fade,
.block.block--anim-slide-up,
.block.block--anim-slide-down,
.block.block--anim-slide-in-left,
.block.block--anim-slide-in-right {
    animation-duration: var(--block-anim-duration, 700ms);
    animation-delay: var(--block-anim-delay, 0ms);
    animation-timing-function: cubic-bezier(.4, 0, .2, 1);
    animation-fill-mode: both;
}
.block.block--anim-fade           { animation-name: blokky-anim-fade; }
.block.block--anim-slide-up       { animation-name: blokky-anim-slide-up; }
.block.block--anim-slide-down     { animation-name: blokky-anim-slide-down; }
.block.block--anim-slide-in-left  { animation-name: blokky-anim-slide-in-left; }
.block.block--anim-slide-in-right { animation-name: blokky-anim-slide-in-right; }

/* F4-610: scroll-triggered pending-state. Hold animated elements at
   opacity: 0 until blocks-shared.js adds .is-in-view. Gated by
   `html.blocks-anim-scroll` so ONLY themes that opt in get the pending
   behavior — themes that haven't updated their page.php keep the
   pre-F4-610 paint-time trigger.

   Why keep the keyframes and add a separate opacity hold? Because
   `animation-fill-mode: both` already leaves the block at its final
   frame after playing — so we can't rely on the animation itself to
   both hide and reveal. The hold is a discrete step before the
   animation is allowed to run.

   Progressive-enhancement chain:
   - Theme page.php sets `<html class="blocks-anim-scroll">` server-side.
   - No JS / IO unsupported / prefers-reduced-motion → blocks-shared.js
     strips the class itself, so pending state doesn't stick and content
     stays visible.
   - JS + IO + normal motion → observer marks above-fold synchronously
     (no flash) and below-fold when scrolled into view. */
html.blocks-anim-scroll .hero[class*="--anim-"]:not(.is-in-view) .hero__inner,
html.blocks-anim-scroll .block[class*="--anim-"]:not(.is-in-view) {
    opacity: 0;
    animation-play-state: paused;
}

/* Slide distances: bumped 24-32px → 3rem (~48px) 2026-07-03 followup.
   The subtle 24-32px offset was designed for hero-inner where the
   BG-band paints instantly and only the content translates a little
   — nice, but per-block scroll-in gives no context so the movement
   needs to read as "arrival" not "jitter". 3rem is dramatic enough
   to notice on all viewport sizes without feeling like a bounce. */
@keyframes blokky-anim-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
}
@keyframes blokky-anim-slide-up {
    from { opacity: 0; transform: translateY(3rem); }
    to   { opacity: 1; transform: translateY(0); }
}
@keyframes blokky-anim-slide-down {
    from { opacity: 0; transform: translateY(-3rem); }
    to   { opacity: 1; transform: translateY(0); }
}
@keyframes blokky-anim-slide-in-left {
    from { opacity: 0; transform: translateX(-3rem); }
    to   { opacity: 1; transform: translateX(0); }
}
@keyframes blokky-anim-slide-in-right {
    from { opacity: 0; transform: translateX(3rem); }
    to   { opacity: 1; transform: translateX(0); }
}

/* Accessibility: users who ask for reduced motion get no entrance animations
   anywhere in the block system. Applies to every `--anim-*` class so per-
   child animations (F4-586 planned) share the same kill-switch without
   restating the media query per block. F4-610: also drop the pending-state
   opacity: 0 so content is visible even if blocks-shared.js decided to
   skip the observer for reduced-motion users. */
@media (prefers-reduced-motion: reduce) {
    [class*="--anim-"] .hero__inner,
    [class*="--anim-"] { animation: none !important; opacity: 1 !important; }
}

/* ============================================================
 * F4-611: typewriter effect on HeadingBlock.
 *
 * Server-render ships the full heading text so a11y + no-JS visitors
 * see readable content. blocks-shared.js empties `[data-typewriter]`
 * and re-types char-by-char once it decides the client can animate
 * (IntersectionObserver, not reduced-motion). The `blocks-anim-
 * typewriter-live` class is added by the JS on the block wrapper
 * before typing starts — the cursor pseudo only shows when the class
 * is present, so a static heading (no JS or reduced-motion) never
 * has a lonely blinking bar next to it.
 *
 * `.vh` (visually-hidden) below is used by JS to mirror the full text
 * for screen readers (aria-hidden on the visually-typed element so
 * screen readers don't hear the character stream).
 * ============================================================ */
.block--heading.blocks-anim-typewriter-live .heading__text::after {
    content: '|';
    display: inline-block;
    margin-left: 0.08em;
    font-weight: normal;
    color: currentColor;
    /* aria-hidden on generated content is implicit — SRs skip ::after */
    animation: blokky-typewriter-blink 0.9s step-end infinite;
}
.block--heading.blocks-anim-typewriter-live.blocks-anim-typewriter-no-cursor .heading__text::after {
    display: none;
}
@keyframes blokky-typewriter-blink {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
    .block--heading .heading__text::after { display: none; }
}

/* Visually-hidden helper — mirrors app.css `.vh` (retningslinjer §3a
   rule 11) for use in published output. Published pages ship theme.css
   + blocks-shared.css but NOT app.css, so anything a11y-related that
   needs to be reachable outside the CMS lives here. */
.vh {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* ============================================================
 * F4-633: ButtonBlock — .btn__icon-wrap holds the optional CTA icon so
 * canvas text-editing can't delete the SVG (contenteditable="false" on
 * the wrap). Structural rules live here so every theme's inline-flex
 * `.btn` gets the same icon-slot layout without per-theme copy-paste.
 * The wrap is inline-flex to keep the SVG behaving as a `flex: none`
 * gap-participant in `.btn`, matching the pre-wrap layout.
 * ============================================================ */
.btn__icon-wrap {
    display: inline-flex;
    flex: none;
    align-items: center;
    user-select: none;
    cursor: default;
}

/* ============================================================
 * F4-596: HeaderConfigBlock — brand + nav + actions layout inside the
 * theme's <header class="site-header">. Themes handle colours + spacing;
 * these rules are the structural bare-minimum so each preset lines up
 * without per-theme copy-paste.
 *
 * `.site-header__row` is the inner flex row emitted by
 * HeaderConfigBlock::render(); the outer <header> stays theme-owned.
 * Layout classes on the row: `--layout-spread` / `--compact` / `--centered`.
 * ============================================================ */
.site-header__row {
    display: flex;
    align-items: center;
    gap: 1rem;
    width: 100%;
}
.site-header__brand { display: inline-flex; align-items: center; gap: .5rem; }
.site-header__brand--has-logo.site-header__brand--has-text { gap: .6rem; }
.site-header__brand-logo { display: block; width: auto; }
/* F4-605: square brand-logo modifier for the "kvadratisk + tekst"-modus.
   Kompakt 40x40 så merkevaren sitter tett ved siden av nettstednavnet. */
.site-header__brand-logo--square {
    width: 40px;
    height: 40px;
    object-fit: contain;
    border-radius: 6px;
}
.site-header__brand-text { display: inline-block; }
.site-header__actions {
    display: inline-flex;
    align-items: center;
    gap: .75rem;
}
.site-header__secondary {
    color: inherit;
    text-decoration: none;
    font-weight: 500;
    font-size: .95rem;
}
.site-header__secondary:hover,
.site-header__secondary:focus-visible {
    text-decoration: underline;
}

/* Layout: spread — brand left, nav center, actions right.
 * Nav flex-1 so it fills the middle; actions push to the far right. */
.site-header--layout-spread .site-nav { flex: 1 1 auto; justify-content: center; display: flex; }
.site-header--layout-spread .site-header__actions { margin-left: auto; }

/* Layout: compact — brand left, nav + actions grouped right. Eksplisitt
 * margin-right: 0 fordi noen temaer (frisk, eksklusiv-varianter) setter
 * `.site-nav { margin: 0 auto }` som shorthand — det gir BÅDE margin-left
 * OG margin-right auto = sentrert i tilgjengelig plass. Uten right: 0 her
 * ville shared-regelen bare overstyrt left og latt right stå på auto, så
 * nav havnet i midten i stedet for helt til høyre. */
.site-header--layout-compact .site-nav {
    margin-left: auto;
    margin-right: 0;
}
.site-header--layout-compact .site-header__actions { margin-left: 0; }

/* Layout: centered — brand center, actions/hamburger right.
 * `.site-nav` is hidden on desktop (typically toggled via hamburger on
 * mobile in this preset). For the MVP we surface it below the row on
 * narrow viewports; a proper hamburger toggle is F4-598-backlog. */
.site-header--layout-centered { position: relative; }
.site-header--layout-centered .site-header__brand {
    margin-left: auto; margin-right: auto;
}
.site-header--layout-centered .site-nav {
    position: absolute;
    right: 3.5rem;
    top: 50%;
    transform: translateY(-50%);
}
.site-header--layout-centered .site-header__actions {
    position: absolute;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
}
@media (max-width: 720px) {
    .site-header--layout-centered .site-nav { display: none; }
}

/* F4-596: legal-links row inside `.site-footer`. Themes emit two `<a>`
 * elements with a `<span class="site-footer__legal-sep">·</span>`
 * separator between (inline `&middot;` so the spacing works even when
 * blocks-shared.css hasn't loaded yet in the editor iframe). */
.site-footer__legal {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
    align-items: center;
    padding-block: .75rem;
}
.site-footer__legal-sep { color: var(--site-muted, currentColor); opacity: .6; }

/* ============================================================
 * F4-592: LogoBlock — shared base rules. Themes can override colours via
 * --site-* tokens; the structural rules (img sizing, link affordance,
 * placeholder styling) live here so a new theme gets a working logo
 * without copy-paste.
 * ============================================================ */
.block--logo { display: inline-flex; align-items: center; }
.block--logo .block__link { display: inline-flex; align-items: center; }
.block--logo .block__media { display: block; height: auto; max-width: 100%; }
/* Empty-state (editor-only): drawn as a dashed frame + icon so a dropped
   logo without a brand-logo-configured site is still visible on the
   canvas. Server-side render never emits this — it renders nothing when
   no logo resolves. */
.logo__placeholder {
    display: inline-flex; align-items: center; gap: .5rem;
    padding: .5rem .75rem;
    border: 1px dashed color-mix(in srgb, var(--site-text, #333) 30%, transparent);
    border-radius: 6px;
    color: color-mix(in srgb, var(--site-text, #333) 65%, transparent);
    font-size: .85rem;
    background: transparent;
}
.logo__placeholder svg { width: 20px; height: 20px; }

/* ============================================================
 * F4-592: SitemapBlock — shared base rules for the footer link-list.
 * `--cols-N` sets a responsive CSS Grid so the block scales from
 * 4-column at wide viewports down to 1-column on mobile without a
 * per-theme media-query. Themes can override typography + colour via
 * --site-* tokens.
 * ============================================================ */
.block--sitemap {
    display: grid;
    gap: calc(var(--site-space, 1rem) * 1.5);
    grid-template-columns: repeat(var(--sitemap-cols, 3), minmax(0, 1fr));
    margin-block: calc(var(--site-space, 1rem) * 1.5);
}
.block--sitemap.sitemap--cols-1 { --sitemap-cols: 1; }
.block--sitemap.sitemap--cols-2 { --sitemap-cols: 2; }
.block--sitemap.sitemap--cols-3 { --sitemap-cols: 3; }
.block--sitemap.sitemap--cols-4 { --sitemap-cols: 4; }
.sitemap__group { display: flex; flex-direction: column; gap: .5rem; }
.sitemap__heading {
    margin: 0;
    font-size: .95rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--site-text, inherit);
}
.sitemap__links {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: .35rem;
}
.sitemap__links a {
    text-decoration: none;
    color: color-mix(in srgb, var(--site-text, #333) 80%, transparent);
}
.sitemap__links a:hover,
.sitemap__links a:focus-visible {
    color: var(--site-accent, var(--site-text, #333));
    text-decoration: underline;
}
.sitemap__links span { color: var(--site-muted, #666); font-weight: 600; }
/* Task B (2026-07-05): sitemap now uses the shared `.block-empty-placeholder`
   pattern in editor.js#injectThemeCss — same visual language as form / image /
   news. Old `.sitemap__placeholder` selector is dead. */
@media (max-width: 640px) {
    .block--sitemap,
    .block--sitemap.sitemap--cols-1,
    .block--sitemap.sitemap--cols-2,
    .block--sitemap.sitemap--cols-3,
    .block--sitemap.sitemap--cols-4 { grid-template-columns: 1fr; }
}

/* ============================================================
 * F4-634: text-color-trait override — forces inner text elements to
 * inherit the block wrapper's inline `color:` (set by the shared
 * typography-trait) even when a theme rule explicitly colors them
 * (e.g. `.block--checklist .checklist__text { color: var(--site-text) }`).
 *
 * The typography helper emits `.block--text-color-override` on the
 * wrapper only when the user has picked a custom color, so themes
 * still own the default color otherwise. Specificity `.block.block--
 * text-color-override .<inner>` = (0,3,1) beats typical theme rules
 * (0,2,1) so the override wins regardless of theme file source order.
 *
 * Add new inner selectors here when adding text-color-trait to more
 * blocks — one shared list beats N per-theme edits (jf. themes/README.md
 * cross-theme-scaling-rationale).
 *
 * Intermediate wrappers that themes color directly (e.g.
 * `.checklist__item { color: var(--site-accent) }` in every theme —
 * that sets a default icon-current-color on <li>) get `color: unset`
 * so they inherit the wrapper's color instead. Otherwise
 * `color: inherit` on the inner text span would inherit the theme's
 * accent color from the <li>, not the user's chosen color from the
 * <ul> wrapper. Icon color still works because it moved to
 * `.checklist__icon-wrap` (see checklist.gjs.js / ChecklistBlock.php).
 * ============================================================ */
.block.block--text-color-override .checklist__item {
    color: unset !important;
}
.block.block--text-color-override .checklist__text,
.block.block--text-color-override .heading__text,
.block.block--text-color-override .text__body {
    color: inherit !important;
}
/* F4-648: RTE inline color (foreColor → `<span style="color:X">`) is now
   the primary text-color surface — an override forcing descendants to
   inherit would break that painting. The previous `!important` on
   `span, font` inside these text elements has been dropped. Legacy
   content authored through the old sidebar Typography colour path still
   works via `color: inherit !important` on the container above (the
   wrapper's `color:` cascades into the raw text). If a customer set a
   sidebar colour AND painted words with RTE, the RTE-emitted span wins
   for those words (inline (1,0,0,0) beats descendant `!important` at
   (0,3,1)) — which is the intended per-word-vs-whole-block hierarchy. */

/* ============================================================
 * F4-612: FAQ / accordion. Uses native <details>/<summary> so it works
 * without JS (progressive enhancement — allowMultipleOpen only matters
 * when a client-side snippet closes siblings; otherwise every item
 * behaves like allowMultipleOpen=true). Themes override colours via
 * --site-* tokens.
 * ============================================================ */
.block--accordion {
    display: flex;
    flex-direction: column;
    gap: .5rem;
    margin-block: var(--site-space, 1rem);
}
.accordion__item {
    /* F4-612 followup 5: explicit token-based border so it's visible on
       both light and dark themes AND in the editor canvas. `currentColor
       15% mixed with transparent` was near-invisible on dark backgrounds. */
    border-bottom: 1px solid var(--site-border, rgba(128, 128, 128, .25));
}
.accordion__question {
    display: flex;
    align-items: center;
    gap: .75rem;
    cursor: pointer;
    list-style: none; /* strips default marker in most browsers */
    padding: .75rem 0;
    font-weight: 600;
    font-size: 1.05rem;
}
.accordion__question::-webkit-details-marker { display: none; }
.accordion__question-text {
    flex: 1 1 auto;
    min-width: 0;
}
.accordion__chevron {
    flex: none;
    display: inline-flex;
    align-items: center;
    transition: transform 180ms ease;
}
.accordion__chevron svg {
    width: 18px;
    height: 18px;
}
.accordion__item[open] .accordion__chevron {
    /* Chevron points right (>) closed → rotate 90deg to point down when
       open. F4-612 followup 4: 180deg (left) was wrong; 90deg lands the
       tip pointing at the answer below, which matches every native
       disclosure widget. */
    transform: rotate(90deg);
}
.accordion__answer {
    /* F4-612 followup 4: indent the answer to align with the question text
       (chevron width 18px + summary gap .75rem). */
    padding: 0 0 var(--site-space, 1rem) calc(18px + .75rem);
    color: color-mix(in srgb, currentColor 85%, transparent);
}
/* F4-612 followup 6: force-reset child `.block` margins so the
   editor + published side render identically (WYSIWYG). The theme's
   `.block { margin-block: 1.5rem }` was leaking through unless our
   rule loaded first — !important guarantees the accordion answer
   stays tight regardless of injection order. */
.accordion__answer > * { margin-block: 0 !important; }
.accordion__answer > * + * { margin-top: .5rem !important; }
.accordion__answer > .block > :first-child { margin-top: 0 !important; }
.accordion__answer > .block > :last-child  { margin-bottom: 0 !important; }

/* Variant: bordered — each item as a rounded card. */
.accordion--bordered .accordion__item {
    border: 1px solid color-mix(in srgb, currentColor 15%, transparent);
    border-radius: var(--site-radius, 8px);
    padding: 0 .9rem;
}
.accordion--bordered .accordion__item + .accordion__item {
    margin-top: 0;
}

/* Variant: plus-minus — swap chevron for + / − via CSS pseudo. */
.accordion--plus-minus .accordion__chevron svg { display: none; }
.accordion--plus-minus .accordion__chevron::before {
    content: '+';
    font-size: 1.4rem;
    line-height: 1;
    font-weight: 400;
}
.accordion--plus-minus .accordion__item[open] .accordion__chevron {
    transform: none;
}
.accordion--plus-minus .accordion__item[open] .accordion__chevron::before {
    content: '−';
}

/* Motion opt-out. */
@media (prefers-reduced-motion: reduce) {
    .accordion__chevron { transition: none; }
}

/* ============================================================
 * F4-646: form block — baseline that inherits theme --site-* tokens.
 *
 * Before this section, 4 of 9 themes (default/delikat/leken/skandi)
 * copied a full form CSS recipe into their own theme.css and 5 shipped
 * NO form styling at all — a form on the eksklusiv-preview rendered as
 * raw browser default (white input on dark bg, label inline with input,
 * no radius, no padding). By putting the recipe here — with all colors
 * pulled from --site-bg / --site-surface / --site-text / --site-border /
 * --site-accent / --site-radius — every theme gets modern form styling
 * for free, and per-theme overrides in theme.css still win at equal
 * specificity because they're declared AFTER this file (per the loader
 * comment at the top of this file).
 * ============================================================ */

.block--form { display: block; }
.block--form .form-grid {
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    gap: var(--site-space, 1rem);
    margin-bottom: var(--site-space, 1rem);
}
.block--form .form-field {
    grid-column: span var(--form-cols, 12);
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}
/* Label sits ABOVE the input (label default is inline, which produced
   the "90s form" look before F4-646). */
.block--form .form-field label {
    display: block;
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--site-text, inherit);
}
.block--form .form-field__required {
    color: #d32f2f;
}
/* Help text under a field — muted, smaller, never red before actual
   error (retningslinjer §3a regel 9). */
.block--form .form-field__help {
    color: var(--site-muted, #6b6f78);
    font-size: 0.75rem;
    margin-top: 2px;
    line-height: 1.4;
}
.block--form .form-field__radio-group {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 4px 0;
}
.block--form .form-field__radio {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
    font-weight: 400;
    color: var(--site-text, inherit);
}
.block--form .form-field__radio input { width: auto; margin: 0; }
.block--form .form-section-heading {
    grid-column: span var(--form-cols, 12);
    margin: calc(var(--site-space, 1rem) * 0.75) 0 0;
    font-size: 1.15rem;
    font-weight: 700;
    color: var(--site-text, inherit);
}

/* Inputs / textarea / select — use --site-surface as the field
   background so the field reads as a distinct row against the page.
   color-mix on the border gives a slightly stronger edge than the
   theme's default divider so the field is scannable at a glance. */
.block--form input,
.block--form textarea,
.block--form select {
    width: 100%;
    padding: 0.7em 0.85em;
    border: 1px solid var(--site-border, #d0d4dc);
    border-radius: var(--site-radius, 6px);
    background: var(--site-surface, #fff);
    color: var(--site-text, inherit);
    font: inherit;
    font-size: 1rem;
    box-sizing: border-box;
    transition: border-color 120ms ease, box-shadow 120ms ease;
}
.block--form textarea { resize: vertical; min-height: 6rem; }
.block--form input[type="checkbox"],
.block--form input[type="radio"] {
    width: auto;
    padding: 0;
    margin: 0;
    border-width: 1px;
}
.block--form input::placeholder,
.block--form textarea::placeholder {
    color: color-mix(in srgb, var(--site-muted, #6b6f78) 80%, transparent);
    opacity: 1;
}
/* Focus: accent-colored ring with a soft outer glow so both keyboard
   and pointer users see clearly where they are. Using color-mix rather
   than opacity so a dark-theme accent (gold on navy, teal on cream)
   still lands with the right contrast. */
.block--form input:focus,
.block--form textarea:focus,
.block--form select:focus {
    outline: none;
    border-color: var(--site-accent, #2563eb);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--site-accent, #2563eb) 22%, transparent);
}
/* Error state — bumps border to red and adds a red halo. Toggled by
   the FormBlock submit IIFE alongside aria-invalid. */
.block--form .form-field__error {
    margin: 4px 0 0;
    color: #d32f2f;
    font-size: 0.85rem;
}
.block--form .form-field.has-error input,
.block--form .form-field.has-error textarea,
.block--form .form-field.has-error select {
    border-color: #d32f2f;
    box-shadow: 0 0 0 3px color-mix(in srgb, #d32f2f 18%, transparent);
}
.block--form .block--form__error {
    margin: 0 0 var(--site-space, 1rem);
    padding: 0.7em 0.9em;
    background: color-mix(in srgb, #d32f2f 10%, var(--site-surface, #fff));
    color: #b71c1c;
    border: 1px solid color-mix(in srgb, #d32f2f 25%, transparent);
    border-radius: var(--site-radius, 6px);
    font-size: 0.9rem;
}
/* F4-443a: upload-confirmation badge shown after a successful file
   upload swaps out the file input. Green checkmark + filename so the
   customer sees the upload worked while the submit continues. */
.block--form .form-field__upload-badge {
    margin: 4px 0 0;
    padding: 6px 10px;
    background: color-mix(in srgb, #1b5e20 8%, var(--site-surface, #fff));
    color: #1b5e20;
    border: 1px solid color-mix(in srgb, #1b5e20 25%, transparent);
    border-radius: var(--site-radius, 6px);
    font-size: 0.85rem;
    font-weight: 500;
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* Success confirmation card — green tint, checkmark icon inline as data
   URL so no asset dependency. */
.block--form .block__confirmation {
    margin: var(--site-space, 1rem) 0 0;
    padding: 1em 1em 1em 3rem;
    background: color-mix(in srgb, #1b5e20 8%, var(--site-surface, #fff));
    color: #1b5e20;
    border: 1px solid color-mix(in srgb, #1b5e20 25%, transparent);
    border-radius: var(--site-radius, 6px);
    font-size: 1rem;
    line-height: 1.5;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%231b5e20' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6L9 17l-5-5'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: 0.9rem 1em;
    background-size: 1.4rem 1.4rem;
}
.block--form .block__confirmation:focus { outline: none; }
/* Submit button — uses --site-accent. Themes with a strong custom .btn
   recipe will override this via specificity, but we ensure a working
   baseline so a form is submittable-looking on ANY theme. */
.block--form button[type="submit"] {
    margin-top: 0.25rem;
    padding: 0.7em 1.4em;
    border: 0;
    border-radius: var(--site-radius, 6px);
    background: var(--site-accent, #2563eb);
    color: var(--site-accent-contrast, #fff);
    cursor: pointer;
    font: inherit;
    font-weight: 600;
    transition: filter 120ms ease;
}
.block--form button[type="submit"]:hover { filter: brightness(0.92); }
.block--form button[type="submit"]:focus-visible {
    outline: 3px solid color-mix(in srgb, var(--site-accent, #2563eb) 55%, transparent);
    outline-offset: 2px;
}

/* On narrow viewports every field collapses to full-width so a col=6
   input isn't absurdly cramped. Matches per-theme rule that was already
   inside a mobile media query. */
@media (max-width: 640px) {
    .block--form .form-grid > .form-field { grid-column: span 12 !important; }
}

/* NB: `.block--form-placeholder` used to live here (F4-627 initial round)
   — replaced by the shared `.block-empty-placeholder` pattern in
   editor.js's injectThemeCss because it's canvas-only and every block
   with an empty state should use the same visual. See
   docs/editor-trait-patterns.md → "Empty-state placeholder". */

/* Preview-only confirmation card — real page hides it until submit
   succeeds, but the canvas shows it dimmed so the copy is visible
   during design. */
.block__confirmation--preview {
    margin-top: .75rem;
    opacity: .7;
    border-style: dashed;
}

/* ============================================================
 * Palette-bg (bg--surface/muted/accent/dark) beats per-block default
 * wrapper background — 2026-07-05 fix.
 *
 * Every theme.css sets `.block--card { background: var(--site-surface); ... }`
 * declared AFTER its own `.bg--<palette> { background: ... }` rules. At
 * equal specificity (both are one class), source order wins → .block--card
 * takes back the background even when the customer picked Accent/Dark/etc.
 * Symptom: card shows white surface + white text (from .bg--accent's color)
 * = invisible content, reported 2026-07-05 on `test`-page card block.
 *
 * .bg--custom already lives after .block--card in every theme (documented in
 * theme.css) so it wins for hex-picker case. Palette variants weren't
 * hoisted the same way. Instead of re-ordering 8 themes, this shared rule
 * bumps palette-on-card to spec 20 (.block--card + .bg--<palette>) — theme
 * loads AFTER shared, but tema's .block--card is spec 10 so it can't reach
 * this. Theme authors that want a per-theme palette variant on cards
 * (delikat's warmer accent, e.g.) override with the same compound selector
 * in their own theme.css; equal spec + later source = theme wins.
 *
 * Only `.block--card` is affected because it's the only .block--<X>
 * wrapper that sets `background` cross-theme. hero/section/columns/etc.
 * paint background via inner slots (.section__inner, .hero--has-bg image
 * layer, etc.) and don't collide with .bg--<palette> on the outer wrapper.
 * ============================================================ */
.block--card.bg--surface { background: var(--site-surface); }
.block--card.bg--muted   { background: color-mix(in srgb, var(--site-accent) 8%, var(--site-surface)); }
.block--card.bg--accent  { background: var(--site-accent); color: var(--site-accent-contrast); }
.block--card.bg--dark    { background: var(--site-dark, #14181f); color: #f4f6fa; }

/* 2026-07-06 followup — cards inside a coloured section (parent has
 * `.bg--accent` / `.bg--dark`) inherited that section's white text
 * color, and `.block--card`'s own `background: var(--site-surface)`
 * is white, so heading + text rendered invisible white-on-white. The
 * customer saw a blank card and couldn't target the contentEditable
 * area with a click. Reported 2026-07-06 on splonk.no page 43 «Ting
 * å se / gjøre» seksjon.
 *
 * Force color back to the theme default UNLESS the card itself opted
 * into an inverse palette above (bg--accent / bg--dark rows already
 * set their own readable contrast colour). Spec 20+ (two classes +
 * two :not-classes = 40) beats theme.css' `.block--card` (spec 10)
 * without touching every theme. */
.block--card:not(.bg--accent):not(.bg--dark) { color: var(--site-text); }
/* 2026-07-06 iter 2 — links inside the card body inherit color from
 * the surrounding text (so a light-blue card background doesn't leave
 * an accent-blue link invisible, and RTE colour picks via inline
 * `<span style="color:X">` win the cascade instead of fighting a
 * class-based rule). Distinction is signalled with a underline
 * instead: the classic web convention, works on any background, and
 * satisfies WCAG "more than colour" for accessibility.
 *
 * `.card__link` (the sidebar-configured link with the arrow →) keeps
 * its own no-underline styling — that variant relies on font-weight
 * and the arrow character for distinction, and gets an underline on
 * hover. Any OTHER anchor in the card (a pasted link inside a Text
 * block, a link inside a Heading) gets a persistent underline.
 *
 * Spec 30+ (three classes and one type) beats theme's `.bg--accent a`
 * (spec 11) for the colour override; the underline rule doesn't fight
 * anything cross-theme. */
.block--card:not(.bg--accent):not(.bg--dark) a { color: inherit; }
.block--card a:not(.card__link) { text-decoration: underline; text-underline-offset: 2px; }
.block--card a:not(.card__link):hover { text-decoration-thickness: 2px; }

/* Same treatment for .block--section (2026-07-07): links in a section
 * inherit surrounding text colour so colored-palette or bg-image sections
 * (dark overlay + white text) don't leave accent-blue links unreadable
 * against dark or busy backgrounds. `color: inherit` is safe across all
 * palette variants because the parent (.bg--accent, .bg--dark, section--
 * has-bg with customer RTE colour) already carries the right colour;
 * inherit just follows it. Underline is the visual signal — classic web
 * convention, works on any background, satisfies WCAG "more than colour".
 *
 * Excludes .card__link (arrow-link inside cards keeps theme styling —
 * no underline, accent colour) and .btn (buttons keep their own chrome).
 *
 * Spec 41 (3 classes + 3 pseudo + 1 tag) beats theme's `.bg--accent a`
 * (spec 11), `.bg--dark a` (spec 11), and generic `a` (spec 1). Cards
 * inside sections keep card-styling via the higher-spec `.block--card`
 * selectors above. */
.block--section a:not(.card__link):not(.btn) {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 2px;
}
.block--section a:not(.card__link):not(.btn):hover { text-decoration-thickness: 2px; }

/* Text-block links: eksplisitt underline (2026-07-07). Browser-default gir
 * underline på `<a href>`, men tema-CSS' `line-height`, `font`-oppsett og
 * globalt `a { color }` er inkonsistent nok til at rene text-blokker kan
 * la lenke se ut som «bare litt annen farge tekst». Kanonisk pattern speiler
 * .block--card + .block--section over: eksplisitt underline + tydelig hover.
 * Tema-fargen (`--site-accent`) beholdes — kontrast + underline = klassisk
 * web-affordance som beviser at det ER en lenke. Ekskluder .btn (buttons
 * har egen chrome). */
.block--text a:not(.btn) {
    text-decoration: underline;
    text-underline-offset: 2px;
}
.block--text a:not(.btn):hover { text-decoration-thickness: 2px; }

/* Stretched-link — whole card clickable (Bootstrap-style, 2026-07-07).
 *
 * When the customer sets a Lenke-URL on a card without a Lenke-tekst,
 * CardBlock.php emits an empty `<a class="card__stretched">` inside the
 * <article>. CSS below positions it absolute over the entire card so
 * clicks anywhere activate the link. Other interactive elements inside
 * the card (card__link with arrow, pasted anchors in text blocks) sit
 * on a higher z-index so they still respond to direct clicks.
 *
 * a11y: :focus-visible ring on the stretched-link covers the whole card
 * — matches how keyboard users expect a card-sized target to look. */
.block--card { position: relative; }
.block--card .card__stretched {
    position: absolute;
    inset: 0;
    z-index: 1;
    text-decoration: none;
}
.block--card .card__stretched:focus-visible {
    outline: 2px solid var(--site-accent, currentColor);
    outline-offset: 4px;
    border-radius: inherit;
}
/* Everything else interactive inside the card lifts above the stretched
 * overlay so a customer clicking a card__link, a button, or a linked
 * word in a text block hits that element — not the stretched link. */
.block--card .card__link,
.block--card .btn,
.block--card a:not(.card__stretched) {
    position: relative;
    z-index: 2;
}

/* Light overlays (2026-07-07) — additive to the per-theme dark overlays
 * (`.section--overlay-{light,medium,dark}` = rgba(0,0,0,X) in each theme.css).
 * The customer picks `Lys lett / Lys middels / Lys sterk` from the sidebar
 * Overlay dropdown when they want a white wash over a dark-toned bg image
 * instead of dimming it further. Kept in blocks-shared so all themes get
 * the same treatment — the per-theme dark overlays vary slightly in alpha
 * (0.25/0.5/0.7 default, natt uses 0.3/0.55/0.75, eksklusiv uses different
 * hues) and that per-theme variation is preserved for the dark side. Light
 * side is canonical 0.25/0.5/0.7 white until we have a reason to vary. */
.section--overlay-light-white::before,
.section--overlay-medium-white::before,
.section--overlay-dark-white::before,
.hero--overlay-light-white::before,
.hero--overlay-medium-white::before,
.hero--overlay-dark-white::before {
    content: "";
    position: absolute;
    inset: 0;
    background: rgba(255, 255, 255, 0.25);
    pointer-events: none;
    z-index: 0;
    border-radius: inherit;
}
.section--overlay-medium-white::before,
.hero--overlay-medium-white::before { background: rgba(255, 255, 255, 0.5); }
.section--overlay-dark-white::before,
.hero--overlay-dark-white::before   { background: rgba(255, 255, 255, 0.7); }

/* Video-embed block (2026-07-07) — YouTube MVP.
 *
 * Both published iframe (.embed__frame) and editor thumbnail
 * (.embed__thumb) share the same 16:9 wrapper so the canvas preview
 * matches the published rectangle exactly. Iframe is positioned
 * absolute inside the wrapper — the classic responsive-video pattern.
 *
 * The thumbnail-only editor state is layered: <img> fills the frame,
 * .embed__play-button sits centred with a semi-transparent circle
 * behind the triangle so it reads at the same visual weight as the
 * eventual YouTube player controls. */
.block--embed .embed__frame {
    position: relative;
    aspect-ratio: 16 / 9;
    background: #000;
    border-radius: var(--site-radius, 8px);
    overflow: hidden;
    width: 100%;
}
.block--embed .embed__frame iframe {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
}
.block--embed .embed__thumb img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.block--embed .embed__play-button {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    color: #fff;
}
.block--embed .embed__play-icon {
    width: 64px;
    height: 64px;
    background: rgba(0, 0, 0, 0.55);
    border-radius: 50%;
    padding: 14px 14px 14px 20px;
    box-sizing: border-box;
}
/* Vimeo canvas-placeholder — Vimeo har ingen fri thumbnail-URL uten
 * oEmbed-fetch, så editor viser en branded plassholder istedet for
 * en img. #1ab7ea er Vimeos merkefarge. */
.block--embed .embed__thumb--vimeo {
    background: #1ab7ea;
    display: flex;
    align-items: center;
    justify-content: center;
}
.block--embed .embed__vimeo-placeholder {
    color: #fff;
    text-align: center;
    display: flex;
    flex-direction: column;
    gap: 4px;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.block--embed .embed__vimeo-mark {
    font-size: 32px;
    font-weight: 700;
    letter-spacing: 0.5px;
}
.block--embed .embed__vimeo-id {
    font-size: 13px;
    opacity: 0.85;
    font-variant-numeric: tabular-nums;
}

/* Map block (2026-07-07) — Leaflet + OSM tiles.
 *
 * Height variants match MapBlock::HEIGHTS keys so the customer's
 * sidebar select stays the source of truth. `.map__canvas` fills the
 * block; Leaflet mounts inside. Attribution sits below the map so the
 * copyright line stays legible even when the tile layer covers dark
 * imagery.
 *
 * No hardcoded border-radius here — sidebar-first (architecture.md
 * § Fire): rounding is a Spacing-widget choice on the customer side,
 * inherited via inline style on the block wrapper. Setting it in shared
 * CSS drifted from what the editor sidebar displayed (canvas iframe
 * doesn't load blocks-shared.css, so editor showed 0 while published
 * showed 8px). `overflow: hidden` stays so tiles clip cleanly against
 * whatever radius the Spacing widget applies. */
.block--map { display: block; }
.block--map .map__canvas {
    width: 100%;
    overflow: hidden;
    background: #e5e7eb;
    border-radius: inherit; /* pick up the block wrapper's radius */
}
.block--map--h-s .map__canvas { height: 280px; }
.block--map--h-m .map__canvas { height: 400px; }
.block--map--h-l .map__canvas { height: 560px; }
.block--map .map__attribution {
    margin: 6px 0 0;
    font-size: 12px;
    color: var(--site-muted, #64748b);
}
.block--map .map__attribution a { color: inherit; text-decoration: underline; }
/* Editor canvas: live Leaflet-mount. Same height variants + same
 * inherit-radius rule so the editor rectangle matches the published one. */
.block--map .map-canvas-live {
    width: 100%;
    overflow: hidden;
    background: #e5e7eb;
    border-radius: inherit;
}
.block--map--h-s .map-canvas-live { height: 280px; }
.block--map--h-m .map-canvas-live { height: 400px; }
.block--map--h-l .map-canvas-live { height: 560px; }
/* Leaflet popup — inherit theme typography inside; keep the frame tight. */
.block--map .leaflet-popup-content { margin: 10px 12px; font-size: 14px; line-height: 1.4; }
.block--map .leaflet-popup-content strong { display: block; margin-bottom: 4px; }
.block--map .leaflet-popup-content span { display: block; color: #475569; }
/* DivIcon wrapper Leaflet gives our SVG marker gets a transparent
 * background so the pin's coloured path is what the customer sees. */
.block--map__pin-icon { background: transparent; border: 0; }

/* Cards keep flow-margin regardless of palette-bg — 2026-07-05 follow-up.
 *
 * Every theme.css declares a wildcard band rule that zeroes margin on any
 * block with palette-bg so hero/section/columns.full butter mot hverandre
 * for multi-colour page layouts:
 *
 *     .block--hero, .block--section, .block--columns.columns--full,
 *     .block[class*="bg--"] { margin-block: 0; }
 *
 * The `.block[class*="bg--"]` catch-all incorrectly captures card-with-
 * palette. Kort er en selvstendig celle (egen bg fra `.block--card`), ikke
 * en full-width bånd. Effekt før fixen: Standard-swatch (paletteBg='default'
 * → ingen .bg--* klasse) beholdt default 32px flow-margin; Accent/Mørk/
 * Egen-swatch la til `.bg--<x>` som utløste båndregelen → margin: 0. Kort
 * "hoppet" i høyde ved bytte mellom swatches.
 *
 * Restore flow-margin for card-with-palette. Spec 30 (.block + .block--card
 * + [class*=]) beats tema's spec 20 wildcard. Value matches the shared
 * `.block { margin-block: var(--block-flow-space, ...) }` default in this
 * file so cards inherit any tema `--block-flow-space` override.
 */
.block.block--card[class*="bg--"] {
    margin-block: var(--block-flow-space, calc(var(--site-space, 1rem) * 1.5));
}

/* Container-child reset — flex-gap on `.card__body` / `.section__inner` /
 * `.hero__text` / `.column__body` is single source of vertical rhythm.
 * The existing `.card__body > .block { margin-block: 0 }` reset (spec 20)
 * loses to our spec-30 rule above, so we re-assert at spec 40 (parent +
 * three-token compound) for card-with-palette living inside a container.
 */
.card__body > .block.block--card[class*="bg--"],
.section__inner > .block.block--card[class*="bg--"],
.hero__text > .block.block--card[class*="bg--"],
.column__body > .block.block--card[class*="bg--"] {
    margin-block: 0;
}

/* ============================================================
 * Stats flat variant: distribute across parent width — 2026-07-05 fix.
 *
 * All 8 themes ship `.block--stats { display: flex; justify-content:
 * flex-start; ... }`, which clusters 3-4 stats at the left edge of a
 * wide section so 30-40% of the row is empty. Stretching items via
 * `flex: 1 1 0` fills the row evenly — every stat gets equal width,
 * the theme's `gap` handles spacing. Reads cleanly whether there are
 * 2, 3 or 4 stats, and wrap-behaviour survives on narrow parents
 * (small screens / column-child) since flex-wrap: wrap is still on
 * the parent from the theme's base rule.
 *
 * `.stats--boxed` (grid-based) is excluded — the auto-fit grid already
 * handles its own sizing. Alignment overrides `.stats--center` /
 * `.stats--right` are still meaningful for wrapped rows so we leave
 * them alone. Spec 30 (.block + .block--stats + .stat) beats each
 * theme's spec-10 base .stat rule.
 * ============================================================ */
.block.block--stats:not(.stats--boxed) > .stat {
    flex: 1 1 0;
    min-width: max-content;
}

/* ============================================================
 * Hero — empty-bg placeholder.
 * When a hero has no image / video / palette / custom colour picked,
 * it would otherwise bleed into the theme background — user reported
 * 2026-07-03 «vanskelig å se kantene». Show a bundled Unsplash starry-
 * sky (benjamin-voros-phIFdC6lA4E, 1200x801/75q ≈ 125 KB) so the block
 * has visible edges + hints at what a real hero could look like.
 *
 * Historically (F4-hero-empty-canvas, 2026-07-03) this lived in
 * `editor.js#injectThemeCss` and was canvas-only. That broke WYSIWYG
 * parity — the block looked different in preview / publish, which
 * confused the user (2026-07-04). Rule promoted here so all three
 * render targets (canvas / preview / published) show identical
 * starting state, and the same customer-facing background swap
 * removes the placeholder consistently once real content lands.
 *
 * The negated classes match:
 *   - `.hero--has-bg`         — real image / poster (JS + PHP add this)
 *   - `.hero--has-bg-video`   — video bg
 *   - `[class*="bg--"]`       — palette (bg--accent, bg--muted…),
 *                                custom hex (bg--custom, F4-409),
 *                                any future palette variant.
 *
 * `background-attachment: local` prevents mobile Safari from painting
 * a jarring full-height image behind the entire viewport when the
 * hero itself is short.
 *
 * SitePublisher copies /assets/img/hero-canvas-placeholder.jpg to
 * /sites/<slug>/assets/img/ at publish time so the path resolves on
 * customer domains. */
/* 2026-07-05: hero demo-bg is now a real data preset (`bg_demo=true`
   on hero JSON), not a CSS-only placeholder. HeroBlock.php + hero.gjs.js
   both emit `/assets/img/hero-canvas-placeholder.jpg` as the bg URL —
   editor, preview, and publish all render identically. SitePublisher
   copies the asset into `/sites/<slug>/assets/img/` at publish. Design
   packs and templates never carry `bg_demo=true`, so themes that ship
   a hero without a bg (e.g. frisk's media-right side-photo) stay
   bg-less. This supersedes the earlier CSS-only attempts that leaked
   the demo bg onto themes/packs that didn't want it. */

/* 2026-07-05: centered-hero text alignment fix — safety net for themes
   that impose `max-width` on `.block--hero .block--text p`. Without a
   width cap this rule is a no-op (a full-width block-level p ignores
   `margin-inline: auto`), but if a theme narrows the paragraph the auto
   margins recenter it inside `.hero--centered .hero__inner`.
   Cross-tema fix per architecture.md § 5.
   2026-07-06: frisk dropped its own `.block--hero .block--text p {
   max-width: 40ch }` (customer had no sidebar control to widen it — the
   inline-styled seed only survived until first meaningful edit). No
   shipped theme currently triggers this rule; kept as a safety net. */
.hero--centered .hero__text .block--text p {
    margin-inline: auto;
}

/* Hero bg-embed (YouTube). Speiler .hero__bg-video-slottet: absolute-
   positionert first-child bak .hero__inner. YouTube-iframen er alltid 16:9
   og hero-høyden varierer (s/m/l/xl), så vi cover-fitter med container-query-
   units: max(100cqw, 100cqh * 16/9) og speilvendt for høyden ⇒ iframen
   skaleres opp så den fyller parent i BEGGE akser, uansett parent-aspect;
   overflow: hidden kropper det ekstra. Container-type: size aktiverer cqw/cqh
   relativt til .hero__bg-embed (som selv er 100% via inset: 0 av parent hero).
   Pointer-events off så child-blokker (heading/text) er klikkbare.
   prefers-reduced-motion: skjul iframen (YouTube autoplay respekterer ikke
   OS-preferansen selv, så vi skjuler hele wrapperen — poster faller inn
   som fallback via hero--has-bg-video-mekanikken). */
.hero__bg-embed {
    position: absolute; inset: 0; overflow: hidden;
    z-index: 0; pointer-events: none;
    container-type: size;
}
.hero__bg-embed iframe {
    position: absolute; top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    width:  max(100cqw, calc(100cqh * 16 / 9));
    height: max(100cqh, calc(100cqw * 9 / 16));
    border: 0; pointer-events: none;
}
.hero__bg-embed--canvas img {
    position: absolute; inset: 0;
    width: 100%; height: 100%; object-fit: cover;
}
@media (prefers-reduced-motion: reduce) {
    .hero__bg-embed { display: none; }
}

/* Overlay-fix for video/embed-bg. Tema-CSS gir `.hero--overlay-<X>::before`
   z-index: 0 (samme som `.hero__bg-video` / `.hero__bg-embed`) → video/iframe
   dekker overlay-tint. Overlay må sitte MELLOM bg-media og innhold. Denne
   regelen løfter overlay til z-index 1 og innhold til z-index 2 kun når bg
   faktisk er video eller embed; image-bg bruker CSS custom property og
   trenger ikke løftet. Speilet for section-video. Cross-tema fix per
   architecture.md § Fem. */
.hero--has-bg-video.hero--overlay-light::before,
.hero--has-bg-video.hero--overlay-medium::before,
.hero--has-bg-video.hero--overlay-dark::before,
.hero--has-bg-video.hero--overlay-light-white::before,
.hero--has-bg-video.hero--overlay-medium-white::before,
.hero--has-bg-video.hero--overlay-dark-white::before,
.section--has-bg-video.section--overlay-light::before,
.section--has-bg-video.section--overlay-medium::before,
.section--has-bg-video.section--overlay-dark::before {
    z-index: 1;
}
.hero--has-bg-video .hero__inner,
.section--has-bg-video .section__inner {
    z-index: 2;
}

/* Text-block alignment (sidebar-eid, speiler heading--center/right). Emittet
   av TextBlock::render + text.gjs.js#classFor når data.align !== 'left'.
   `text-align` cascades til alle `<p>`/`<li>`-etterkommere så én regel dekker
   både uformatert og RTE-produsert markup. Ikke sett margin — spacing-panelet
   eier den akselen. */
.block--text.text--center { text-align: center; }
.block--text.text--right  { text-align: right; }

/* 2026-07-05: block-margin STACKING inside flex-gap containers.
   Every shipped theme has a generic `.block { margin-block: calc(var
   (--site-space) * N) }` (N ∈ [1.5, 2.5]) that gives blocks vertical
   breathing room in the ordinary flow. But when the parent is a
   flex column that already spaces its children via `gap` — the four
   composable containers below — that block-margin STACKS on top of
   the gap (flexbox `gap` does NOT collapse with item margins). Two
   consecutive children add:
     child1.margin-bottom + parent.gap + child2.margin-top
   which for a theme with N=2 and gap=0.5×site-space gives a visual
   space of 4.5×site-space between siblings — customer reported it
   as "avstand er hardkodet og ignorer Spacing-panelet".
   Root fix: neutralise the block-margin inside these parents so the
   parent's `gap` is the single source of vertical rhythm. Applies
   to ALL block types (heading, text, button, image, …), not just
   heading. The Spacing panel still overrides on a per-block basis
   when the customer sets a value (inline style wins over this rule).
   Per-theme `.hero__text > .block { margin-block: ... }` rules
   duplicate this and can be dropped from each theme in a follow-up.
   Cross-tema fix per architecture.md § 5. Verified visually on
   default + frisk + kyst. */
/* Task D (2026-07-05): consolidated block-flow-space. Every shipped theme
   used to declare its own `.block { margin-block: calc(var(--site-space)
   * N) }` where N varied (default/delikat/leken/natt/skandi = 1.5, frisk/
   kyst = 2, eksklusiv = 2.5). Duplication meant a change to the default
   flow-space needed edits across 8 files, and themes couldn't override
   just the spacing amount without repeating the rule. Now the DEFAULT
   lives here reading `--block-flow-space`; themes only set the token to
   override (see themes/<name>/theme.css :root). The reset below (0 for
   flex-gap containers) wins on specificity in card / section / hero /
   column contexts. Spacing-panel inline styles always win regardless. */
.block {
    margin-block: var(--block-flow-space, calc(var(--site-space, 1rem) * 1.5));
}

.card__body > .block,
.section__inner > .block,
.hero__text > .block,
.column__body > .block {
    margin-block: 0;
}

/* ==================================================================
 * Task N (2026-07-05): unified hero heights — vh-based min-heights so
 * the size trait actually reads visually. Previous per-theme padding-
 * based approach made "Høy" only ~140px tall (~6×site-space) which the
 * customer read as "middels" at best — there was no real difference
 * between the sizes without knowing the padding-tokens trick.
 *
 * New scale (min-height, flex-center for vertical alignment):
 *   Lav (s)      ~ 30vh
 *   Middels (m)  ~ 50vh   (half the viewport)
 *   Høy (l)      ~ 75vh   ("almost fullscreen", customer's spec)
 *   Full (xl)    ~ 100vh  (unchanged, per-theme handles header overlap)
 *
 * flex-column + justify-content: center promotes the xl-hero pattern to
 * all sizes so content stays vertically centered as heros grow — without
 * this, a 75vh Høy hero with 200px of content would sit at the top with
 * 400px empty space below. Cross-tema fix per architecture.md § 5.
 * Verified on default + frisk + kyst. */
.block--hero .hero__inner {
    display: flex;
    flex-direction: column;
    justify-content: center;
}
.hero--h-s .hero__inner { min-height: 30vh; }
.hero--h-m .hero__inner { min-height: 50vh; }
.hero--h-l .hero__inner { min-height: 75vh; }

/* ==================================================================
 * Task G (2026-07-05): news-block layout variants. Five layouts share
 * the same item markup; visual differentiation lives here so themes
 * don't need to reimplement them per-brand. Cross-tema regel per
 * architecture.md § 5 — verify on default + frisk + kyst before commit.
 *
 * All layouts read a `.news-item` with an optional `.news-item__image`
 * ahead of `.news-item__body`; when the image is absent (article has no
 * cover_media_id) the card compacts to text-only automatically.
 * ================================================================== */

.block--news .news-list {
    list-style: none;
    margin: 0;
    padding: 0;
}
/* Universal image-container contract: every layout renders `.news-item__image`
   as a block with a fixed aspect-ratio (set per-layout), and the <img> inside
   fills that box with object-fit: cover. `overflow: hidden` is the belt to
   the object-fit suspenders — even if a browser somehow ignores object-fit
   for a specific image, the container clips it. Guarantees that a customer
   uploading a disproportional image (4:5, 21:9, whatever) can never break
   the grid alignment across items.
   No background colour here — `object-fit: cover` always fills the box, and
   an empty grey fallback would bleed through sub-pixel gaps at the parent
   card's rounded corners (visible as a grey spike where the image meets
   the border). If a customer's article has no cover, the resolver omits
   `.news-item__image` entirely and the card falls into a text-only variant. */
.block--news .news-item__image {
    display: block;
    overflow: hidden;
    border-radius: calc(var(--site-radius, 8px) - 2px);
}
.block--news .news-item__image img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* Also set intrinsic aspect on the img itself so browsers with partial
       aspect-ratio support (very old Safari) can still compute the img's
       height correctly from its parent container's aspect-ratio. */
    aspect-ratio: inherit;
}
.block--news .news-item__title {
    margin: 0 0 .25rem;
    font-size: 1.15rem;
    line-height: 1.3;
}
.block--news .news-item__title a { text-decoration: none; color: inherit; }
.block--news .news-item__title a:hover { text-decoration: underline; }
.block--news .news-item__lead {
    margin: 0 0 .5rem;
    color: color-mix(in srgb, var(--site-text, #333) 75%, transparent);
    font-size: .95rem;
    line-height: 1.5;
}
.block--news .news-item__date {
    color: var(--site-muted, #6b7280);
    font-size: .85rem;
}

/* --- list-compact (default) — plain vertical list, title + date. --- */
.block--news.news--list-compact .news-item {
    padding-block: .75rem;
    border-bottom: 1px solid var(--site-border, #e5e7eb);
}
.block--news.news--list-compact .news-item:last-child { border-bottom: 0; }
.block--news.news--list-compact .news-item__image { display: none; }
.block--news.news--list-compact .news-item__lead   { display: none; }

/* --- list-media — full-width card, image left, text right. --- */
.block--news.news--list-media .news-list {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}
.block--news.news--list-media .news-item {
    display: grid;
    grid-template-columns: minmax(140px, 220px) 1fr;
    gap: 1.25rem;
    padding: 1rem;
    border: 1px solid var(--site-border, #e5e7eb);
    border-radius: var(--site-radius, 8px);
    background: var(--site-surface, #f9fafb);
}
.block--news.news--list-media .news-item > article {
    display: contents;
}
.block--news.news--list-media .news-item__image {
    aspect-ratio: 4/3;
}
.block--news.news--list-media .news-item:not(:has(.news-item__image)) {
    grid-template-columns: 1fr;
}

/* --- grid — three-column cards. --- */
.block--news.news--grid .news-list {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1.25rem;
}
.block--news.news--grid .news-item {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--site-border, #e5e7eb);
    border-radius: var(--site-radius, 8px);
    overflow: hidden;
    background: var(--site-surface, #f9fafb);
}
.block--news.news--grid .news-item > article {
    display: flex;
    flex-direction: column;
    height: 100%;
}
.block--news.news--grid .news-item__image {
    aspect-ratio: 16/10;
    /* Reset the shared radius so the image sits flush with the card top. */
    border-radius: 0;
}
.block--news.news--grid .news-item__body {
    padding: 1rem;
    display: flex;
    flex-direction: column;
    gap: .35rem;
    flex: 1;
}

/* --- hero-plus-two — hero-item to the left, two smaller stacked on the
       right. Grid is 2 columns × 2 rows; the hero-item spans both rows
       in the left column, and the remaining two items flow into the
       right column one per row. Matches «Én stor + to på siden» — the
       small items live BESIDE the big one, not below it. --- */
.block--news.news--hero-plus-two .news-list {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    grid-template-rows: 1fr 1fr;
    gap: 1.25rem;
}
.block--news.news--hero-plus-two .news-item {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--site-border, #e5e7eb);
    border-radius: var(--site-radius, 8px);
    overflow: hidden;
    background: var(--site-surface, #f9fafb);
}
.block--news.news--hero-plus-two .news-item > article {
    display: flex;
    flex-direction: column;
    height: 100%;
}
/* Hero cell takes the left column across both rows. */
.block--news.news--hero-plus-two .news-item--hero {
    grid-column: 1;
    grid-row: 1 / -1;
}
.block--news.news--hero-plus-two .news-item__image {
    aspect-ratio: 16/10;
    border-radius: 0;
}
/* Hero image fills the taller cell — image grows to consume its row/col.
   `flex: 1` on the anchor + `height: 100%` on the img (already set in
   shared rules) makes the image occupy the cell height instead of
   its own aspect-ratio, so both left and right columns visually align. */
.block--news.news--hero-plus-two .news-item--hero .news-item__image {
    aspect-ratio: auto;
    flex: 1;
    min-height: 0;
}
.block--news.news--hero-plus-two .news-item--hero .news-item__title {
    font-size: 1.6rem;
}
.block--news.news--hero-plus-two .news-item__body {
    padding: 1rem;
    /* Small cards: title should stay at the bottom under a fixed-ratio
       image, so no flex-grow. Hero card: body sits under the image
       with normal padding — same declaration works for both. */
}

/* --- stacked-big — full-width big cards stacked. --- */
.block--news.news--stacked-big .news-list {
    display: flex;
    flex-direction: column;
    gap: 2rem;
}
.block--news.news--stacked-big .news-item {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--site-border, #e5e7eb);
    border-radius: var(--site-radius, 8px);
    overflow: hidden;
    background: var(--site-surface, #f9fafb);
}
.block--news.news--stacked-big .news-item__image {
    aspect-ratio: 21/9;
    border-radius: 0;
}
.block--news.news--stacked-big .news-item__title {
    font-size: 1.5rem;
}
.block--news.news--stacked-big .news-item__body { padding: 1.5rem; }

/* Mobile: compact grid + hero-plus-two into a single column. */
@media (max-width: 720px) {
    .block--news.news--grid .news-list,
    .block--news.news--hero-plus-two .news-list {
        grid-template-columns: 1fr;
        grid-template-rows: auto;
    }
    .block--news.news--hero-plus-two .news-item--hero {
        grid-column: auto;
        grid-row: auto;
    }
    .block--news.news--hero-plus-two .news-item--hero .news-item__image {
        aspect-ratio: 21/9;
        flex: none;
    }
    .block--news.news--list-media .news-item {
        grid-template-columns: 1fr;
    }
}

/* ==================================================================
 * F4-609 trinn 3: Gallery karusell + lightbox.
 *
 * `.block--gallery[data-layout]` gates karusell-oppførsel så rutenett
 * (default, ingen data-layout) er urørt. Scroll-snap gjør at NO-JS-
 * brukere fortsatt kan bla horisontalt; JS-en i blocks-shared.js
 * legger på autoplay + dots + arrows + tastatur-nav.
 * ================================================================== */

.block--gallery[data-layout^="carousel"] {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    /* Skjul standard scrollbar; JS-en gir dots/arrows som visuelt
       fyller navigasjons-rollen. Bruker kan fortsatt bla via trackpad. */
    scrollbar-width: none;
    gap: 0;
    padding-inline: 0;
    /* Container-lignende positioning gir dots + arrows noe å ankre til. */
    position: relative;
    /* Tema-avhengig aspect-forhold. Vi tvinger ikke en spesifikk høyde. */
}

.block--gallery[data-layout^="carousel"]::-webkit-scrollbar {
    display: none;
}

.block--gallery[data-layout^="carousel"] > .block__item {
    scroll-snap-align: center;
    scroll-snap-stop: always;
    flex: 0 0 100%;
    max-width: 100%;
    /* Reset the theme's grid-cell padding. Karusell-cellen dekker hele bredden. */
    padding: 0;
    margin: 0;
    list-style: none;
}

.block--gallery[data-layout^="carousel"] > .block__item > .block__media {
    width: 100%;
    height: 100%;
    max-height: 70vh;
    object-fit: cover;
    display: block;
}

/* Nav-piler — bygges dynamisk av JS-en; vi styler dem her. */
.blokky-carousel-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: 0;
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    cursor: pointer;
    padding: 0;
    z-index: 2;
    transition: background 120ms, opacity 120ms;
}

.blokky-carousel-nav:hover,
.blokky-carousel-nav:focus-visible {
    background: rgba(0, 0, 0, 0.75);
    outline: none;
}

.blokky-carousel-nav:focus-visible {
    box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.6);
}

.blokky-carousel-nav[data-dir="prev"] { left: 12px; }
.blokky-carousel-nav[data-dir="next"] { right: 12px; }

.blokky-carousel-nav[disabled] {
    opacity: 0.35;
    cursor: default;
}

.blokky-carousel-nav svg {
    width: 22px;
    height: 22px;
    stroke: currentColor;
    fill: none;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Prikker — bygges dynamisk av JS-en. Sentrert under karusellen. */
.blokky-carousel-dots {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 8px;
    margin-top: var(--space-3, 12px);
    padding: 0;
}

.blokky-carousel-dots__dot {
    appearance: none;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    border: 0;
    padding: 0;
    background: color-mix(in srgb, currentColor 30%, transparent);
    cursor: pointer;
    transition: background 120ms, transform 120ms;
}

.blokky-carousel-dots__dot--active {
    background: currentColor;
    transform: scale(1.2);
}

.blokky-carousel-dots__dot:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 2px;
}

/* Lightbox — vises som `<dialog>`, JS-en åpner/lukker.
   Én modal per side, gjenbrukes for alle gallerier med data-lightbox. */
.blokky-lightbox {
    padding: 0;
    border: 0;
    background: transparent;
    max-width: 100vw;
    max-height: 100vh;
    width: 100vw;
    height: 100vh;
    color: #fff;
}

.blokky-lightbox::backdrop {
    background: rgba(0, 0, 0, 0.92);
}

.blokky-lightbox__inner {
    display: grid;
    grid-template-rows: auto 1fr auto;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    justify-items: center;
    width: 100%;
    height: 100%;
    padding: 24px;
    box-sizing: border-box;
}

.blokky-lightbox__close {
    grid-row: 1;
    grid-column: 3;
    justify-self: end;
    appearance: none;
    background: rgba(255, 255, 255, 0.15);
    color: #fff;
    border: 0;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background 120ms;
}

.blokky-lightbox__close:hover,
.blokky-lightbox__close:focus-visible {
    background: rgba(255, 255, 255, 0.35);
    outline: none;
}

.blokky-lightbox__nav {
    appearance: none;
    background: rgba(255, 255, 255, 0.15);
    color: #fff;
    border: 0;
    width: 56px;
    height: 56px;
    border-radius: 50%;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background 120ms;
    grid-row: 2;
}

.blokky-lightbox__nav--prev { grid-column: 1; }
.blokky-lightbox__nav--next { grid-column: 3; }

.blokky-lightbox__nav:hover,
.blokky-lightbox__nav:focus-visible {
    background: rgba(255, 255, 255, 0.35);
    outline: none;
}

.blokky-lightbox__nav svg,
.blokky-lightbox__close svg {
    width: 26px;
    height: 26px;
    stroke: currentColor;
    fill: none;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.blokky-lightbox__stage {
    grid-row: 2;
    grid-column: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    max-width: 100%;
    max-height: 100%;
    overflow: hidden;
}

.blokky-lightbox__img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    display: block;
}

.blokky-lightbox__caption {
    grid-row: 3;
    grid-column: 1 / -1;
    margin: 16px 0 0;
    text-align: center;
    font-size: 14px;
    color: rgba(255, 255, 255, 0.9);
}

/* Gallery-item i lightbox-modus — pekefinger så affordansen er klar. */
.block--gallery[data-lightbox="1"] .block__item {
    cursor: zoom-in;
}
.block--gallery[data-lightbox="1"] .block__media {
    /* Fjern outline på fokus via klikk; behold ved keyboard-fokus. */
    transition: transform 200ms;
}
.block--gallery[data-lightbox="1"] .block__item:hover .block__media {
    transform: scale(1.02);
}

/* Reduced-motion: no scale-hover, no smooth-scroll. Autoplay stopper i JS. */
@media (prefers-reduced-motion: reduce) {
    .block--gallery[data-layout^="carousel"] {
        scroll-behavior: auto;
    }
    .block--gallery[data-lightbox="1"] .block__item:hover .block__media {
        transform: none;
    }
    .blokky-carousel-dots__dot--active {
        transform: none;
    }
}
