/* ============================================================================
   STRUCTURAL COMPONENTS — LOCKED. Do not edit this file (the design audit
   verifies its integrity). It carries structure only — layout, spacing
   discipline, accessibility, responsive behavior — and takes ALL of its look
   from tokens.css. Restyle by setting tokens; extend freely in app.css.

   Why locked: every recurring defect we shipped (footers flush against the
   viewport, uneven padding, invisible buttons, unstyled mobile nav) came from
   rebuilding these primitives per project. The structure is solved once, here.
   ========================================================================== */

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--text-base);
  color: var(--text);
  background: var(--bg);
  line-height: var(--leading);
  min-height: 100vh;
  overflow-wrap: break-word;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-display);
  line-height: var(--leading-heading);
  text-wrap: balance;
}
h1 { font-size: var(--text-h1); line-height: var(--leading-tight); }
h2 { font-size: var(--text-h2); }
h3 { font-size: var(--text-h3); }
p { text-wrap: pretty; }
img, picture, video, canvas, svg { max-width: 100%; }
img, video { height: auto; }
img { display: block; }
/* Placeholder images from the {{ph}} helper. The SVG carries width/height
   attributes for its aspect ratio, so without this it renders at that intrinsic
   size instead of filling its slot. That makes this sizing load-bearing, not
   taste — and it lived only in app.css, the file whose own header invites a full
   rewrite. A build reported every placeholder on the site silently distorted and
   cropped after a theme rewrite dropped the one rule, until the agent worked out
   what was missing and put it back. Colour and treatment stay themable in
   app.css, which is later in the cascade and still wins. */
.ph { display: block; width: 100%; height: auto; }
/* Inline icons, from the vendored Lucide set via {{icon "name"}}. Sized in em
   so a glyph scales with the text it sits beside instead of being a fixed 24px
   next to a 14px label, and stroke/fill stay currentColor so an icon takes the
   colour of its context like everything else here. Restyle freely in app.css —
   only the sizing baseline and the no-shrink live in the locked file, because a
   flex row squashing an icon into an oval is a layout bug, not a taste one. */
.icon { width: 1.25em; height: 1.25em; flex: none; vertical-align: -.15em; }
/* The placeholder's LABEL keeps the page's own ink. The helper already renders
   it as currentColor at 65%, which is legible by construction — it is the same
   colour the surrounding text uses — and every placeholder legibility failure
   on record came from a project overriding that fill with a decorative colour:
   a brass label on a dark gradient that no contrast model could pass, and a
   2:1 caption on cream that shipped because nothing measured it. The field and
   the edge below stay yours to restyle; the label's COLOUR is not a design
   surface, because a caption nobody can read fails the one job a placeholder
   has. Opacity, size and letter-spacing remain overridable. */
.ph .ph-label { fill: currentColor !important; fill-opacity: .65; }
/* The field, edge and crop marks are currentColor at a tint. The tint is mixed
   INTO the colour rather than applied as a separate fill-opacity, because the
   opacity used to be an inline SVG attribute the helper wrote: a project that
   restyled `.ph-field { fill: #DDD0BB }` still got its light field at 6%, which
   over a dark card is a black void — shipped on every work card of a portfolio
   home page. Mixed in, one declaration does what it says, and a project that
   wants the starter tint back can simply not write one.
   IF YOU RESTYLE .ph-field, SET `color` ON THE .ph TOO. The label above is
   locked to the page's ink, so a light field painted onto a dark page keeps a
   light label and disappears; `color` moves the label, the edge and the marks
   with it, which is the one knob that keeps all four legible together. */
.ph-field { fill: color-mix(in srgb, currentColor 6%, transparent); }
.ph-edge { stroke: color-mix(in srgb, currentColor 28%, transparent); }
.ph-mark { stroke: color-mix(in srgb, currentColor 22%, transparent); }

/* Universal keyboard affordance. The second outline keeps the ring visible on
   both pale and dark project palettes without prescribing a visual theme. */
:where(a, button, input, textarea, select, summary, [tabindex]):focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
  box-shadow: 0 0 0 1px var(--focus-offset);
}
:where(a, button, summary, label[for]) { touch-action: manipulation; }

.skip-link {
  position: fixed; top: var(--space-2); left: var(--container-pad); z-index: 100;
  transform: translateY(calc(-100% - var(--space-4)));
  padding: var(--space-2) var(--space-4); border-radius: var(--radius-sm);
  color: var(--accent-ink); background: var(--accent); font-weight: 700;
}
.skip-link:focus { transform: translateY(0); }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* The `hidden` attribute must actually hide. The browser's own rule for it is
   a UA rule, so ANY author rule that sets display on the same element wins —
   and `.card { display: flex }` is the first thing anyone writes. Two separate
   builds shipped a filter that set hidden on the filtered-out cards, updated
   the URL and the "showing N of M" counter, and left every card on screen:
   every structural check passed, the page was simply wrong. !important because
   this has to outrank whatever the project CSS does to its own components. */
[hidden] { display: none !important; }

/* Bot trap: a field humans never see and naive bots fill. The WRAPPER carries
   [hidden] (display:none — invisible to the rendered-page audit, so it can't
   trip the touch-target check the way an off-screen input does) and the server
   rejects any submission where it has a value. Copy this pattern verbatim:

     <div hidden aria-hidden="true">
       <label>Leave this field empty
         <input type="text" name="website" tabindex="-1" autocomplete="off">
       </label>
     </div>
*/

/* --- Navigation -------------------------------------------------------- */
/* Solid background by default (--nav-bg): a header must never be invisible
   over a hero. Overlay navs are an explicit token choice, not an accident. */
.nav {
  position: relative;
  display: flex; justify-content: space-between; align-items: center;
  padding: var(--space-4) var(--container-pad);
  background: var(--nav-bg);
  color: var(--nav-ink);
  border-bottom: 1px solid var(--border);
}
.brand { font-weight: 700; color: var(--nav-ink); text-decoration: none; }
/* Base link color: without this, every in-content link ships browser-default
   blue (#0000EE) unless each build remembers to theme it in app.css — and a
   real build shipped exactly that. Bare element selector, so any project rule
   (.section-dark a, .card a, app.css's own a { }) overrides it untouched. */
a { color: var(--accent); }
.navlinks { display: flex; align-items: center; gap: var(--space-4); }
.navlinks a { color: var(--nav-ink); text-decoration: none; }
.navlinks a:hover { text-decoration: underline; text-underline-offset: .22em; }
/* space-between above only balances TWO children. The moment a build adds the
   supported third one — .nav-action, the CTA the mobile block below documents
   and orders — space-between spreads all three evenly and strands the button
   in open space beside the brand, links far away at the other edge. A real
   build shipped exactly that on every page: "Se dagens bröd" floating mid-
   header at 1280px. The phone layout had already solved this with an auto
   margin; desktop was simply never given the same rule.
   Brand left, then ONE auto margin absorbing the free space, so links and
   action group together at the right edge in that order — the CTA reads as
   the end of the navigation, not as a second brand. */
@media (min-width: 721px) {
  /* Without a gap the grouped items have nothing holding them apart and the
     button lands ON the last link — space-between only ever separated the two
     children it knew about. */
  .nav { gap: var(--space-4); }
  .nav > .navlinks { margin-left: auto; }
  /* Ordered, not left to markup: an action written before the links in the
     HTML (the natural way to write it, since the mobile slot wants it as a
     direct child of .nav) would otherwise sit between the brand and the menu. */
  .nav > .nav-action { order: 1; flex: 0 0 auto; white-space: nowrap; }
}

/* Cross-page polish: native navigations get a smooth crossfade on browsers
   that support view transitions (Chrome, Safari, recent Firefox) and a normal
   page load everywhere else. Zero JS, honors reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
}

/* Mobile navigation — links collapse into an accessible hamburger below 720px,
   and the open menu covers the WHOLE viewport (the Tesla/Stripe pattern), not a
   dropdown strip. Pure CSS (the #nav-toggle checkbox drives it): no JavaScript.
   Cramped links on a phone are the #1 "unfinished" tell, so keep a real mobile
   menu on every site.

   Why full-page: a dropdown under the header reads as a widget; a full page of
   large, unhurried links reads as designed — and every generated site shares
   this file, so the one pattern has to be the better one. The panel takes
   --nav-bg, so a project's own header palette carries into the menu without any
   per-site work. */
.nav-toggle, .nav-burger { display: none; }
/* The in-menu duplicate of .nav-action (see the 480px block below): hidden
   everywhere the header's own copy is visible, shown only on the smallest
   phones where components.css hides the header CTA. */
/* .navlinks scope beats the .btn display rules later in this file — the bare
   class alone lost that cascade fight and left both CTAs standing on desktop. */
.navlinks .nav-action-menu { display: none; }
@media (max-width: 480px) { .navlinks .nav-action-menu { display: inline-flex; } }
@media (max-width: 720px) {
  .nav-toggle { display: block; min-height: 0; }
  /* Brand, burger and the persistent .nav-action float ABOVE the open panel, so
     the header row stays visible (and the X reachable) while the menu is up. */
  .nav > .brand, .nav > .nav-action, .nav-burger { position: relative; z-index: 50; }
  .nav-burger {
    display: flex; flex-direction: column; justify-content: center; gap: 5px;
    width: 44px; height: 44px; padding: 11px; cursor: pointer;
    border: 1px solid var(--border); border-radius: var(--radius);
  }
  /* The three bars only. The label also holds a .sr-only span with its
     accessible name, and `.nav-burger span` (0,1,1) outranks `.sr-only`
     (0,1,0) — so without the :not() that span became a fourth full-width bar
     whose absolutely-positioned box pushed the page ~333px wider than the
     viewport on a phone. Found by a build agent that had to work around it in
     app.css; fixed here so no build has to again. */
  .nav-burger span:not(.sr-only) {
    display: block; width: 100%; height: 2px; background: var(--nav-ink);
    transition: transform .2s ease, opacity .2s ease;
  }
  /* The burger's accessible NAME follows its state: the open-label shows by
     default, :checked swaps in the close-label. A checkbox hack cannot rewrite
     an aria-label, so a single static label announces "open menu" while the
     menu is open. display:none removes the inactive one from the accessibility
     tree; .sr-only keeps the active one visually hidden. Both labels carry
     .sr-only, so the bars rule above still ignores them. */
  .nav-burger .nav-label-close { display: none; }
  .nav-toggle:checked ~ .nav-burger .nav-label-open { display: none; }
  .nav-toggle:checked ~ .nav-burger .nav-label-close { display: block; }
  /* Kept display:flex at all times and hidden via visibility/opacity instead of
     display:none, so the panel can fade — display can't transition. */
  .navlinks {
    position: fixed; inset: 0; z-index: 40;
    flex-direction: column; align-items: stretch; justify-content: flex-start;
    gap: 0;
    padding: calc(44px + 2 * var(--space-4) + var(--space-6)) var(--container-pad) var(--space-8);
    background: var(--nav-bg);
    overflow-y: auto;
    visibility: hidden; opacity: 0; transform: translateY(-0.4rem);
  }
  /* DIRECT children only. As a descendant selector this reached into any
     grouped cluster inside the menu and made every link in it a full-width h3
     row: a real build's language switcher — an inline-flex pill of four chips
     with 2px padding — got four giant stacked rows crammed inside a border
     sized for chips. The site's own `.lang-opt` (0,1,0) never stood a chance
     against `.navlinks a:not(.btn):not(.nav-action)` (0,3,1).
     A cluster is ONE menu item, not four; only top-level items stretch.
     `.linklike` stays a descendant on purpose — it is an explicit opt-in the
     markup applies, and the template's own logout button sits inside a form. */
  .navlinks > a:not(.btn):not(.nav-action), .navlinks > form, .navlinks .linklike {
    display: block; width: 100%; text-align: left;
    font-size: var(--text-h3); line-height: var(--leading-heading);
    padding: var(--space-3) 0; border: 0;
  }
  /* …and the cluster itself gets room around it, so it reads as its own row
     rather than being jammed against the link above. */
  .navlinks > :not(a):not(form):not(.btn):not(.nav-action) {
    align-self: flex-start; margin: var(--space-3) 0;
  }
  /* A BUTTON in the menu keeps its own shape. The rule above sets
     `padding: <y> 0` so plain links sit flush with the panel's own inset — and
     it was matching filled buttons too, zeroing their inline padding so the
     label pressed against the edge of the fill. A real build shipped its
     "Begär offert för restaurang" CTA that way: the button still worked, still
     passed contrast and touch-target checks, and simply looked broken. Buttons
     keep their padding and their natural width; only the plain links stretch. */
  .navlinks .btn, .navlinks .nav-action {
    align-self: flex-start; width: auto; margin: var(--space-3) 0;
  }
  .nav-toggle:checked ~ .navlinks { visibility: visible; opacity: 1; transform: none; }
  @media (prefers-reduced-motion: no-preference) {
    .navlinks { transition: opacity .22s ease, transform .22s ease, visibility 0s .22s; }
    .nav-toggle:checked ~ .navlinks { transition: opacity .22s ease, transform .22s ease, visibility 0s 0s; }
  }
  /* The page behind the open menu must not scroll. :has() is progressive
     enhancement — browsers without it just keep background scroll. */
  body:has(.nav-toggle:checked) { overflow: hidden; }
  /* Tapping a link in the OPEN menu flickered. The cross-document view
     transition above captures the outgoing page — which, with the menu open, is
     a full-viewport opaque panel — and cross-fades it over the incoming page,
     where the checkbox is naturally unchecked and the menu is gone. So every
     mobile navigation ended with a full screen of nav colour fading away on top
     of the new page. Desktop never showed it: there is no panel there.
     Naming the panel makes it its own transition group instead of part of the
     root snapshot, and an old group with no animation simply stops being drawn.
     The rest of the page keeps its crossfade. */
  .navlinks { view-transition-name: nav-menu; }
  ::view-transition-old(nav-menu) { animation: none; }
  /* One action may stay OUTSIDE the collapse: mark it .nav-action and place it
     as a direct child of .nav (not inside .navlinks). Sites whose primary
     action is a call or an order button need it visible on a phone, where the
     hamburger hides everything — a real build's plan required an always-visible
     tel: button, and with no supported slot the agent had to re-declare the
     whole collapse in app.css at a different breakpoint to get one.

     The three-item mobile header is ORDERED AND BOUNDED here rather than left
     to whatever order the markup happens to use. Two builds shipped the same
     week proved both failure modes: one wrote .nav-action after the label, so
     the burger landed in the MIDDLE of the header (x=197 of 375); the other's
     brand and CTA were together wider than the phone and overlapped by 85px.
     Neither is a mistake an agent can be told not to make — the geometry has
     to be decided by the locked file.

     Burger last, always: it is the one control every visitor reaches for, and
     on a phone it belongs under the thumb at the right edge whatever the
     markup says. Brand shrinks and truncates; the action never wraps and never
     grows. Everything stays on ONE row. */
  .nav { gap: var(--space-3); flex-wrap: nowrap; }
  /* The customer's own name is never cut. Truncating it was this file's first
     answer to a crowded header, and the very next build shipped "Skomakeriet
     Hor…" on every page — its critic called it the site's worst finding and the
     builder overrode all of these rules in app.css to undo it. Identity outranks
     tidiness: the name wraps if it must, and the SECONDARY action yields first
     (hidden below 480px, still one tap away inside the menu). */
  .nav > .brand {
    order: 0; flex: 0 1 auto; min-width: 0;
    white-space: normal; overflow-wrap: anywhere;
  }
  /* text-overflow only ellipsizes a BLOCK's own inline text. A brand that wraps
     its parts in elements — the common "name + location badge" — is a flex
     container, so the rule above silently HARD-CLIPS the badge mid-element
     instead: a real build shipped a red pill sliced down the middle, which
     reads as a rendering fault rather than a truncation. Give each part its own
     ellipsis, and let anything after the name yield FIRST (shrink factor 100)
     so the business's name is the last thing to give way. */
  .nav > .brand > * { min-width: 0; }
  .nav > .brand > :first-child { flex: 0 1 auto; }
  /* A badge beside the name yields before the name does. */
  .nav > .brand > :first-child ~ * { flex: 0 100 auto; }
  .nav > .nav-action {
    order: 1; flex: 0 0 auto; margin-left: auto;
    display: inline-flex; align-items: center; min-height: 44px;
    white-space: nowrap; max-width: 11rem; overflow: hidden; text-overflow: ellipsis;
  }
  .nav-burger { order: 2; flex: 0 0 auto; }
  /* Below 480px the header is the name and the way in, nothing else.
     THIS HIDES THE CTA: if .nav-action is the site's primary action (a tel:
     jour line, an order button), it MUST also exist inside .navlinks or the
     smallest phones lose it entirely — a real build's first audit pass caught
     exactly that. Duplicate the link; don't move it — and mark the duplicate
     .nav-action-menu, which shows it ONLY where the header copy is hidden.
     Without that class the duplicate rides the desktop nav row next to the
     original: a real build shipped two identical CTAs side by side, and the
     only escape was re-styling in app.css per project. */
  @media (max-width: 480px) { .nav > .nav-action { display: none; } }
  /* The auto margin sits on the ACTION, not the burger: it absorbs the free
     space once, so action and burger group together at the right edge. An auto
     margin on both would DIVIDE the free space between them and float the
     action back into the middle — the bug being fixed. With no action present,
     space-between still puts the lone burger hard right. */
  .nav-toggle:focus-visible + .nav-burger {
    outline: 3px solid var(--focus); outline-offset: 3px;
    box-shadow: 0 0 0 1px var(--focus-offset);
  }
  /* Hamburger morphs into an X while the menu is open. */
  .nav-toggle:checked ~ .nav-burger span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
  .nav-toggle:checked ~ .nav-burger span:nth-child(2) { opacity: 0; }
  .nav-toggle:checked ~ .nav-burger span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
}

/* --- Layout rhythm ------------------------------------------------------ */
.container {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: var(--space-6) var(--container-pad) var(--space-8);
}
/* Every container carries the horizontal inset itself. It used to live only on
   .container, so the composition these variants are DOCUMENTED for —
   .section > .container-wide — had no gutter at all: .section zeroes horizontal
   padding, and a max-width does nothing once the viewport is narrower than it.
   Below 1184px a bare .container-wide is full-bleed, which is every phone; the
   only thing standing between a build and text flush against the screen edge
   was the author remembering to re-add padding on each of their own section
   classes. One shipped site did exactly that on five sections and forgot on the
   sixth — a course detail page with its breadcrumb at x=0.
   Belt and braces with .container's own padding: padding-inline wins over the
   shorthand's horizontal half either way, so nesting stays safe. */
.container-wide { max-width: var(--container-wide); margin-inline: auto; padding-inline: var(--container-pad); }
.container-copy { max-width: var(--container-copy); margin-inline: auto; padding-inline: var(--container-pad); }
/* .container-narrow is the name that matches the token (--container-narrow) and
   its two siblings above. .narrow is the original spelling, kept so workspaces
   restored from older snapshots keep rendering — it was the one class of the
   four whose name broke the pattern, and a build looking for a narrow container
   by analogy wrote .container-narrow, got no rules at all, and shipped a
   full-bleed page. Prefer .container-narrow. */
.container-narrow,
.narrow { max-width: var(--container-narrow); margin-inline: auto; padding-inline: var(--container-pad); }
.prose { max-width: var(--measure-copy); }
.lead { max-width: var(--measure-lead); }

/* Composition primitives: structure without a house style. They give every
   build a disciplined grid while tokens + app.css still decide the identity. */
.split { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-7); align-items: center; }
.grid-2, .grid-3, .grid-4 { display: grid; gap: var(--space-5); }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }
.section-heading { max-width: var(--measure-copy); margin-bottom: var(--space-6); }
.media-frame { overflow: hidden; border-radius: var(--media-radius); }
.media-frame > :where(img, picture, video) { width: 100%; height: 100%; object-fit: cover; }
.trust-strip { display: flex; flex-wrap: wrap; gap: var(--space-4) var(--space-6); align-items: center; }

/* Full-bleed section band: background spans the viewport, content stays inside
   a container. Stack these on the landing page; the shared vertical rhythm and
   horizontal inset come from tokens, so spacing stays consistent by
   construction. Restyle per project with your own band classes in app.css
   (e.g. .band-dark { background: ...; color: ... }). */
.section { padding: var(--section-pad-y) 0; }
.section > .container { padding-top: 0; padding-bottom: 0; }
.section-tint { background: var(--bg-2); }

.hero { padding: var(--space-7) 0 var(--space-6); }
.hero h1 { margin: 0 0 var(--space-2); }
.lead { color: var(--muted); font-size: var(--text-lead); }

.panel {
  background: var(--bg-2); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--space-5); margin-top: var(--space-5);
}
.page-head { padding: var(--space-4) 0 0; }

/* --- Footer -------------------------------------------------------------- */
/* Symmetric vertical padding + the same horizontal inset as every section —
   footer content never sits flush against the viewport edge. Alignment is
   yours: add .center for a simple one-line footer, or build richer footers by
   putting a .container (with your own grid) inside — but pick ONE alignment
   per footer; mixing centered and left-aligned blocks reads as broken. */
.foot, .footer {
  padding: var(--space-7) var(--container-pad);
  color: var(--muted); font-size: var(--text-sm);
}
/* The footer supplies its own inset, so a container inside it must not add a
   second one — all four variants, not just .container, now that they all carry
   padding-inline. */
.footer > .container,
.footer > .container-wide,
.footer > .container-copy,
.footer > .container-narrow,
.footer > .narrow { padding: 0; }
.footer-row { display: flex; justify-content: space-between; align-items: center; gap: var(--space-4); flex-wrap: wrap; }
/* A multi-column footer, which .footer-row's single flex row could not express:
   a build that wanted identity / navigation / legal side by side hand-rolled
   style="display:grid; grid-template-columns:1.2fr 1fr 1fr" in layout.html —
   inline, so no media query could reach it, and at 390px the legal column was
   cut off past the right edge. auto-fit collapses the columns on its own, so
   there is nothing to remember to stack. */
.footer-columns {
  display: grid; gap: var(--space-6) var(--space-8);
  grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
}
.footer a { color: inherit; }
.center { text-align: center; }

/* --- Forms ---------------------------------------------------------------- */
.stack { display: flex; flex-direction: column; gap: var(--space-4); }
label { font-weight: 500; }
/* Same trap, quieter symptom: the :where() block below asks for 400 on a choice
   label (an option, not a field name) and lost to the 0,0,1 rule above, so every
   checkbox option rendered medium-weight. Keeping `label` outside the :where()
   makes this exactly 0,0,1 too, so it wins on source order alone — no
   specificity escalation, still trivially overridable by the project theme. */
label:where(:has(> input[type="checkbox"]), :has(> input[type="radio"]), .choice-label) { font-weight: 400; }
/* Checkboxes and radios are excluded by SELECTOR, not by a later override. Their
   compact sizing lives in a :where() rule below, which has zero specificity by
   design — and a bare `input` here is 0,0,1, so it won every time: an 18px-wide
   checkbox stretched to a 44px-tall box, its glyph sitting 13px below its own
   label, on every site this starter has ever built. The >=44px touch target is
   the LABEL's job, which is what that rule already says.
   The exclusion is wrapped in :where() so this rule stays at 0,0,1 exactly as
   before — a project theme must not find the starter harder to override. */
input:where(:not([type="checkbox"]):not([type="radio"])), textarea, select {
  font: inherit; min-height: var(--control-min); padding: var(--space-2) var(--space-3); border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--bg); color: var(--text);
}
input[type="checkbox"], input[type="radio"] { font: inherit; }
.form-field { display: flex; flex-direction: column; gap: var(--space-1); }
/* Label text and required marker are ONE inline flow, not two flex items. As
   flex items the star was laid out after the whole text block, so any label
   long enough to wrap pushed it away from the words: on a real build's
   "Vilka kaffen och ungefärlig mängd? (minst 2 kg totalt) *" it landed
   mid-phrase on desktop and against the right edge on mobile. */
.field-label { display: inline; max-width: 100%; }
:where(.req, .required-mark, .required-indicator, [data-required-marker]) {
  display: inline; width: auto; margin: 0 0 0 .2em; color: var(--error);
  line-height: 1; align-self: baseline; justify-self: start;
  /* The marker's own margin supplies the gap, so the templates emit no space
     before it — with no break opportunity there, the star cannot be orphaned
     onto a line of its own. Markup that does write a space still works; it
     just keeps the one rare wrap this cannot reach. */
}

/* A wrapped field label is a two-row grid: label text + required marker share
   row one, and the control/hint occupy full-width rows below. This deliberately
   handles both the recommended .field-label wrapper and generated markup such
   as `Name <span class="req">*</span><input …>` — the star can never become a
   full-width flex row between the name and its control. */
:where(
  label:has(> input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"])),
  label:has(> textarea),
  label:has(> select)
) {
  display: grid;
  grid-template-columns: minmax(0, max-content) minmax(0, 1fr);
  align-items: baseline;
  column-gap: .2em;
  row-gap: var(--space-1);
}
:where(
  label:has(> input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"])),
  label:has(> textarea),
  label:has(> select)
) > :where(
  input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),
  textarea, select, .field-label, .field-hint, .field-error, .hint
) {
  grid-column: 1 / -1;
}
:where(
  label:has(> input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"])),
  label:has(> textarea),
  label:has(> select)
) > :where(input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]), textarea, select) {
  width: 100%;
}

/* Checkbox/radio is one atomic choice row. The label is the ≥44px hit target;
   the native control stays compact and aligned with the first text line. */
:where(label:has(> input[type="checkbox"]), label:has(> input[type="radio"]), .choice-label) {
  display: grid;
  grid-template-columns: 1.25rem minmax(0, 1fr);
  align-items: start;
  column-gap: var(--space-2);
  row-gap: var(--space-1);
  min-height: var(--control-min);
  padding-block: var(--space-2);
  font-weight: 400;
  cursor: pointer;
}
:where(label:has(> input[type="checkbox"]), label:has(> input[type="radio"]), .choice-label) > :where(input[type="checkbox"], input[type="radio"]) {
  grid-column: 1;
  grid-row: 1;
  width: 1.125rem;
  height: 1.125rem;
  min-height: 0;
  margin: .2em 0 0;
  padding: 0;
  place-self: start;
  accent-color: var(--accent);
}
:where(label:has(> input[type="checkbox"]), label:has(> input[type="radio"]), .choice-label) > :where(.field-hint, .field-error, .hint) {
  grid-column: 2;
}
.choice-group { display: grid; gap: var(--space-1); }
fieldset { min-inline-size: 0; margin: 0; padding: 0; border: 0; }
legend { margin-bottom: var(--space-1); font-weight: 600; }
.field-hint { color: var(--muted); font-size: var(--text-sm); font-weight: 400; }
.field-error { color: var(--error); font-size: var(--text-sm); font-weight: 600; }

/* --- Buttons --------------------------------------------------------------
   Color/background are protected with !important so a generic link rule
   (e.g. `.my-dark-band a { color: ... }`) can NEVER make button text invisible
   — the exact bug that shipped an unreadable "Boka en tid". You still control
   the look completely, via custom properties (they pass through !important):
     .btn-secondary { --btn-bg: var(--bg-2); --btn-ink: var(--text); }

   Recolour via the PROPERTIES, never by out-specifying the !important: a build
   that wanted its .nav-action cherry/white inside .navlinks fought this rule
   with `.navlinks .nav-action { color: ... }`, lost silently, and paid for it
   as a contrast finding on its first audit pass. The one-liner that works:
     .nav-action { --btn-bg: var(--cherry); --btn-ink: #fff; }                */
.btn, a.btn {
  font: inherit; font-weight: 600; cursor: pointer; display: inline-block;
  background: var(--btn-bg, var(--accent)) !important;
  color: var(--btn-ink, var(--accent-ink)) !important;
  border: 0; text-decoration: none;
  min-height: var(--control-min); padding: var(--space-2) var(--space-4); border-radius: var(--radius-sm);
  align-self: flex-start;
}
.btn:hover { filter: saturate(1.08) brightness(.94); }
.linklike {
  font: inherit; background: none; border: 0; padding: 0;
  color: var(--muted); cursor: pointer; text-decoration: underline;
}
.inline { display: inline; }

@media (max-width: 900px) {
  .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 720px) {
  .split, .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
  .container { padding-left: var(--container-pad); padding-right: var(--container-pad); }
  input, textarea, select { font-size: max(16px, var(--text-base)); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    scroll-behavior: auto !important;
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

/* --- Feedback + utilities -------------------------------------------------- */
.flash {
  background: var(--bg-2); border: 1px solid var(--border); color: var(--text);
  border-radius: var(--radius); padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
}
.muted { color: var(--muted); }
.small { font-size: .85rem; }

.messages { list-style: none; padding: 0; display: flex; flex-direction: column; gap: var(--space-4); }
.messages li { border-bottom: 1px solid var(--border); padding-bottom: var(--space-3); }
.messages p { margin: var(--space-1) 0 0; white-space: pre-wrap; }
.msg-head { display: flex; gap: var(--space-3); align-items: baseline; flex-wrap: wrap; }

/* --- Public data tables (member-facing lists; /admin has its own CSS) ------ */
.table-list { list-style: none; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.table-list li { display: flex; align-items: baseline; gap: var(--space-4); padding: var(--space-3) var(--space-4); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.table-scroll { overflow-x: auto; }
.data-table { width: 100%; border-collapse: collapse; font-size: .85rem; }
.data-table th, .data-table td { text-align: left; padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--border); vertical-align: top; }
.data-table th { font-weight: 600; white-space: nowrap; }
.data-table td { max-width: 28rem; overflow-wrap: anywhere; }
.row-actions { white-space: nowrap; }
.pager { display: flex; gap: var(--space-5); margin-top: var(--space-4); }
.row-detail dt { margin-top: var(--space-3); }
.row-detail dd { margin: var(--space-1) 0 0; overflow-wrap: anywhere; }

/* Public-site tables that must survive a phone. Plans keep making tables the
   bearing component (price lists, spec sheets, schedules), and with no
   sanctioned narrow pattern the columns get squeezed until words break mid-word
   ("Liten servic e" on a real build). Mark the table .stack-table and give each
   td a data-label naming its column; below 480px the rows become stacked cards,
   label above value, wrapping normally. Wider than that it is a plain table. */
@media (max-width: 480px) {
  .stack-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .stack-table, .stack-table tbody, .stack-table tr, .stack-table td { display: block; width: 100%; }
  .stack-table tr { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--space-2) var(--space-3); margin-bottom: var(--space-3); }
  .stack-table td { border: 0; padding: var(--space-1) 0; max-width: none; }
  .stack-table td::before { content: attr(data-label); display: block; font-size: .78rem; font-weight: 600; opacity: .7; }
  .stack-table td:empty { display: none; }
}

/* --- Notification hooks ----------------------------------------------------- */
.hook-list { list-style: none; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-4); }
.hook-list li { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); padding: var(--space-2) var(--space-3); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.hook-target { font-weight: 500; }
.hook-actions { margin-left: auto; display: flex; gap: var(--space-3); }
.badge { font-size: .72rem; padding: .1rem var(--space-2); border-radius: 999px; }
.badge-on { background: #dff5e1; color: #1c6b2e; }
.badge-off { background: #eee; color: #666; }
.hook-add { display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; }
.hook-add input[type=email] { flex: 1; min-width: 14rem; }
