@layer reset, tokens, base, layout, components, utilities;

@layer reset {
/*
 * Modern minimal reset.
 *
 * Scope: only the browser defaults that get in the way of every project
 * (box-sizing, margins, media sizing, form control inheritance). It does
 * NOT set colours, fonts or spacing scale values — those are tokens
 * (layers/tokens.css) and element defaults (layers/base.css). Keeping the
 * reset free of tokens means it never has to change when the design
 * changes, and it can be read/audited on its own.
 *
 * Loaded first (`@layer reset`), so anything here is the lowest-priority
 * layer: base, layout, components and utilities all win over it.
 */

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

* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  min-height: 100svh;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img,
picture,
svg,
video,
canvas {
  display: block;
  max-width: 100%;
}

input,
button,
textarea,
select {
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
}

button {
  background: none;
  border: none;
  cursor: pointer;
}

p,
h1,
h2,
h3,
h4,
h5,
h6 {
  overflow-wrap: break-word;
}

ol,
ul {
  list-style: none;
}

table {
  border-collapse: collapse;
  border-spacing: 0;
}

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

fieldset {
  border: none;
}

/* Respect the OS-level reduced-motion preference at the reset level too,
   so that any animation added later (including by project CSS outside
   this package) is off by default unless the browser says motion is fine. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

}

@layer tokens {
/*
 * Design tokens — @layer tokens.
 *
 * Consolidates the design system that mietwerk (canonical class/token
 * names), zeitwerk and gastwerk each forked and restyled with their own
 * accent colour (see docs/css-architecture.md for the full inventory of
 * what came from where and every rename). Same leitmotif in all three: a
 * calm, flat look — hairlines instead of heavy borders, restrained
 * shadows, ONE accent colour for everything interactive, a neutral grey
 * scale, a spacing scale, a system font stack (no webfonts), WCAG AA
 * contrast.
 *
 * THEME MODEL — replaces the "write the dark block twice" pattern every
 * fork used (once under `@media (prefers-color-scheme: dark) { :root:not(
 * [data-theme="light"]) {...} }` for "no explicit choice saved", once under
 * `:root[data-theme="dark"] {...}` for an explicit override — each fork's
 * base.css header explains this was intentional, since plain CSS cannot
 * reuse a custom-property block across an `@media` boundary). `light-dark()`
 * removes the duplication: every colour token is written ONCE as
 * `light-dark(<light value>, <dark value>)`, and only `color-scheme` still
 * needs the three-way split below. The three-state UX (system/light/dark —
 * kept from zeitwerk, see its base.css/docs/ui.md, not just a light/dark
 * toggle) stays a JS-layer concern: "system" is the absence of a
 * `data-theme` attribute on `<html>`, "light"/"dark" set it explicitly.
 * `light-dark()` only needs the *resolved* scheme, which `color-scheme`
 * communicates.
 *
 * MULTI-LAYER VALUES (box-shadow) — light-dark()'s two arguments split on
 * every top-level comma, so a value that is itself a comma-separated list
 * (a two-layer box-shadow) can't be inlined directly — it would parse as
 * extra light-dark() arguments. Fix: give each theme's shadow list its own
 * custom property first (the comma is now safely inside that property's
 * own value, not inside light-dark()'s argument list), then reference both
 * with `light-dark(var(--x-light), var(--x-dark))` — a single var() token
 * has no top-level comma, so this always parses as exactly two arguments.
 * See --shadow-sm/-md/-lg below.
 *
 * BRAND / ACCENT — `--color-primary` and friends are designed to be
 * overridden by a generated brand.css (later task, proposal section 4.2):
 * a project sets `GRUNDWERK_UI["ACCENT"] = {"light": ..., "dark": ...}` and
 * brand.css only has to emit `--brand-accent-light`/`--brand-accent-dark`.
 * Everything else — hover, tint, tint border, focus ring — is DERIVED here
 * from `--brand-accent` with `color-mix()`, so a project never hand-picks
 * a hover/tint hex itself (previously each fork's header comment had to
 * document its own hand-picked hover/tint value per accent colour). The
 * values below are the neutral fallback (mietwerk's green accent) used
 * when no brand.css is loaded yet.
 *
 * RENAMES vs mietwerk (the canonical fork — full table with reasons in
 * docs/css-architecture.md, "Tokens: renames vs mietwerk"):
 *
 *   --color-primary-soft        -> --color-primary-tint
 *   --color-primary-soft-border -> --color-primary-tint-border
 *   --transition (one flat value) -> --duration-base + --ease-standard
 *                                     (--transition kept as a computed alias)
 *
 * New tokens no fork had (previously hardcoded inline, repeated per file):
 * --shadow-lg, --z-sticky/-drawer/-scrim/-overlay, --target-size-min,
 * --sidebar-width, --brand-accent(-light/-dark).
 *
 * CONTRAST — computed with a small relative-luminance script (WCAG AA,
 * scratchpad-only, not part of the repo — same approach each fork's header
 * documents) against the DEFAULT fallback accent (mietwerk's green,
 * #0d7047 light / #3fcf8e dark), since that is what ships without a
 * brand.css. A project's own brand.css must clear the same bar — that is
 * what the proposal's brand-contrast system check (section 4.2) is for.
 *
 *   Light: text #15181d/bg #f7f8f9 = 16.73:1; muted #616a76/bg = 5.15:1;
 *   primary(link) #0d7047/bg = 5.77:1, /surface #ffffff = 6.13:1;
 *   primary-contrast #ffffff/primary (button text) = 6.13:1; hover
 *   ~#0b5f3c (color-mix 85% primary + black)/bg = 7.27:1; primary text on
 *   14%-tint surface = 4.99:1; danger #b3261e/bg = 6.15:1; danger-contrast/
 *   danger = 6.54:1; badge text/bg — success 6.21:1, warning 6.79:1,
 *   danger 5.61:1, info 7.13:1, muted 5.99:1; focus ring (primary) vs
 *   surface, UI >=3:1 -> 6.13:1.
 *
 *   Dark: text #e7e9ec/bg #0e1013 = 15.66:1; muted #99a1ad/bg = 7.31:1;
 *   primary(link) #3fcf8e/bg = 9.55:1, /surface #16191e = 8.83:1;
 *   primary-contrast #052517/primary = 8.2:1; hover ~#5cd69f (color-mix
 *   85% primary + white)/bg = 10.5:1; primary text on 18%-tint surface =
 *   6.18:1; danger #ff8a80/bg = 8.35:1; danger-contrast/danger = 7.73:1;
 *   badge text/bg — success 8.43:1, warning 8.16:1, danger 6.2:1, info
 *   6.96:1, muted 7.94:1; focus ring (primary) vs surface, UI >=3:1 ->
 *   8.83:1.
 *
 *   All pairs comfortably clear 4.5:1 (text AA) / 3:1 (UI AA). The
 *   color-mix()-derived hover shade was approximated in sRGB for this
 *   check (the browser mixes `in oklch`, which trends the same direction —
 *   darker in light mode, lighter in dark mode — so this is a lower bound,
 *   not an exact figure).
 *
 * RULE: everything outside this file uses tokens only, never a literal
 * hex/rgb/hsl/oklch colour — see ui/tests/test_css_token_audit.py.
 */

:root {
  color-scheme: light dark;
}

:root[data-theme="light"] {
  color-scheme: light;
}

:root[data-theme="dark"] {
  color-scheme: dark;
}

:root {
  /* ------------------------------------------------------------ Brand
     Overridden wholesale by a generated brand.css (proposal 4.2), which
     only needs to set these two. Everything "accent-flavoured" below
     derives from --brand-accent. */
  --brand-accent-light: #0d7047;
  --brand-accent-dark: #3fcf8e;
  --brand-accent: light-dark(var(--brand-accent-light), var(--brand-accent-dark));

  /* -------------------------------------------------------- Neutral scale
     Cool grey, deliberately low-contrast against itself: hierarchy comes
     from spacing and typography, not border weight. */
  --color-bg: light-dark(#f7f8f9, #0e1013);
  --color-surface: light-dark(#ffffff, #16191e);
  --color-surface-alt: light-dark(#f3f4f6, #1c2026);
  --color-surface-hover: light-dark(#eceef1, #23272e);
  --color-border: light-dark(#e4e6ea, #282d34);
  --color-border-strong: light-dark(#c4c9d0, #3c434c);
  /* v0.1.2: neither --color-border nor --color-border-strong clears WCAG
     1.4.11 (non-text contrast, >=3:1) against --color-surface -
     --color-border-strong measures ~1.67:1 in light mode and ~1.76:1 in
     dark (both computed the same relative-luminance way as this file's
     own CONTRAST note below) - both were designed as *hairlines*
     (deliberately low-contrast, see this file's own header comment on
     the neutral scale), fine for a decorative divider but not for a
     control whose boundary is its only visible affordance
     (`.button-secondary` on a white card, found barely visible on
     staging). This token is for exactly that case: ~3.25:1 in light mode,
     ~3.65:1 in dark, against --color-surface. */
  --color-border-control: light-dark(#8a8f99, #6b7280);
  --color-text: light-dark(#15181d, #e7e9ec);
  --color-muted: light-dark(#616a76, #99a1ad);

  /* Backdrop behind an overlay/mobile drawer — always dark, independent of
     theme (a scrim over content reads dark in both themes; zeitwerk
     tokenized this, mietwerk/gastwerk hardcoded it inline each time). */
  --color-scrim: rgba(15, 17, 21, 0.45);

  /* Solid, theme-invariant count pill (badge.css's `.badge-count`, v0.1.1
     — a small unread-notification-style counter, not a status badge).
     Independent of `--color-danger`/`-contrast` on purpose, even though
     it starts from the same pair: a project may re-brand its danger
     colour without wanting its notification counter to move with it, and
     a solid pill's contrast never depends on the *page* background the
     way every other token above does (same reasoning as `--color-scrim`).
     Ported from zeitwerk's own project-local `--zw-badge-count-bg/-fg`
     (docs/css-architecture.md's inventory flagged this as a "candidate
     add-on, needs its own token pair" - this is that pair). */
  --color-count-bg: #b3261e;
  --color-count-fg: #ffffff;

  /* ---------------------------------------------------------------- Accent
     The one accent colour. Everything interactive (links, primary
     buttons, active nav, focus ring) uses it — nothing else does. */
  --color-primary: var(--brand-accent);
  --color-primary-hover: color-mix(in oklch, var(--brand-accent) 85%, light-dark(black, white));
  --color-primary-contrast: light-dark(#ffffff, #052517);
  --color-primary-tint: color-mix(in oklch, var(--brand-accent) 14%, transparent);
  --color-primary-tint-border: color-mix(in oklch, var(--brand-accent) 28%, transparent);

  /* --------------------------------------------------------------- Danger */
  --color-danger: light-dark(#b3261e, #ff8a80);
  --color-danger-hover: light-dark(#8f1e18, #ffa9a1);
  --color-danger-contrast: light-dark(#ffffff, #370605);
  --color-success: light-dark(#14663a, #6fe0a0);

  /* --------------------------------------------- Status triples (badges,
     messages, notices): bg (tint) / text / border per status. */
  --color-success-bg: light-dark(#e8f4ec, rgba(63, 207, 142, 0.13));
  --color-success-border: light-dark(#bcdfc9, rgba(63, 207, 142, 0.30));
  --color-warning-text: light-dark(#7a4a06, #f2c46b);
  --color-warning-bg: light-dark(#fdf3e0, rgba(242, 196, 107, 0.13));
  --color-warning-border: light-dark(#f0d9ab, rgba(242, 196, 107, 0.30));
  --color-danger-bg: light-dark(#fbeae8, rgba(255, 138, 128, 0.13));
  --color-danger-border: light-dark(#f3c6c1, rgba(255, 138, 128, 0.30));
  --color-info-text: light-dark(#1d4f8f, #8fbcf5);
  --color-info-bg: light-dark(#e9f0fa, rgba(143, 188, 245, 0.13));
  --color-info-border: light-dark(#c3d6ee, rgba(143, 188, 245, 0.30));
  --color-muted-bg: light-dark(#f0f1f3, rgba(255, 255, 255, 0.05));
  --color-muted-text: light-dark(#545c67, #b3bac4);
  --color-muted-border: light-dark(#dcdfe4, rgba(255, 255, 255, 0.12));

  /* ---------------------------------------------------------------- Focus
     A thin SOLID ring (box-shadow). The forks used the accent at 30%
     opacity, which only reaches ~1.7:1 against the light surface and fails
     WCAG 1.4.11 (non-text contrast >= 3:1) for every accent. A solid 2px
     ring in the accent colour inherits the accent's contrast (checked by
     grundwerk_ui.E102). */
  --focus-ring-color: var(--brand-accent);
  --focus-ring: 0 0 0 2px var(--focus-ring-color);
  --focus-ring-danger-color: var(--color-danger);
  --focus-ring-danger: 0 0 0 2px var(--focus-ring-danger-color);

  /* ---------------------------------------------------------------- Space */
  --space-xs: 0.25rem;
  --space-sm: 0.5rem;
  --space-md: 1rem;
  --space-lg: 1.5rem;
  --space-xl: 2.5rem;
  --space: 1rem;

  /* --------------------------------------------------------------- Radius */
  --radius-sm: 5px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-pill: 999px;

  /* --------------------------------------------------------------- Shadow
     Deliberately flat: depth comes from the hairline border, shadow only
     for things that genuinely float above the page (dropdown, dialog,
     lightbox). See the file header for why each is split into a
     light/dark custom property before being passed to light-dark(). */
  --shadow-sm-light: 0 1px 2px rgba(16, 20, 26, 0.05);
  --shadow-sm-dark: 0 1px 2px rgba(0, 0, 0, 0.35);
  --shadow-sm: light-dark(var(--shadow-sm-light), var(--shadow-sm-dark));

  --shadow-md-light: 0 8px 24px -6px rgba(16, 20, 26, 0.16), 0 2px 6px rgba(16, 20, 26, 0.06);
  --shadow-md-dark: 0 10px 28px -8px rgba(0, 0, 0, 0.65), 0 2px 6px rgba(0, 0, 0, 0.4);
  --shadow-md: light-dark(var(--shadow-md-light), var(--shadow-md-dark));

  --shadow-lg-light: 0 22px 44px -16px rgba(16, 20, 26, 0.22);
  --shadow-lg-dark: 0 24px 48px -16px rgba(0, 0, 0, 0.55);
  --shadow-lg: light-dark(var(--shadow-lg-light), var(--shadow-lg-dark));

  /* ---------------------------------------------------------- Typography
     System font stack only — no webfont download (CSP, privacy, no flash
     of invisible text). */
  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  --font-size-xs: 0.75rem;
  --font-size-sm: 0.85rem;
  --font-size-base: 0.95rem;
  --font-size-lg: 1.1rem;
  --font-size-xl: 1.5rem;
  --font-size-2xl: 2rem;

  /* ---------------------------------------------------------------- Motion
     Split into duration + easing (each fork only had one flat
     `--transition: 130ms ease`) — `--transition` stays as a computed
     alias so existing component patterns (`transition: background-color
     var(--transition)`) keep working unchanged. */
  --duration-fast: 100ms;
  --duration-base: 130ms;
  --duration-slow: 220ms;
  --ease-standard: ease;
  --transition: var(--duration-base) var(--ease-standard);

  /* --------------------------------------------------------------- Z-index
     Previously hardcoded per fork (30 for sticky headers, 40 for the
     drawer scrim, 50 for the drawer itself, 100 for the lightbox) — named
     here so a new overlay component picks the right layer instead of
     guessing a number. */
  --z-sticky: 30;
  --z-scrim: 40;
  --z-drawer: 50;
  --z-overlay: 100;

  /* --------------------------------------------------------- Target size
     WCAG 2.5.8 (AA) — every interactive control's hit area (button, icon
     button, checkbox/radio, link acting as a control) should be at least
     this in both dimensions. */
  --target-size-min: 24px;

  /* --------------------------------------------------------------- Layout */
  --content-width: 78rem;
  --sidebar-width: 15.5rem;
}

}

@layer base {
/*
 * Element defaults — @layer base (see ../grundwerk.css for the layer
 * order; this file is imported into that layer, it does not wrap its
 * own rules in `@layer base { ... }`).
 *
 * Scope: bare-element styling only (body, headings, links, lists,
 * tables, form controls) so that a page with no component classes at
 * all still looks like grundwerk-ui, not like the browser default.
 * Every colour is a token from layers/tokens.css (see that file's
 * header and docs/css-architecture.md, "components use tokens only" —
 * the rule applies here too, and tests/test_css_token_audit.py checks
 * it for this file exactly like every other).
 *
 * Ported from mietwerk's base.css (canonical) with two additions no
 * fork had: `accent-color` on native checkboxes/radios/range, and a
 * `prefers-reduced-motion` guard around `@view-transition` (proposal
 * 4.3: "Page transitions come from `@view-transition { navigation:
 * auto; }` in base.css, wrapped in prefers-reduced-motion:
 * no-preference. Browsers without support (Firefox) switch pages
 * without animation.").
 */

body {
  font-family: var(--font-sans);
  font-size: var(--font-size-base);
  color: var(--color-text);
  background: var(--color-bg);
  line-height: 1.55;
  text-rendering: optimizeLegibility;
}

h1,
h2,
h3,
h4 {
  line-height: 1.25;
  letter-spacing: -0.011em;
  font-weight: 600;
}

h1 {
  font-size: var(--font-size-2xl);
}

h2 {
  font-size: var(--font-size-xl);
}

h3 {
  font-size: var(--font-size-lg);
}

h4 {
  font-size: var(--font-size-base);
}

small {
  font-size: var(--font-size-sm);
}

code,
kbd,
pre,
samp {
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
}

a {
  color: var(--color-primary);
  text-decoration: none;
}

a:hover {
  color: var(--color-primary-hover);
  text-decoration: underline;
}

/* WCAG 1.4.1 (use of colour) / axe's "link-in-text-block": a link sitting
   inside a paragraph of running text must be distinguishable from its
   surrounding text by more than colour alone (found by axe on the
   allauth login page's "please sign up first" sentence). Scoped to
   `:where(p, li, dd, td)` - prose/table-cell contexts only - and
   `:not([class])` so a *styled* link (`.button`/`.badge`/`.tab`/a
   `<c-gw.sidebar-link>`, all of which carry a class and are never inside
   running text anyway) is never accidentally underlined. `:where()`
   keeps this at zero specificity, same as `a`/`a:hover` above, so a more
   specific component rule still wins without needing `!important`. */
:where(p, li, dd, td) a:not([class]) {
  text-decoration: underline;
  text-underline-offset: 2px;
}

hr {
  border: none;
  border-top: 1px solid var(--color-border);
}

/* ---------------------------------------------------------------- Tables
   A bare <table> already looks right: bordered/rounded container, muted
   uppercase head, hairline rows, subtle zebra + hover. No wrapper class
   needed (mietwerk convention, kept). Sortable headers, empty-state
   styling and responsive stacking are component-layer concerns (table
   component, later task) - this is only the element-level default. */
table {
  width: 100%;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

th,
td {
  padding: var(--space-sm) var(--space-md);
  text-align: left;
  border-bottom: 1px solid var(--color-border);
}

thead th {
  background: var(--color-surface-alt);
  color: var(--color-muted);
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

tbody tr:nth-child(even) {
  background: var(--color-surface-alt);
}

tbody tr:hover {
  background: var(--color-surface-hover);
}

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

/* --------------------------------------------------------------- Forms
   Text-like inputs share one rule via an attribute-selector list, so any
   matching input type is styled without a class (mietwerk convention). */
label {
  display: inline-block;
  font-weight: 600;
  font-size: var(--font-size-sm);
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="search"],
input[type="tel"],
input[type="url"],
input[type="number"],
input[type="date"],
input[type="datetime-local"],
input[type="time"],
input[type="file"],
select,
textarea {
  width: 100%;
  max-width: 32rem;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
}

textarea {
  min-height: 6rem;
}

input::placeholder,
textarea::placeholder {
  color: var(--color-muted);
  opacity: 0.8;
}

input:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--focus-ring-color);
  box-shadow: var(--focus-ring);
}

/* `::file-selector-button` is its own UA-shadow control, not styled by
   any of the rules above - left at the browser default (a plain grey
   OS-native button, clashing hard with every other control on the page)
   until now. Same look as `.button-secondary` (layers/components/
   button.css) - literal declarations, not a class, since a pseudo-
   element can't be given one. */
input[type="file"]::file-selector-button {
  margin-inline-end: var(--space-sm);
  padding: 0.45rem 0.85rem;
  border: 1px solid var(--color-border-control);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  transition:
    background-color var(--transition),
    border-color var(--transition),
    color var(--transition);
}

input[type="file"]::file-selector-button:hover {
  background: var(--color-surface-alt);
  border-color: var(--color-border-control);
}

/* Native checkbox/radio/range use accent-color instead of custom-built
   replacements - no fork had this; it is the platform-native way to
   theme these controls and keeps them fully accessible for free. */
input[type="checkbox"],
input[type="radio"],
input[type="range"] {
  accent-color: var(--color-primary);
}

fieldset {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-md);
}

legend {
  padding: 0 var(--space-xs);
  font-weight: 600;
}

/* -------------------------------------------------------- Focus-visible
   One universal rule instead of the enumerated per-selector list every
   fork used (mietwerk/zeitwerk's list had to be updated by hand whenever
   a new interactive element was added, and mietwerk's own list still
   references a `.nav-link` class that no longer exists anywhere in its
   CSS - a dead selector from that drift). `:focus-visible` already only
   fires for keyboard/programmatic focus, so a blanket rule does not show
   the ring on a mouse click. */
:focus-visible {
  outline: 2px solid var(--focus-ring-color);
  outline-offset: 2px;
}

::selection {
  background: var(--color-primary-tint);
  color: var(--color-text);
}

/* ----------------------------------------------------------- Motion
   Reduced-motion is handled twice, deliberately: layers/reset.css turns
   off *all* animations/transitions unconditionally for that preference
   (a blanket safety net), and the @view-transition rule below only ever
   activates in the opposite media query, so a page transition can never
   appear for a user who asked for less motion. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}

/* ------------------------------------------------------------------ Icons
   `{% icon %}` (templatetags/grundwerk_ui.py) always renders its <svg>
   with this class. Without an explicit size, a bare inline <svg> falls
   back to the UA default intrinsic size (300x150 CSS px) - invisible
   inside a size-only wrapper that sets no width/height of its own
   (.side-icon/.alert-icon/.empty-state-icon/.icon-button, all
   components-layer - see each file's own `> svg` rule sizing it to
   100% of the wrapper instead, which wins regardless of specificity:
   components > base in layer order, see ../grundwerk.css), but a
   glaring 150px-tall icon anywhere the tag is used directly inline with
   text (e.g. a sidebar link's `{% icon "dashboard" %} Dashboard`, no
   wrapper at all) - found on staging (v0.1.2). `flex: none` keeps the
   icon from being compressed/stretched by a flex ancestor (`.side-link`,
   `.cluster`, …) the way a plain inline element would be. */
.icon {
  width: 1em;
  height: 1em;
  flex: none;
}

}

@layer layout {
/*
 * Layout — @layer layout (see ../grundwerk.css for the layer order; this
 * file is imported into that layer, it does not wrap its own rules in
 * `@layer layout { ... }`).
 *
 * Two things live here:
 *
 *   1. The app shell (`.app-shell` > `.sidebar` + `.app-main` >
 *      `.topbar` + `<main>`), ported from mietwerk's base.css - the same
 *      structure zeitwerk and gastwerk forked unchanged. Mobile collapse
 *      does NOT use the CSS-only checkbox hack the three forks used
 *      (`#nav-toggle` + a `<label>`): `static/grundwerk_ui/js/nav.js`
 *      deliberately drops that in favour of a real `<button>`, because a
 *      checkbox can't express `aria-expanded`, doesn't close on Escape,
 *      and doesn't return focus to the toggle (see that file's header
 *      comment). This file wires to nav.js's actual contract instead:
 *
 *        <button data-gw-nav-toggle aria-expanded="false" aria-controls="ID">
 *        <aside class="sidebar" data-gw-nav-panel id="ID"> ... </aside>
 *        <div data-gw-nav-scrim></div>   (optional backdrop)
 *
 *      nav.js only ever touches attributes (`data-gw-nav-open` on the
 *      panel, `aria-expanded` on the toggle) - this file is the only
 *      place those attributes turn into a visual open/closed state. The
 *      *visual* styling of the toggle button itself is a component
 *      concern (nav component, later task) - this file only handles the
 *      structural collapse (position, transform, z-index, scrim).
 *
 *   2. Generic layout primitives (Every Layout-style) no fork had as
 *      reusable classes - `.stack`, `.cluster`, `.grid-auto`, `.switcher`.
 *      Each fork rebuilt the same handful of flex/grid patterns ad hoc
 *      per component (`.stat-row`, `.dashboard-grid`, `.nav-links`,
 *      `.filter-bar`, ...) instead of naming the underlying primitive.
 *      Components (later tasks) should reach for one of these before
 *      writing a bespoke `display: flex` rule.
 *
 * Every dimension here comes from a token (--space-*, --sidebar-width,
 * --content-width, --z-*) - see docs/css-architecture.md, "components use
 * tokens only": the rule applies to every layer above tokens, not just
 * the components layer.
 */

/* ------------------------------------------------------------ App shell */
.app-shell {
  display: flex;
  min-height: 100vh;
  align-items: stretch;
}

.sidebar {
  position: sticky;
  top: 0;
  align-self: flex-start;
  height: 100vh;
  flex: 0 0 var(--sidebar-width);
  display: flex;
  flex-direction: column;
  background: var(--color-surface);
  border-right: 1px solid var(--color-border);
}

[data-gw-nav-scrim] {
  display: none;
}

.app-main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.topbar {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-lg);
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
}

.app-main > main,
.app-main > .site-footer {
  width: 100%;
}

.app-main > main {
  flex: 1 0 auto;
  min-width: 0;
}

/* ------------------------------------------------------- Page container */
.page {
  max-width: var(--content-width);
  margin-inline: auto;
  padding: var(--space-xl) var(--space-lg);
  container-type: inline-size;
  container-name: page;
  min-width: 0;
}

/* Vertical rhythm between stacked page-level blocks (v0.1.6 fix, gastwerk
   review item 3) - a lobotomized-owl flow rule ("every element that has
   a preceding sibling gets a top margin"), the same pattern Every
   Layout's `.stack` uses. Before this, `{% block page_header %}` +
   `{% block content %}`'s own top-level elements (two `<c-gw.card>`s in
   a row, a `.tabs` bar immediately followed by its content, a
   `.table-scroll` right under a card, `.pagination`, `.back-link`, a
   `.detail-grid`, `.alert`, `.toolbar`/`.filter-bar`, a bare `<form>`
   from `<c-gw.form>`, `.gallery`, `.timeline`, ...) had no margin of
   their own - none of them know what will or won't follow them on the
   page - so stacked cards touched, tab content sat flush under the tab
   bar, and a table butted against the card above it (confirmed on
   gastwerk's staging deployment). Scoped to `.page`'s own *direct*
   children only, so it never fights a component's *internal* spacing
   (a card's header/body/footer regions use their own negative-margin
   convention, see card.css; a `<c-gw.form>`'s fields use `.form-field`'s
   own margin-bottom, see form-field.css) - see docs/components.md,
   "Spacing / flow". `.page-header`'s own `margin-bottom` (previously in
   page-header.css) is removed as part of this same fix: it was always
   `.page`'s first child, so this rule now owns that gap too - keeping
   both would double it for that one case. */
.page > * + * {
  margin-block-start: var(--space-lg);
}

/* ------------------------------------------------------------- Footers
   `.site-footer` on every layout shell's <footer> (base_app.html/
   base_public.html/base_auth.html) - v0.1.2 fix: this class already had a
   sizing rule above (`.app-main > .site-footer { width: 100% }`), but no
   base template actually rendered a bare <footer> with the class, so the
   rule never matched anything and an app-shell footer with real content
   could render unconstrained (found on staging rendering below/beside the
   sidebar instead of spanning `.app-main` in a full-page screenshot).
   `overflow-wrap` keeps a long, unbreakable footer string (a URL, a
   version string with no spaces) from pushing the footer wider than its
   container instead of wrapping. */
.site-footer {
  padding: var(--space-md) var(--space-lg);
  color: var(--color-muted);
  font-size: var(--font-size-sm);
  overflow-wrap: break-word;
}

.site-footer:empty {
  display: none;
}

.app-main > .site-footer {
  border-top: 1px solid var(--color-border);
  flex: 0 0 auto;
}

.site-footer a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Auth shell's footer (base_auth.html: class="site-footer auth-footer")
   - centred under `.login-card`/`.auth-shell`, its own links (the
   documented "Impressum/Datenschutz"-style `auth_footer` block, see
   base_auth.html's header comment) laid out with breathing room between
   them via `gap` rather than relying on whatever whitespace/separator
   character the project's own block content happens to contain. */
.auth-footer {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: var(--space-sm);
  text-align: center;
}

/* ------------------------------------------------ Mobile drawer toggle
   Below this width the sidebar becomes an off-canvas drawer, closed by
   default, shown by nav.js setting `data-gw-nav-open` on it (contract in
   the file header above). 48rem matches the breakpoint every fork
   already used. */
@media (max-width: 48rem) {
  .page {
    padding: var(--space-lg) var(--space-md);
  }

  .sidebar[data-gw-nav-panel] {
    position: fixed;
    top: 0;
    left: 0;
    z-index: var(--z-drawer);
    width: var(--sidebar-width);
    max-width: 85vw;
    overflow: hidden;
    overflow-y: auto;
    transform: translateX(-100%);
    transition: transform var(--transition);
    box-shadow: var(--shadow-md);
  }

  .sidebar[data-gw-nav-panel][data-gw-nav-open] {
    transform: translateX(0);
  }

  [data-gw-nav-scrim] {
    display: block;
    position: fixed;
    inset: 0;
    z-index: var(--z-scrim);
    background: var(--color-scrim);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--transition);
  }

  /* :has() lets the scrim react to the panel's state without nav.js
     touching a third element - it only ever sets attributes on the
     toggle and the panel (see the contract in the file header). */
  .app-shell:has([data-gw-nav-panel][data-gw-nav-open]) [data-gw-nav-scrim] {
    opacity: 1;
    pointer-events: auto;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sidebar[data-gw-nav-panel] {
    transition: none;
  }
}

/* ------------------------------------------------------ Layout primitives
   Every Layout-style building blocks - reach for these before writing an
   ad hoc `display: flex`/`display: grid` rule in a component file. */
.stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.stack--sm {
  gap: var(--space-sm);
}

.stack--lg {
  gap: var(--space-lg);
}

.cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
}

.grid-auto {
  display: grid;
  gap: var(--space-md);
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
}

/* A switcher lays its children side by side until they no longer fit at
   --switcher-threshold each, then stacks them - the same responsive
   pattern every fork hand-rolled once per component (.stat-row's
   flex-wrap, .dashboard-grid's auto-fit grid, .nav-links' mobile
   column-stack) without naming it. */
.switcher {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
}

.switcher > * {
  flex: 1 1 var(--switcher-threshold, 24rem);
}

@container page (min-width: 40rem) {
  .grid-auto--2up {
    grid-template-columns: repeat(2, 1fr);
  }
}

}

@layer components {
/*
 * Component layer index.
 *
 * grundwerk.css imports this file into `@layer components`. Every import
 * below is intentionally WITHOUT its own `layer()` — an unlayered
 * `@import` nested inside a layered one is assigned to the enclosing
 * layer (see grundwerk.css's header comment and
 * docs/css-architecture.md), so each component file listed here lands in
 * `components` without repeating the layer name.
 *
 * Rule for every file imported here: components use tokens only (see
 * docs/css-architecture.md, "components use tokens only") — no literal
 * colours, no magic spacing numbers.
 */

/* Actions */
/*
 * Button — @layer components (imported by components/_index.css, itself
 * imported into `components` by ../../grundwerk.css; see that file's
 * header for why this file does not wrap its rules in `@layer { ... }`).
 *
 * Ported from mietwerk's base.css (canonical; byte-identical in zeitwerk
 * and gastwerk per docs/css-architecture.md's inventory table). Three
 * weights: primary (filled, accent — one per view), secondary (surface +
 * hairline), danger (filled, red). `.button`/`button`/`input[type=submit]`
 * all share the primary look so a plain `<button>`/submit input needs no
 * class — mietwerk convention, kept.
 *
 * `<c-gw.button>` (see cotton/gw/button.html) adds two things no fork
 * had as CSS: a `.button-sm` size (was already a documented target in the
 * css-architecture.md inventory row, `.button-sm`) and a `.button-ghost`
 * variant (proposal 4.3 lists "ghost" alongside primary/secondary/danger;
 * no fork built it — modelled here on `.button-secondary` with a
 * transparent resting background, since "ghost" means exactly that: same
 * shape and weight as secondary, no surface fill until interacted with).
 * `.button-loading` (aria-busy state) is likewise new — see the cotton
 * template's docstring for the markup contract.
 *
 * `.button-secondary`/`.button-danger`/`.button-ghost` are in the base
 * box-model selector below (v0.1.6 fix, gastwerk review item 2) so they
 * still work as **standalone** classes on a hand-written
 * `<a class="button-secondary">` with no `.button` alongside it —
 * mietwerk/zeitwerk/gastwerk's own convention before this package
 * existed, ~175 call sites in gastwerk alone. 0.1.3 only fixed this
 * inside `<c-gw.button>` itself (which always emits the base `.button`
 * class too, so it never hit the gap); a project's own hand-written
 * markup, and `docs/css-architecture.md`'s "Identical in zeitwerk,
 * gastwerk"/`docs/components.md`'s "Unchanged class names" claims, were
 * both wrong until this fix. `button button-secondary` (the redundant,
 * belt-and-braces form) keeps working identically either way — the
 * selector list below matches on either class alone or the pair.
 */

.button,
.button-secondary,
.button-danger,
.button-ghost,
button,
input[type="submit"] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  min-height: var(--target-size-min);
  padding: 0.45rem 0.85rem;
  background: var(--color-primary);
  color: var(--color-primary-contrast);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  text-decoration: none;
  cursor: pointer;
  font-family: inherit;
  font-size: var(--font-size-sm);
  font-weight: 600;
  line-height: 1.4;
  white-space: nowrap;
  transition:
    background-color var(--transition),
    border-color var(--transition),
    color var(--transition);
}

.button:hover,
button:hover,
input[type="submit"]:hover {
  background: var(--color-primary-hover);
  /* Explicit, or `a:hover { color }` (higher specificity than
     `.button { color }`) wins on an <a class="button"> link and the text
     goes invisible against the new background — same for
     text-decoration, since `a:hover { text-decoration: underline }`. */
  color: var(--color-primary-contrast);
  text-decoration: none;
  opacity: 1;
}

/* `border-color`/`box-shadow` use `--color-border-control`, not
   `--color-border` (v0.1.2 fix) - this is a control's *only* visible
   edge on a plain surface (unlike a hairline divider between two
   already-adjacent regions), so its boundary needs to actually clear
   WCAG 1.4.11's 3:1 non-text contrast minimum against `--color-surface`
   - found failing that (and reading as barely visible) on a white card
   on staging; see tokens.css's own comment on this token for the
   contrast figures. The shadow is a second, redundant visual cue (not
   relied on for the contrast figure itself), same spirit as
   `.login-card`'s own `--shadow-sm`. */
.button-secondary {
  background: var(--color-surface);
  color: var(--color-text);
  border-color: var(--color-border-control);
  box-shadow: var(--shadow-sm);
}

.button-secondary:hover {
  background: var(--color-surface-alt);
  border-color: var(--color-border-control);
  color: var(--color-text);
  text-decoration: none;
}

/* New variant (proposal 4.3), modelled on .button-secondary: same shape,
   no surface fill until hover/focus - for a low-emphasis action next to a
   primary/secondary pair. */
.button-ghost {
  background: transparent;
  color: var(--color-text);
  border-color: transparent;
}

.button-ghost:hover {
  background: var(--color-surface-alt);
  border-color: var(--color-border);
  color: var(--color-text);
  text-decoration: none;
}

.button-danger {
  background: var(--color-danger);
  color: var(--color-danger-contrast);
  border-color: transparent;
}

.button-danger:hover {
  background: var(--color-danger-hover);
  color: var(--color-danger-contrast);
  text-decoration: none;
  opacity: 1;
}

/* Also used standalone (`class="button-secondary button-sm"` etc.) - not
   just on `.button`. */
.button-sm {
  min-height: 0;
  padding: 0.2rem 0.6rem;
  font-size: var(--font-size-xs);
}

.button:disabled,
button:disabled,
input[type="submit"]:disabled,
.button[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
}

/* aria-busy state (new): dims the label and shows a small spinner instead
   of the leading icon slot, via a ::before border-spin - no extra markup,
   no JS. prefers-reduced-motion turns the spin into a static ring
   (layers/reset.css already zeroes animation-duration globally for that
   preference; the explicit rule below is a second guard specific to this
   ::before, in case a project overrides the reset for buttons only). */
.button-loading {
  position: relative;
  color: transparent !important;
  pointer-events: none;
}

.button-loading::before {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  color: inherit;
  animation: gw-button-spin var(--duration-slow) linear infinite;
}

.button-loading.button,
button.button-loading,
input.button-loading[type="submit"] {
  color: var(--color-primary-contrast);
}

.button-loading.button::before,
button.button-loading::before,
input.button-loading[type="submit"]::before {
  color: var(--color-primary-contrast);
}

.button-loading.button-secondary::before,
.button-loading.button-ghost::before {
  color: var(--color-text);
}

.button-loading.button-danger::before {
  color: var(--color-danger-contrast);
}

@keyframes gw-button-spin {
  to {
    transform: rotate(360deg);
  }
}

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

/*
 * Icon button — @layer components.
 *
 * New component file: no fork had a *named* small-square icon-only button
 * class, but the pattern already existed three times over with an
 * inline rule each time (mietwerk/zeitwerk/gastwerk's `.nav-toggle-label`
 * / `.theme-toggle`, both `width: 2rem; height: 2rem` squares in
 * layers/layout.css's predecessor). `.icon-button` generalizes that
 * shape into a reusable class so any icon-only action (not just the nav
 * toggle/theme switcher) gets the same treatment, sized off
 * `--target-size-min` rather than a hard-coded 2rem so it never drops
 * below the WCAG 2.5.8 minimum hit area.
 */

.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--target-size-min);
  height: var(--target-size-min);
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-muted);
  font-size: var(--font-size-base);
  line-height: 1;
  cursor: pointer;
  transition:
    background-color var(--transition),
    border-color var(--transition),
    color var(--transition);
}

.icon-button:hover {
  background: var(--color-surface-alt);
  border-color: var(--color-border-strong);
  color: var(--color-text);
  text-decoration: none;
}

.icon-button-ghost {
  border-color: transparent;
  background: transparent;
}

.icon-button-ghost:hover {
  background: var(--color-surface-alt);
  border-color: transparent;
}

.icon-button:disabled,
.icon-button[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
}

/* See layers/components/sidebar.css's `.side-icon > svg` for why this is
   needed alongside `{% icon %}`'s own base-layer `.icon` class - fills
   the button's full `--target-size-min` box (icon-button.html's own
   `icon`/default slot already renders it with no extra padding). */
.icon-button > svg {
  width: 100%;
  height: 100%;
}

/*
 * Theme toggle — @layer components (v0.1.2).
 *
 * <c-gw.theme-toggle> (cotton/gw/theme_toggle.html) is a single
 * `.icon-button` (layers/components/icon-button.css already sizes/
 * borders it) wrapping three icon spans, one per theme state
 * (`[data-gw-theme-icon="system|light|dark"]`) - this file's only job is
 * showing exactly the one matching the button's own
 * `data-gw-theme-current` attribute (kept in sync by
 * static/grundwerk_ui/js/theme.js's `[data-gw-theme-cycle]` handler) and
 * hiding the other two. No inline `style=` (proposal 4.4's CSP rule) -
 * the attribute is what changes, this file is what reacts to it.
 */

.theme-toggle [data-gw-theme-icon] {
  display: none;
  width: 100%;
  height: 100%;
}

.theme-toggle [data-gw-theme-icon] > svg {
  width: 100%;
  height: 100%;
}

/* :not([data-gw-theme-current]) is a defensive fallback for a toggle
   built by hand (not through the cotton component, which always renders
   the attribute) with no data-gw-theme-current yet - shows "system" so a
   pre-JS/broken-JS render never stacks all three icons on top of each
   other. */
.theme-toggle:not([data-gw-theme-current]) [data-gw-theme-icon="system"],
.theme-toggle[data-gw-theme-current="system"] [data-gw-theme-icon="system"],
.theme-toggle[data-gw-theme-current="light"] [data-gw-theme-icon="light"],
.theme-toggle[data-gw-theme-current="dark"] [data-gw-theme-icon="dark"] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/*
 * Copy button — @layer components.
 *
 * Ported from gastwerk's base.css (byte-identical class; absent in
 * zeitwerk per docs/css-architecture.md's inventory table). Bare-icon
 * button, no border/surface — sits inline next to the value it copies
 * (an IBAN, a reference number) rather than looking like a standalone
 * action.
 */

.copy-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--target-size-min);
  min-height: var(--target-size-min);
  padding: 0.1rem 0.3rem;
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-muted);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  vertical-align: middle;
}

.copy-button:hover {
  background: var(--color-surface-alt);
  color: var(--color-text);
}


/* Data display */
/*
 * Card — @layer components.
 *
 * Ported from mietwerk's base.css (canonical; byte-identical in zeitwerk
 * and gastwerk). A bordered, softly elevated surface for grouping
 * content; `<c-gw.card>` (cotton/gw/card.html) adds header/body/footer
 * slots on top of the same class.
 */

.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--space-lg);
}

.card > *:last-child {
  margin-bottom: 0;
}

.card > h2,
.card > h3 {
  font-size: var(--font-size-lg);
  margin: 0 0 var(--space-md);
}

.card > h3 {
  font-size: var(--font-size-base);
}

/* New: header/footer regions for the cotton component's named slots. Kept
   as simple flex rows with a hairline separator - no fork needed these,
   because no fork had a slot-based card, only a bare div with headings
   flowing inside it. */
.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  margin: calc(var(--space-lg) * -1) calc(var(--space-lg) * -1) var(--space-md);
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--color-border);
}

.card-header > *:last-child {
  margin-bottom: 0;
}

.card-footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-sm);
  margin: var(--space-md) calc(var(--space-lg) * -1) calc(var(--space-lg) * -1);
  padding: var(--space-md) var(--space-lg);
  border-top: 1px solid var(--color-border);
}

/*
 * Stat tile — @layer components.
 *
 * Ported from mietwerk's base.css (canonical; byte-identical in zeitwerk
 * and gastwerk). `.stat-card` is used both standalone in `<main>` and
 * nested in a `.card` (deposits/lease_detail.html in mietwerk) - both
 * cases are kept.
 *
 * New (not in any fork): `.stat-delta`, modelled on the badge status
 * tokens, for the "delta/trend" the proposal's component table (4.3)
 * asks for - no fork's stat card carried a trend indicator, only the
 * three state-colour variants below (which colour the whole tile, not a
 * delta figure).
 */

.stat-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  margin-bottom: var(--space-lg);
}

.stat-card {
  flex: 1 1 11rem;
  padding: var(--space-md);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  text-decoration: none;
  color: inherit;
}

a.stat-card:hover {
  border-color: var(--color-border-strong);
  text-decoration: none;
  color: inherit;
}

.card .stat-row {
  margin-bottom: 0;
}

.card .stat-card {
  background: var(--color-surface-alt);
}

.stat-value {
  font-size: var(--font-size-xl);
  font-weight: 600;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
}

.stat-label {
  color: var(--color-muted);
  font-size: var(--font-size-xs);
  letter-spacing: 0.03em;
  text-transform: uppercase;
  margin-top: 0.15rem;
}

.stat-delta {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  margin-top: var(--space-xs);
  font-size: var(--font-size-xs);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.stat-delta-up {
  color: var(--color-success);
}

.stat-delta-down {
  color: var(--color-danger);
}

.stat-delta-neutral {
  color: var(--color-muted);
}

.stat-card-warning {
  border-left: 3px solid var(--color-warning-text);
}

.stat-card-warning .stat-value {
  color: var(--color-warning-text);
}

.stat-card-danger {
  border-left: 3px solid var(--color-danger);
}

.stat-card-danger .stat-value {
  color: var(--color-danger);
}

.stat-card-success {
  border-left: 3px solid var(--color-success);
}

.stat-card-success .stat-value {
  color: var(--color-success);
}

/*
 * Badge — @layer components.
 *
 * Ported from mietwerk's base.css (canonical; byte-identical in zeitwerk
 * and gastwerk). `<c-gw.badge variant="primary">` (proposal 4.3's
 * component table lists the variant as "primary", matching the
 * `--color-primary` token vocabulary used everywhere else) renders
 * `.badge-primary`; `.badge-accent` (every fork's original class name,
 * "accent" was the one place that vocabulary disagreed with itself) is
 * kept as a selector alias so existing non-cotton templates keep working
 * unchanged.
 */

.badge {
  display: inline-block;
  padding: 0.1rem 0.55rem;
  border-radius: var(--radius-pill);
  font-size: var(--font-size-xs);
  font-weight: 600;
  letter-spacing: 0.01em;
  line-height: 1.6;
  border: 1px solid transparent;
  white-space: nowrap;
  vertical-align: 0.05em;
}

.badge-success {
  background: var(--color-success-bg);
  color: var(--color-success);
  border-color: var(--color-success-border);
}

.badge-warning {
  background: var(--color-warning-bg);
  color: var(--color-warning-text);
  border-color: var(--color-warning-border);
}

.badge-danger {
  background: var(--color-danger-bg);
  color: var(--color-danger);
  border-color: var(--color-danger-border);
}

.badge-info {
  background: var(--color-info-bg);
  color: var(--color-info-text);
  border-color: var(--color-info-border);
}

.badge-primary,
.badge-accent {
  background: var(--color-primary-tint);
  color: var(--color-primary);
  border-color: var(--color-primary-tint-border);
}

.badge-muted {
  background: var(--color-muted-bg);
  color: var(--color-muted-text);
  border-color: var(--color-muted-border);
}

/* Numeric count badge (v0.1.1) — a small, solidly-filled pill for an
 * unread/pending count (e.g. next to a nav link), distinct from the
 * status `.badge-*` variants above: those tint a status colour over the
 * surface, this is always fully opaque so it reads at a glance regardless
 * of what it sits on. Ported from zeitwerk's own `.badge-count` (its
 * base.css, "Phase B1" addition) - unchanged rule, now backed by a real
 * shared token pair (`--color-count-bg`/`-fg`, layers/tokens.css) instead
 * of a project-local one. Colour is never the only signal here either —
 * always pair with an accessible name for what's being counted (e.g. via
 * the nav link's own visible text or an `aria-label`).
 */
.badge-count {
  display: inline-block;
  min-width: 1.25rem;
  padding: 0 0.35rem;
  border-radius: var(--radius-pill);
  background: var(--color-count-bg);
  color: var(--color-count-fg);
  font-size: var(--font-size-xs);
  line-height: 1.3rem;
  text-align: center;
  font-weight: 600;
}

/*
 * Table — @layer components.
 *
 * The bare-element table look (border, zebra, hover) already lives in
 * layers/base.css so a plain `<table>` needs no class. This file adds
 * the component-level pieces from docs/css-architecture.md's inventory
 * row for `table`: the horizontal-scroll wrapper, sortable-header
 * visuals, the two-line cell pattern, and the colspan empty-state row.
 * Ported from mietwerk's base.css (canonical; byte-identical in zeitwerk
 * and gastwerk).
 *
 * `.sort-th`/`.sort-link`/`.sort-arrow*` style a sortable column header,
 * but the markup that toggles sort direction and preserves the
 * querystring (Django's `{% querystring %}` tag) is a later templatetag
 * task (proposal 4.1: "sort_header" in templatetags/grundwerk_ui.py) -
 * `<c-gw.table>` (cotton/gw/table.html) only leaves a `head` slot for
 * whichever `<th>` markup (plain or sortable) a caller puts there; these
 * classes are ready for that later task to use unchanged.
 */

.table-scroll {
  overflow-x: auto;
}

.table-scroll > table {
  margin-bottom: 0;
}

.sort-th {
  padding: 0;
}

.sort-link {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  padding: var(--space-sm) var(--space-md);
  color: inherit;
  font: inherit;
  text-decoration: none;
  white-space: nowrap;
}

.sort-link:hover {
  background: var(--color-surface-hover);
  color: var(--color-text);
  text-decoration: none;
}

.sort-link-active {
  color: var(--color-primary);
}

.sort-arrow {
  font-size: 0.7em;
  line-height: 1;
}

.sort-arrow-idle {
  opacity: 0.35;
}

.sort-link:hover .sort-arrow-idle {
  opacity: 0.7;
}

/* Two-line cell: primary value + a smaller sub-label below it (e.g. a
   tenant's name with their unit underneath). Saves a column and keeps a
   scanned row unambiguous. */
.cell-primary {
  display: block;
  font-weight: 500;
}

.cell-sub {
  display: block;
  margin-top: 0.1rem;
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

/* The table's built-in empty state: the caller renders exactly one row
   with a single cell spanning every column
   (`<tr><td colspan="n">…</td></tr>`) - a real data row never has
   `colspan`, so this can style it without any extra markup or wrapper
   class. `<c-gw.table>`'s `empty` slot renders into exactly this shape -
   see the component's docstring. */
tbody tr:only-child td[colspan] {
  padding: var(--space-xl) var(--space-md);
  text-align: center;
  color: var(--color-muted);
  font-size: var(--font-size-sm);
}

tbody tr:only-child:has(td[colspan]):hover {
  background: transparent;
}

/* A sticky <caption> above the scroll region (proposal 4.3 wants a
   focusable, captioned scroll region) - visually a small muted label,
   not the ugly top-aligned browser default. */
.table-scroll > table > caption {
  padding: var(--space-sm) var(--space-md);
  text-align: left;
  font-size: var(--font-size-sm);
  color: var(--color-muted);
  caption-side: top;
}

/* The scroll region itself is the focusable element (tabindex="0" in the
   cotton template) when the table overflows - a visible focus ring tells
   a keyboard user there is more to scroll. */
.table-scroll:focus-visible {
  outline: 2px solid var(--focus-ring-color);
  outline-offset: -2px;
}

/*
 * Pagination — @layer components.
 *
 * Ported from gastwerk's base.css (byte-identical class family; absent
 * in zeitwerk's base.css per docs/css-architecture.md's inventory - no
 * paginated list existed there when its CSS was written).
 *
 * New: `.pagination-numbers` / `.pagination-ellipsis` for the numbered
 * pages `<c-gw.pagination>` renders (mietwerk's own `ui/includes/
 * pagination.html` only ever rendered prev/next + a "Page X of Y" count,
 * never individual page numbers - the proposal's component table (4.3)
 * asks for numbered pages with an ellipsis, so this is new chrome around
 * the same `.pagination-link`/`.pagination-current` family).
 */

.pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

.pagination-pages {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

.pagination-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--target-size-min);
  min-height: var(--target-size-min);
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  font-size: var(--font-size-sm);
  color: var(--color-text);
  text-decoration: none;
  font-variant-numeric: tabular-nums;
}

.pagination-link:hover {
  background: var(--color-surface-alt);
  border-color: var(--color-border-strong);
  text-decoration: none;
}

/* Not a link, a <span> - a dead page must not look clickable or be
   reachable by Tab. */
.pagination-link-disabled {
  color: var(--color-muted);
  background: var(--color-surface-alt);
  opacity: 0.6;
}

.pagination-link-disabled:hover {
  background: var(--color-surface-alt);
  border-color: var(--color-border);
}

.pagination-link[aria-current="page"] {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-primary-contrast);
  font-weight: 600;
}

.pagination-link[aria-current="page"]:hover {
  background: var(--color-primary-hover);
  border-color: var(--color-primary-hover);
}

.pagination-ellipsis {
  padding: 0 0.3rem;
  color: var(--color-muted);
}

.pagination-current {
  padding: 0 var(--space-sm);
  font-size: var(--font-size-sm);
  color: var(--color-muted);
  font-variant-numeric: tabular-nums;
}

.pagination-count {
  margin: 0;
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  font-variant-numeric: tabular-nums;
}

/*
 * Description list ("detail grid") — @layer components.
 *
 * Ported from mietwerk's base.css (canonical; byte-identical in
 * zeitwerk and gastwerk). Two-column key/value grid for detail-view
 * `<dl>`s; collapses to a single column on narrow screens so a long
 * value never gets crushed into a 9rem key column.
 */

.detail-grid {
  display: grid;
  grid-template-columns: minmax(9rem, 14rem) 1fr;
  gap: 0.55rem var(--space-lg);
  font-size: var(--font-size-sm);
}

.detail-grid dt {
  color: var(--color-muted);
  font-weight: 500;
}

.detail-grid dd {
  margin: 0;
}

@media (max-width: 40rem) {
  .detail-grid {
    grid-template-columns: 1fr;
    gap: 0.15rem 0;
  }

  .detail-grid dd {
    margin-bottom: var(--space-sm);
  }
}

/*
 * Timeline (audit entries) — @layer components.
 *
 * New component: no fork had a chronological timeline widget. All three
 * render audit-log entries as a plain table (see zeitwerk's
 * `audit/templates/audit/includes/auditlog_table.html`; mietwerk/gastwerk
 * do the same for their own audit apps) - a table is still the better
 * fit for a *paginated* audit log with a filter bar, and stays covered
 * by `<c-gw.table>`. `<c-gw.timeline>` is for the shorter, unpaginated
 * "what happened to this record" feed the proposal's component table
 * (4.3) separately asks for (e.g. a handful of entries inside a detail
 * page's sidebar) - a connecting line + dot per entry reads faster than
 * a table there. Reuses `.audit-diff`/`.audit-diff-old` (ported
 * unchanged from zeitwerk/gastwerk, byte-identical in both) for the
 * optional old->new value list inside an entry.
 */

.timeline {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.timeline-item {
  position: relative;
  padding: 0 0 var(--space-lg) var(--space-lg);
  border-left: 1px solid var(--color-border);
}

.timeline-item:last-child {
  padding-bottom: 0;
  border-left-color: transparent;
}

.timeline-item::before {
  content: "";
  position: absolute;
  top: 0.3rem;
  left: -0.3125rem;
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: var(--color-primary);
  border: 2px solid var(--color-surface);
}

.timeline-time {
  display: block;
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  font-variant-numeric: tabular-nums;
}

.timeline-title {
  margin: 0.1rem 0 0;
  font-size: var(--font-size-sm);
  font-weight: 600;
}

.timeline-body {
  margin-top: 0.2rem;
  font-size: var(--font-size-sm);
  color: var(--color-muted);
}

.audit-diff {
  max-width: 28rem;
}

.audit-diff dl {
  margin: var(--space-xs) 0 0;
  font-size: var(--font-size-xs);
}

.audit-diff dt {
  font-weight: 600;
  color: var(--color-muted);
}

.audit-diff dd {
  margin: 0 0 var(--space-xs);
  overflow-wrap: anywhere;
}

.audit-diff-old {
  text-decoration: line-through;
  color: var(--color-muted);
}


/* Navigation */
/*
 * Sidebar — @layer components.
 *
 * The structural shell (`.sidebar` positioning, mobile drawer collapse,
 * scrim) already lives in layers/layout.css, wired to nav.js's
 * `data-gw-nav-*` attribute contract - see that file's header comment.
 * This file is the *visual* styling of what goes inside the shell:
 * ported from mietwerk's base.css (canonical; byte-for-byte identical in
 * zeitwerk and gastwerk per docs/css-architecture.md's inventory row).
 *
 * `.side-link-active` (mietwerk's class, the public API existing project
 * templates already use) and `.side-link[aria-current="page"]`
 * (`<c-gw.sidebar-link active>`'s markup - aria-current is the
 * standards-based way to mark the current page, screen readers get it
 * for free) both select the same look, so either markup works.
 */

.sidebar-head {
  padding: var(--space-md) var(--space-md) var(--space-sm);
}

.sidebar-nav {
  flex: 1 1 auto;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 0 0.6rem var(--space-md);
}

/* Section label - a visual grouping hint only, not an interactive
   element. */
.side-section {
  margin: var(--space-md) 0 var(--space-xs);
  padding: 0 0.6rem;
  font-size: var(--font-size-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-muted);
}

.side-section:first-child {
  margin-top: 0;
}

.side-link {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-height: var(--target-size-min);
  padding: 0.4rem 0.6rem;
  border-radius: var(--radius-md);
  color: var(--color-muted);
  text-decoration: none;
  font-size: var(--font-size-sm);
  font-weight: 500;
  transition:
    background-color var(--transition),
    color var(--transition);
}

/* Icons inherit the link colour (currentColor) so hover/active/dark-mode
   follow automatically. */
.side-icon {
  flex: 0 0 auto;
  width: 1.05rem;
  height: 1.05rem;
  opacity: 0.75;
}

/* `{% icon %}`'s own `.icon` class (layers/base.css) already sizes the
   <svg> to 1em, but this wrapper's own explicit 1.05rem should win, not
   whatever font-size happens to be in effect where the icon is used -
   components beats base in layer order regardless of selector
   specificity (see base.css's own comment on this rule). */
.side-icon > svg {
  width: 100%;
  height: 100%;
}

.side-link-active .side-icon,
.side-link[aria-current="page"] .side-icon,
.side-link:hover .side-icon {
  opacity: 1;
}

.side-link:hover {
  background: var(--color-surface-alt);
  color: var(--color-text);
  text-decoration: none;
}

/* Active entry: filled area + accent colour, plus a left-edge bar so it
   still reads at a glance for someone who can't rely on colour alone. */
.side-link-active,
.side-link[aria-current="page"] {
  background: var(--color-primary-tint);
  color: var(--color-primary);
  font-weight: 600;
  box-shadow: inset 2px 0 0 var(--color-primary);
}

.side-link-active:hover,
.side-link[aria-current="page"]:hover {
  background: var(--color-primary-tint);
  color: var(--color-primary);
}

.sidebar-foot {
  border-top: 1px solid var(--color-border);
  padding: var(--space-sm) var(--space-md);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.sidebar-foot-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs);
}

.sidebar-foot-actions > * {
  min-width: 0;
}

.side-link-sm {
  padding: 0.2rem 0.4rem;
  font-size: var(--font-size-xs);
}

/*
 * Nav: topbar, logged-out header nav, back link, skip link — @layer
 * components.
 *
 * `.topbar` structural collapse (sticky positioning, mobile toggle
 * mechanics) lives in layers/layout.css already; this file is its
 * visual chrome plus the logged-out `.nav`/`.nav-brand`/`.nav-user`
 * family, all ported from mietwerk's base.css (canonical; byte-identical
 * in zeitwerk/gastwerk except zeitwerk's simpler logged-out header,
 * noted per-selector below).
 *
 * The mobile nav toggle targets `[data-gw-nav-toggle]`, not a
 * `.nav-toggle-label`/checkbox pair - that markup no longer exists (see
 * docs/css-architecture.md, "Mobile nav toggle - not the checkbox hack",
 * and nav.js's own header comment). `<c-gw.topbar>`/`<c-gw.icon-button>`
 * render the button this file styles.
 */

.topbar-brand {
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-muted);
}

/* New: right-aligned action group in <c-gw.topbar>'s `actions` slot
   (theme toggle, user menu, …) — pushes itself to the end of the
   flex row without an inline style. */
.topbar-actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-left: auto;
}

/* The nav toggle only ever needs to be visible below the drawer
   breakpoint - layers/layout.css already drives the drawer open/close
   state at that same breakpoint (48rem). */
[data-gw-nav-toggle] {
  display: none;
}

@media (max-width: 48rem) {
  [data-gw-nav-toggle] {
    display: inline-flex;
  }
}

/* Logged-out header (mietwerk/gastwerk's `.nav`; zeitwerk's equivalent is
   plainer - bare `.nav a`, no `.nav-brand`/`.nav-links` split - but the
   classes below degrade to that same look when unused, so one file
   covers both). */
.nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  max-width: var(--content-width);
  margin-inline: auto;
  padding: 0.6rem var(--space-lg);
}

.nav-brand {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 600;
  font-size: var(--font-size-base);
  letter-spacing: -0.01em;
  margin-right: var(--space-md);
  padding: 0.3rem 0;
  color: var(--color-text);
}

.nav-logo {
  height: 1.6rem;
  width: 1.6rem;
  border-radius: var(--radius-sm);
  display: block;
}

.nav a {
  color: var(--color-text);
  text-decoration: none;
}

/* State is shown through fill, not underline, inside nav chrome. */
.nav a:hover {
  text-decoration: none;
}

.nav-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs);
  flex: 1 1 auto;
}

.nav-link {
  display: inline-block;
  color: var(--color-muted);
  text-decoration: none;
  font-size: var(--font-size-sm);
  font-weight: 500;
  padding: 0.35rem 0.6rem;
  border-radius: var(--radius-md);
  transition:
    background-color var(--transition),
    color var(--transition);
}

.nav-link:hover {
  background: var(--color-surface-alt);
  color: var(--color-text);
  text-decoration: none;
}

.nav-link-active,
.nav-link[aria-current="page"] {
  color: var(--color-primary);
  background: var(--color-primary-tint);
  font-weight: 600;
}

.nav-link-active:hover,
.nav-link[aria-current="page"]:hover {
  background: var(--color-primary-tint);
  color: var(--color-primary);
}

.nav-user {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-left: auto;
  padding-left: var(--space-md);
  color: var(--color-muted);
  font-size: var(--font-size-sm);
  white-space: nowrap;
}

.nav-logout-form {
  margin: 0;
  padding: 0;
}

.nav-logout-form button {
  background: none;
  border: none;
  padding: 0.25rem 0.4rem;
  border-radius: var(--radius-sm);
  font: inherit;
  color: var(--color-muted);
  cursor: pointer;
  text-decoration: none;
}

.nav-logout-form button:hover {
  background: var(--color-surface-alt);
  color: var(--color-text);
  text-decoration: none;
}

/* Back link (gastwerk-only today; promoted to shared per
   docs/css-architecture.md's inventory - a generic "back to the parent
   list" link, not CMS/booking-specific). Small and muted rather than the
   normal link accent, so it doesn't compete with the page title that
   follows it. */
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  min-height: var(--target-size-min);
  color: var(--color-muted);
  font-size: var(--font-size-sm);
  text-decoration: none;
}

.back-link:hover {
  color: var(--color-text);
  text-decoration: underline;
}

/* Skip link (new - no fork had one). Off-screen until it receives
   keyboard focus, then pinned to the top-left corner above everything
   else - the first focusable element on every page, per the cotton
   component's docstring. */
.skip-link {
  position: absolute;
  top: -3rem;
  left: var(--space-sm);
  z-index: var(--z-overlay);
  padding: var(--space-sm) var(--space-md);
  background: var(--color-surface);
  color: var(--color-text);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  text-decoration: none;
  transition: top var(--transition);
}

.skip-link:focus {
  top: var(--space-sm);
}

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

/*
 * Tabs — @layer components.
 *
 * Ported from mietwerk's base.css (canonical; byte-identical in
 * zeitwerk and gastwerk). Link-based, server-rendered page tabs (see
 * cotton/gw/tabs.html's docstring for why this is not the ARIA
 * `role="tablist"` widget) - `[aria-current="page"]` replaces the
 * fork's `.tab-active` class as the *state* marker (screen readers get
 * the state for free; the class stays only for the fork-compatible
 * selector below).
 */

.tabs {
  display: flex;
  gap: var(--space-xs);
  border-bottom: 1px solid var(--color-border);
  overflow-x: auto;
}

.tab {
  display: inline-flex;
  align-items: center;
  min-height: var(--target-size-min);
  padding: 0.5rem 0.75rem;
  border-bottom: 2px solid transparent;
  color: var(--color-muted);
  font-size: var(--font-size-sm);
  font-weight: 500;
  white-space: nowrap;
  text-decoration: none;
}

.tab:hover {
  color: var(--color-text);
  text-decoration: none;
}

.tab[aria-current="page"],
.tab-active {
  color: var(--color-primary);
  border-bottom-color: var(--color-primary);
  font-weight: 600;
}

/*
 * Breadcrumbs — @layer components.
 *
 * New: no admin/back-office fork had a breadcrumb trail (only gastwerk's
 * unrelated public-site `.breadcrumb` in blocks.css - a different design
 * system entirely, not ported, see docs/css-architecture.md's "gastwerk's
 * four public-site stylesheets" section). Named `.breadcrumbs` (plural),
 * deliberately distinct from that public-site `.breadcrumb` class so the
 * two can never collide if a project ever loads both stylesheets on one
 * page.
 */

.breadcrumbs {
  margin-bottom: var(--space-sm);
}

.breadcrumbs-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--font-size-sm);
  color: var(--color-muted);
}

.breadcrumbs-list li {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}

.breadcrumbs-list li:not(:last-child)::after {
  content: "/";
  color: var(--color-border-strong);
}

.breadcrumbs-list a {
  color: var(--color-muted);
  text-decoration: none;
}

.breadcrumbs-list a:hover {
  color: var(--color-text);
  text-decoration: underline;
}

.breadcrumbs-list [aria-current="page"] {
  color: var(--color-text);
  font-weight: 500;
}


/* Layout */
/*
 * Page header — @layer components.
 *
 * Ported from mietwerk's base.css (canonical; byte-identical in
 * zeitwerk and gastwerk). Recurring list-page scaffold: title on the
 * left, primary action on the right, wrapping to a second line on
 * narrow screens rather than crushing the title.
 *
 * No `margin-bottom` here (v0.1.6 fix, gastwerk review item 3) - this
 * component is always `.page`'s own first child (`{% block page_header
 * %}` renders directly above `{% block content %}` inside `.page`,
 * base_app.html/base_public.html), so `layers/layout.css`'s `.page > *
 * + *` flow rule already owns the gap to whatever follows it; a margin
 * here too would double it.
 */

.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  flex-wrap: wrap;
}

.page-header h1 {
  margin: 0;
}

.page-header-description {
  margin: 0.2rem 0 0;
  color: var(--color-muted);
  font-size: var(--font-size-sm);
}

.page-header-actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-wrap: wrap;
}

/*
 * Toolbar / filter bar — @layer components.
 *
 * `.toolbar` base ported from mietwerk's base.css (canonical; identical
 * in zeitwerk/gastwerk). The two additions below are gastwerk-only
 * today; docs/css-architecture.md's inventory recommends adopting both
 * as shared, since neither is CMS/booking-specific:
 *
 *   - `.toolbar form { display: contents }` - a single-button <form>
 *     (e.g. a lone "regenerate PDF" POST) would otherwise force a line
 *     break inside the toolbar (a <form> is block-level by default).
 *     `display: contents` drops the form's own box from layout so its
 *     child button becomes a flex item of `.toolbar` directly; the form
 *     stays fully functional (submit/CSRF unaffected), only its box
 *     disappears.
 *   - `.toolbar--split` - a second action row, visually separated from
 *     the form/content above it (e.g. "Download PDF" / "Send for
 *     signature" below an edit form).
 */

.toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  align-items: center;
}

.toolbar form {
  display: contents;
}

.toolbar--split {
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-md);
  margin-top: var(--space-md);
}

/* Inline form (v0.1.1) — a small <form> that must sit inline in a table
 * cell or a toolbar row instead of forcing its own line (a <form> is
 * block-level by default; unlike `.toolbar form { display: contents }`
 * above, this is for a form that stands alone, not nested inside
 * `.toolbar`). Ported unchanged from zeitwerk's base.css ("Phase B1"
 * addition) - docs/css-architecture.md's inventory flagged it as a
 * shared candidate ("generic, not time-tracking-specific"), grouped here
 * since it solves the exact same "a <form> shouldn't break the layout
 * around it" problem as the rule above.
 */
.inline-form {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
}

/*
 * Filter bar — @layer components.
 *
 * Two related v0.1.0 gaps live in this one file (docs/ui.md's "grundwerk-
 * ui gaps" #3/#4):
 *
 *   1. `.filter-bar` — a compact, bordered strip for a GET filter <form>
 *      (search input, extra fields, submit, reset). Ported from
 *      zeitwerk's own workaround, which matched every GET form on the
 *      page via the attribute selector `main form[method="get"]` -
 *      promoted here to an explicit class per that gap report's own
 *      suggested fix, so a project opts a <form> into this styling
 *      instead of every GET form automatically getting it (an unrelated
 *      GET-method search form outside a list page would otherwise be
 *      styled as a filter bar too). `<c-gw.filter-bar>` (cotton/gw/
 *      filter-bar.html) renders it.
 *
 *      mietwerk's own `.filter-bar` was a different, unbordered thing —
 *      a plain flex row for the *active-filter chips* below, not a form
 *      wrapper. Both are folded into one bordered container class here:
 *      "the strip of controls above a filtered list" is one visual idea
 *      whether its content is form fields, chips, or both together (a
 *      compact form with the currently-applied filters shown as chips
 *      alongside it is a common combination) — a project that wants the
 *      older, plainer chip-row-only look can still override `.filter-bar`
 *      locally (docs/css-architecture.md, "a project's own CSS ... beats
 *      all six [layers]").
 *   2. `.filter-chip`/`-value`/`-remove` — the active-filters-as-
 *      removable-pills pattern (byte-identical in mietwerk/zeitwerk).
 *      The remove affordance is a real link (`<a class="filter-chip-
 *      remove" href="...">`, an href dropping just that one query
 *      parameter), not a button/JS — works with JS disabled, no
 *      client-side state to keep in sync with the server-rendered list.
 *
 * `.search-input` (width only — border/radius/focus already come from
 * layers/base.css's generic `input[type="search"]`) is also promoted from
 * gap to shared file here: css-architecture.md's inventory suggested
 * "search.css (or folded into toolbar.css)"; folded into filter.css
 * instead, since every project that used it did so inside a filter bar.
 */

.filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-md);
  padding: var(--space-md);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
}

.filter-bar label {
  margin: 0;
  font-size: var(--font-size-xs);
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--color-muted);
}

/* Groups one label+control pair as a single flex item (v0.1.6 fix,
   gastwerk review item 21) - without this, an un-wrapped <label>/<input>
   or <label>/<select> pair are two separate flex items in `.filter-bar`'s
   wrapping row and can end up on different lines on a narrow bar (the
   label ending one row, its control starting the next) - seen on
   gastwerk's own `/nachrichten/` page. `<c-gw.filter-bar>`'s own search
   field uses this wrapper; a caller's own extra fields (the default slot)
   should too - see that component's docstring. */
.filter-bar-field {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.filter-bar input[type="text"],
.filter-bar input[type="search"],
.filter-bar input[type="date"],
.filter-bar input[type="number"],
.filter-bar select {
  width: auto;
  min-width: 11rem;
  max-width: 100%;
}

/* Wraps the submit button/reset link, pushed to the end of the bar - kept
   as its own class (rather than a bare-button selector) so it still works
   when the submit button is a <c-gw.button> (renders class="button ...",
   not <button type="submit"> element selector). */
.filter-bar-actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-left: auto;
}

.search-input {
  min-width: 18rem;
  max-width: 100%;
}

.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  white-space: nowrap;
}

.filter-chip-value {
  color: var(--color-text);
  font-weight: 600;
}

.filter-chip-remove {
  padding: 0 0.15rem;
  border: none;
  background: none;
  color: var(--color-muted);
  font-size: var(--font-size-sm);
  line-height: 1;
  cursor: pointer;
  text-decoration: none;
}

.filter-chip-remove:hover {
  color: var(--color-danger);
  background: none;
  text-decoration: none;
}


/* Auth */
/*
 * Auth shell — @layer components.
 *
 * `grundwerk_ui/base_auth.html` renders `<main class="auth-shell"><div
 * class="login-card"><div class="login-brand">` itself, for every project
 * that extends it - but v0.1.0 shipped no component file styling any of
 * the three classes (a gap found during the zeitwerk pilot adoption,
 * docs/ui.md's "grundwerk-ui gaps" #2: css-architecture.md's own
 * inventory already recommended a `login.css` component file, it just
 * never got built). Named `auth.css`, not `login.css` - the shell is also
 * used for MFA/2FA pages (allauth's `mfa` templates,
 * `templates/allauth/layouts/entrance.html`), not only the login form
 * itself, and its classes are `.auth-shell`/`.login-card`/`.login-brand`
 * either way.
 *
 * `.provider-list` (OIDC provider buttons) and `.breakglass` (a collapsed
 * local-login fallback below them) are ported from zeitwerk's own
 * workaround unchanged - css-architecture.md's inventory recommends
 * adopting both as the canonical shared pattern (a project template
 * supports `with_sso`, so an OIDC provider list with a local-login
 * fallback is generic, not zeitwerk-specific), even though no cotton
 * component or base template here renders either one itself yet (that is
 * project content, same as `auth_content`'s own body - see
 * `base_auth.html`'s own docstring).
 */

.auth-shell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-lg);
  padding-top: var(--space-xl);
  min-height: 60vh;
}

.login-card {
  width: 100%;
  max-width: 23rem;
  padding: var(--space-xl);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}

/* Vertical rhythm inside the login card (v0.1.6 fix, gastwerk review item
   24) - same lobotomized-owl shape as `layers/layout.css`'s `.page > * +
   *`, scoped to `.login-card`'s own direct children instead. Needed for
   allauth's MFA pages specifically: `mfa/authenticate.html` (unstyled by
   this package until now) renders `<h1>`, `<p>`, the login `<form>`, an
   `<hr>`, an `<h2>` ("Alternative options"), then the webauthn/cancel
   button group as siblings straight inside `.login-card` (via
   `elements/hr.html`/`elements/h2.html` - allauth's own unstyled
   defaults, no grundwerk-ui override existed for either) - with no
   margin on any of them, the "Alternative options" heading sat flush
   under the form's own buttons, and its own button group sat flush
   under it in turn. Placed early in this file, before `.login-brand`/
   `.login-card h1`/`.provider-list`/`.breakglass` below, so each of
   those keeps deciding its own margin via ordinary cascade/specificity
   (a type-qualified or later same-specificity rule still wins) instead
   of this rule overriding them. */
.login-card > * + * {
  margin-block-start: var(--space-lg);
}

.login-brand {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-lg);
  font-weight: 600;
}

.login-card h1 {
  margin-top: 0;
  margin-bottom: var(--space-lg);
  font-size: var(--font-size-xl);
  letter-spacing: -0.02em;
}

.login-card input:not([type="checkbox"]):not([type="radio"]),
.login-card button,
.login-card input[type="submit"] {
  max-width: 100%;
  width: 100%;
}

/* `button.css`'s base `.button`/`button`/`input[type=submit]` rule sets
   `white-space: nowrap` (the right default for a compact, inline button
   elsewhere in the app) - inside the login card's fixed 23rem width
   (`.login-card`, above) a long label like the German "Verwenden Sie
   einen Sicherheitsschlüssel" ("Use a security key") overflowed its
   full-width button instead of wrapping (v0.1.6 fix, gastwerk review
   item 24, confirmed on the MFA authenticate page). `text-wrap: balance`
   (not just `white-space: normal`) also keeps a two-line label from
   going ragged (one long line, one short word) - min-height still comes
   from `button.css`'s own `min-height: var(--target-size-min)`, so a
   wrapped two-line label only grows the button, never shrinks its
   target size below the 24px floor. */
.login-card .button,
.login-card button,
.login-card input[type="submit"] {
  white-space: normal;
  text-wrap: balance;
}

/* Found on staging: the blanket `.login-card input { width: 100% }`
   above also caught checkbox/radio inputs, stretching a native checkbox
   into a full-card-width bar instead of leaving it a normal checkbox
   sized box next to its label (`:not(...)` above excludes it from that
   rule) - `.form-field[data-gw-field-checkbox]` (layers/components/
   form-field.css) already lays the box and its label out side by side
   for any checkbox/radio field, login card included; this only needed
   to stop overriding the checkbox's own intrinsic size. */
.login-card .form-field[data-gw-field-checkbox] input {
  width: auto;
}

/* `auth_after`'s content (base_auth.html - e.g. a second card below the
   login card) shares the login card's own width instead of stretching
   full-bleed or drifting to its own arbitrary width - the login card
   width is the "content-width" contract for this shell (`.auth-shell` is
   a centred flex column, see base_auth.html's own header comment for the
   auth_after/auth_footer extension points). `:not(.theme-toggle)`
   excludes `<c-gw.theme-toggle>` (also a direct child of `.auth-shell`,
   see base_auth.html's `theme_toggle` block) - it must keep its own
   compact `.icon-button` size, not stretch to 23rem. A project that
   genuinely needs a wider `auth_after` block can still override with its
   own class/style on the element that block renders (documented in
   docs/templates.md). */
.auth-shell > *:not(.theme-toggle) {
  width: 100%;
  max-width: 23rem;
}

.provider-list {
  list-style: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin: var(--space-lg) 0;
}

.provider-list .button,
.provider-list .button-secondary {
  width: 100%;
}

.breakglass {
  margin-top: var(--space-xl);
  padding-top: var(--space-md);
  border-top: 1px solid var(--color-border);
}

.breakglass summary {
  cursor: pointer;
  color: var(--color-muted);
  font-weight: 600;
  font-size: var(--font-size-sm);
}

.breakglass form {
  margin-top: var(--space-md);
}


/* Feedback */
/*
 * Alert / notice — @layer components.
 *
 * New: no fork had a standalone inline notice component. `ul.messages`/
 * `.toast` (T5's territory - do not confuse the two) is for transient,
 * dismissible, `aria-live` messages; `.errornote` (form-field.css, also
 * not this task) is a single-purpose form-level error box. `.alert` is
 * for a persistent, page-content notice ("this lease ends in 14 days",
 * "the PDF export is disabled while a backfill runs") - same status
 * tokens as `.badge`, block-level instead of an inline pill.
 *
 * `.alert-*-body` used to also set `opacity: 0.85` (a de-emphasis nicety
 * for the body text vs. the title) - dropped: found by the style guide's
 * axe-core run (ui/docs/styleguide.md) as a `color-contrast` violation on
 * `.alert-danger .alert-body` in light mode (4.44:1, needs 4.5:1 - the
 * 85%-alpha blend of `--color-danger` over `--color-danger-bg` is what
 * axe actually measures, not the token pair itself) and put
 * `.alert-success` right at the 4.51:1 edge with near-zero margin. The
 * variant colour (`color: inherit`) already carries enough visual
 * emphasis on its own; opacity bought no accessibility-safe benefit.
 */

.alert {
  display: flex;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-surface-alt);
  color: var(--color-text);
  font-size: var(--font-size-sm);
}

.alert-icon {
  flex: 0 0 auto;
  width: 1.1rem;
  height: 1.1rem;
}

/* See layers/components/sidebar.css's `.side-icon > svg` for why this is
   needed alongside `{% icon %}`'s own base-layer `.icon` class. */
.alert-icon > svg {
  width: 100%;
  height: 100%;
}

.alert-title {
  margin: 0;
  font-weight: 600;
}

.alert-body {
  margin: 0.15rem 0 0;
  color: var(--color-muted);
}

.alert-success {
  background: var(--color-success-bg);
  border-color: var(--color-success-border);
  color: var(--color-success);
}

.alert-success .alert-body {
  color: inherit;
}

.alert-warning {
  background: var(--color-warning-bg);
  border-color: var(--color-warning-border);
  color: var(--color-warning-text);
}

.alert-warning .alert-body {
  color: inherit;
}

.alert-danger {
  background: var(--color-danger-bg);
  border-color: var(--color-danger-border);
  color: var(--color-danger);
}

.alert-danger .alert-body {
  color: inherit;
}

.alert-info {
  background: var(--color-info-bg);
  border-color: var(--color-info-border);
  color: var(--color-info-text);
}

.alert-info .alert-body {
  color: inherit;
}

/*
 * Empty state — @layer components.
 *
 * `.empty-state` existed in every fork's base.css as a plain centred
 * text block (docs/css-architecture.md's inventory groups it with
 * table.css, because the *table's own* built-in empty row
 * (`tbody tr:only-child td[colspan]`, styled in components/table.css)
 * reused the exact same padding/colour recipe inline, not this class).
 * This file is the standalone version for outside a table (an empty
 * list page, an empty card) with icon/title/text/action slots - new
 * chrome the proposal's component table (4.3) asks for, the forks only
 * ever rendered a bare paragraph of muted text here.
 */

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-xl) var(--space-md);
  text-align: center;
  color: var(--color-muted);
  font-size: var(--font-size-sm);
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-lg);
}

.empty-state-icon {
  width: 2rem;
  height: 2rem;
  margin-bottom: var(--space-xs);
  opacity: 0.6;
}

/* See layers/components/sidebar.css's `.side-icon > svg` for why this is
   needed alongside `{% icon %}`'s own base-layer `.icon` class. */
.empty-state-icon > svg {
  width: 100%;
  height: 100%;
}

.empty-state-title {
  margin: 0;
  color: var(--color-text);
  font-size: var(--font-size-base);
  font-weight: 600;
}

.empty-state-text {
  margin: 0;
  max-width: 32rem;
}

.empty-state-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
}

/*
 * Skeleton / loading placeholder — @layer components.
 *
 * New: no fork had a loading-skeleton pattern (every fork waited for a
 * full page load, no client-side placeholder state). A pulsing block
 * shape, `aria-hidden` in the cotton template (it is decorative - the
 * real content it stands in for either isn't in the DOM yet or is
 * announced separately), and inert under reduced motion (a static tint
 * instead of a pulse).
 */

.skeleton {
  display: block;
  width: 100%;
  height: 1em;
  border-radius: var(--radius-sm);
  background: var(--color-surface-alt);
  animation: gw-skeleton-pulse 1.4s ease-in-out infinite;
}

.skeleton-text {
  height: 0.85em;
}

.skeleton-text + .skeleton-text {
  margin-top: var(--space-xs);
}

.skeleton-heading {
  height: 1.4em;
  max-width: 16rem;
}

.skeleton-circle {
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
}

.skeleton-block {
  height: 6rem;
  border-radius: var(--radius-lg);
}

@keyframes gw-skeleton-pulse {
  0%,
  100% {
    opacity: 1;
  }

  50% {
    opacity: 0.5;
  }
}

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

/*
 * Progress — @layer components.
 *
 * New: no fork styled the native `<progress>` element (all left it at
 * the browser default). Restyled with the same accent used everywhere
 * else interactive, in both the WebKit/Blink and Firefox pseudo-element
 * vocabularies (there is no standard way to style `<progress>`'s fill,
 * every engine needs its own selector).
 */

.progress-wrap {
  width: 100%;
}

.progress {
  display: block;
  width: 100%;
  height: 0.5rem;
  border: none;
  border-radius: var(--radius-pill);
  overflow: hidden;
  background: var(--color-surface-alt);
}

.progress::-webkit-progress-bar {
  background: var(--color-surface-alt);
  border-radius: var(--radius-pill);
}

.progress::-webkit-progress-value {
  background: var(--color-primary);
  border-radius: var(--radius-pill);
  transition: width var(--transition);
}

.progress::-moz-progress-bar {
  background: var(--color-primary);
  border-radius: var(--radius-pill);
  transition: width var(--transition);
}

/* Indeterminate (no `value` attribute): a soft repeating sweep instead of
   the (very varied, largely unstyleable) native indeterminate look. */
.progress:indeterminate {
  background: linear-gradient(
    90deg,
    var(--color-surface-alt) 25%,
    var(--color-primary-tint) 50%,
    var(--color-surface-alt) 75%
  );
  background-size: 200% 100%;
  animation: gw-progress-sweep 1.4s linear infinite;
}

.progress-label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-xs);
  font-size: var(--font-size-sm);
  color: var(--color-muted);
}

@keyframes gw-progress-sweep {
  from {
    background-position: 200% 0;
  }

  to {
    background-position: -200% 0;
  }
}

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


/* Overlays */
/*
 * Dialog — @layer components (dialog.css). Styles the native <dialog>
 * rendered by cotton/gw/dialog.html (docs/javascript.md, dialog.js owns
 * opening/closing it, not this file).
 *
 * Sizing: three named sizes via `data-gw-dialog-size`, set by the
 * component's `size` attribute (default "md"). Width always yields to the
 * viewport (`min(..., 92vw)`) so a "lg" dialog never overflows a small
 * screen.
 *
 * Backdrop: `::backdrop` uses --color-scrim, the same token the mobile
 * nav drawer scrim uses (layers/layout.css) - one "overlay behind
 * something" colour for the whole package. `<dialog>` provides the
 * backdrop itself; there is deliberately no separate overlay element to
 * style.
 *
 * v0.1.6 fixes (gastwerk review item 22, both confirmed on staging):
 *
 *   - `margin: auto` on the bare `dialog` rule below - `layers/reset.css`'s
 *     `* { margin: 0 }` zeroes the UA stylesheet's own `dialog:modal {
 *     margin: auto }`, which is what centres a `showModal()`-opened
 *     dialog in the first place; without restoring it here, every
 *     `<c-gw.dialog>` opened top-left of the viewport instead of centred.
 *   - `display: flex` used to sit on the *unqualified* `dialog` selector
 *     - since this file's selector matches every `<dialog>` on the page
 *     (not just `<c-gw.dialog>`'s own, also confirm.js's/lightbox.js's,
 *     neither of which sets its own `display`), it overrode the UA
 *     stylesheet's `dialog:not([open]) { display: none }` for all three,
 *     making every closed dialog visible (and taking up layout space) the
 *     moment this stylesheet loaded, open or not. Moved to `dialog[open]`
 *     below so a closed dialog stays hidden regardless of which JS module
 *     built it.
 */

dialog {
  margin: auto;
  padding: 0;
  border: none;
  border-radius: var(--radius-lg);
  background: var(--color-surface);
  color: var(--color-text);
  box-shadow: var(--shadow-lg);
  width: min(28rem, 92vw);
  max-height: 85vh;
  overflow: hidden;
}

dialog[open] {
  display: flex;
  flex-direction: column;
}

dialog[data-gw-dialog-size="sm"] {
  width: min(22rem, 92vw);
}

dialog[data-gw-dialog-size="lg"] {
  width: min(42rem, 92vw);
}

dialog::backdrop {
  background: var(--color-scrim);
}

.dialog-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--color-border);
}

.dialog-title {
  margin: 0;
  font-size: var(--font-size-lg);
}

[data-gw-dialog-close] {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--target-size-min);
  height: var(--target-size-min);
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-muted);
  font-size: var(--font-size-lg);
  line-height: 1;
  cursor: pointer;
}

[data-gw-dialog-close]:hover {
  background: var(--color-surface-hover);
  color: var(--color-text);
}

.dialog-body {
  padding: var(--space-lg);
  overflow-y: auto;
}

.dialog-footer {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-lg);
  border-top: 1px solid var(--color-border);
}

/*
 * Confirm dialog — @layer components (confirm.css). Styles the single
 * native <dialog> confirm.js builds lazily on first use (docs/javascript.md,
 * confirm.js) - there is no cotton template for it, the JS file's own
 * header comment explains why ("no template dependency, so this works
 * before any cotton component ships"). This file only has to match the
 * exact attributes that script creates:
 *
 *   <dialog data-gw-confirm-dialog>
 *     <p data-gw-confirm-message></p>
 *     <div data-gw-confirm-actions>
 *       <button data-gw-confirm-cancel type="button"></button>
 *       <button data-gw-confirm-accept type="button"></button>
 *     </div>
 *   </dialog>
 *
 * Self-contained (full dialog chrome, not just the confirm-specific
 * parts) rather than relying on dialog.css's bare `dialog` rule for its
 * base look - both land in the same `components` layer with no
 * conflicting selectors, so loading both is harmless, but a project
 * should be able to use confirm.js (which ships with every install, see
 * proposal 4.3) without also pulling in the <c-gw.dialog> component's
 * CSS. `margin: auto` (v0.1.6 fix, gastwerk review item 22) is part of
 * that same self-contained set - restores the UA `dialog:modal { margin:
 * auto }` centring `layers/reset.css`'s `* { margin: 0 }` zeroes, same
 * as dialog.css's own fix and lightbox.css's pre-existing one; a
 * confirm() dialog opened top-left otherwise, same as `<c-gw.dialog>`
 * did before that fix.
 */

dialog[data-gw-confirm-dialog] {
  margin: auto;
  padding: var(--space-lg);
  border: none;
  border-radius: var(--radius-lg);
  background: var(--color-surface);
  color: var(--color-text);
  box-shadow: var(--shadow-lg);
  width: min(24rem, 92vw);
}

dialog[data-gw-confirm-dialog]::backdrop {
  background: var(--color-scrim);
}

[data-gw-confirm-message] {
  margin: 0 0 var(--space-lg);
  font-size: var(--font-size-base);
}

[data-gw-confirm-actions] {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-sm);
}

[data-gw-confirm-cancel],
[data-gw-confirm-accept] {
  min-height: var(--target-size-min);
  padding: var(--space-xs) var(--space-md);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: var(--font-size-sm);
  font-weight: 600;
  cursor: pointer;
}

[data-gw-confirm-cancel]:hover,
[data-gw-confirm-accept]:hover {
  background: var(--color-surface-hover);
}

[data-gw-confirm-accept] {
  border-color: transparent;
  background: var(--color-danger);
  color: var(--color-danger-contrast);
}

[data-gw-confirm-accept]:hover {
  background: var(--color-danger-hover);
}

/*
 * Menu — @layer components (menu.css). Styles cotton/gw/menu.html's
 * trigger button and its Popover-API panel. See that template's own
 * header comment for why the panel carries no role="menu"/"menuitem"
 * (no arrow-key roving focus is implemented - it is a disclosure list of
 * ordinary links/buttons).
 *
 * Positioning: no `anchor-name`/`position-anchor` is declared anywhere,
 * on purpose - a `popovertarget` invoker/popover pair already gets an
 * *implicit* anchor reference from the Popover API itself (the CSS
 * Anchor Positioning spec: an element that opens a popover via
 * `popovertarget` implicitly anchors that popover), so `position-area`
 * below resolves relative to the trigger with no per-instance unique
 * anchor name needed - which also means no inline `style=` is needed to
 * assign one (forbidden anyway, see docs/javascript.md).
 *
 * Fallback (`@supports not (position-area: block-end)` - Firefox, only
 * partial anchor-positioning support as of this writing): the Popover
 * API's own UA default already applies (`position: fixed; inset: 0;
 * margin: auto`, i.e. centered over the viewport) since nothing here
 * overrides it in that branch - a deliberate, honest "fixed/centered"
 * fallback rather than an approximated "absolute below trigger" that
 * would need the very positioning info anchor support provides.
 */

.menu-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: var(--target-size-min);
  padding: var(--space-xs) var(--space-md);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  font-size: var(--font-size-sm);
  font-weight: 600;
  cursor: pointer;
}

.menu-trigger:hover {
  background: var(--color-surface-hover);
}

.menu-trigger::after {
  content: "▾";
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

.menu-panel {
  margin: 0;
  padding: var(--space-xs);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
  box-shadow: var(--shadow-md);
  min-width: 12rem;
  max-width: min(20rem, 92vw);
}

.menu-panel::backdrop {
  background: transparent;
}

@supports (position-area: block-end) {
  .menu-panel:popover-open {
    position-area: block-end span-inline-end;
    margin-top: var(--space-xs);
  }
}

.menu-panel > a,
.menu-panel > button,
.menu-panel > form > button {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: var(--target-size-min);
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-sm);
  background: none;
  border: none;
  color: var(--color-text);
  font: inherit;
  font-size: var(--font-size-sm);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.menu-panel > a:hover,
.menu-panel > button:hover,
.menu-panel > form > button:hover {
  background: var(--color-surface-hover);
}

/*
 * Lightbox — @layer components (lightbox.css). Styles the single native
 * <dialog> lightbox.js builds lazily on first use (docs/javascript.md,
 * lightbox.js) - like confirm.js, there is no cotton template for the
 * dialog itself (<c-gw.gallery>, gallery.css, only styles the thumbnail
 * grid that opens it). This file only has to match the exact attributes
 * that script creates:
 *
 *   <dialog data-gw-lightbox-dialog>
 *     <button data-gw-lightbox-close aria-label="Close">×</button>
 *     <button data-gw-lightbox-prev aria-label="Previous image"></button>
 *     <img data-gw-lightbox-image>
 *     <button data-gw-lightbox-next aria-label="Next image"></button>
 *     <p data-gw-lightbox-caption></p>
 *   </dialog>
 *
 * Deviation from mietwerk/gastwerk's `.lightbox-overlay`/`.lightbox-content`/
 * `.lightbox-close`/`.lightbox-title`: that markup was a manually toggled
 * `.is-open` div, replaced package-wide by a native <dialog> (see
 * lightbox.js's own header comment) - this file styles the new
 * `data-gw-lightbox-*` attributes lightbox.js actually creates, not the
 * old class names, which no longer exist in the generated markup. The
 * visual language (near-black backdrop, centered image, floating
 * close/prev/next controls) is intentionally kept close to mietwerk's,
 * including mietwerk's always-dark chrome (its `.lightbox-title` was a
 * literal `color: #fff`, independent of page theme, since it always sits
 * over a dark backdrop) - reproduced here token-only via `color-scheme:
 * dark` on the dialog itself: every `light-dark()` token referenced
 * inside it (docs/css-architecture.md's theming model) then resolves to
 * its dark value regardless of the page's own light/dark state, with no
 * literal colour needed (tests/test_css_token_audit.py forbids one here).
 */

dialog[data-gw-lightbox-dialog] {
  color-scheme: dark;
  position: relative;
  margin: auto;
  padding: 0;
  border: none;
  background: none;
  color: var(--color-text);
  max-width: 92vw;
  max-height: 92vh;
  overflow: visible;
}

dialog[data-gw-lightbox-dialog]::backdrop {
  background: var(--color-scrim);
}

[data-gw-lightbox-image] {
  display: block;
  max-width: 92vw;
  max-height: 82vh;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  margin-inline: auto;
}

[data-gw-lightbox-caption] {
  margin-top: var(--space-sm);
  text-align: center;
  color: var(--color-text);
  font-size: var(--font-size-sm);
}

[data-gw-lightbox-caption]:empty {
  display: none;
}

[data-gw-lightbox-close],
[data-gw-lightbox-prev],
[data-gw-lightbox-next] {
  position: absolute;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  min-width: var(--target-size-min);
  min-height: var(--target-size-min);
  border: none;
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  color: var(--color-text);
  box-shadow: var(--shadow-md);
  font-size: var(--font-size-lg);
  line-height: 1;
  cursor: pointer;
}

[data-gw-lightbox-close]:hover,
[data-gw-lightbox-prev]:hover,
[data-gw-lightbox-next]:hover {
  background: var(--color-surface-hover);
}

[data-gw-lightbox-close] {
  top: -1rem;
  right: -1rem;
}

[data-gw-lightbox-prev] {
  top: 50%;
  left: -1.25rem;
  translate: 0 -50%;
}

[data-gw-lightbox-next] {
  top: 50%;
  right: -1.25rem;
  translate: 0 -50%;
}

/*
 * Gallery — @layer components (gallery.css). Styles cotton/gw/gallery.html's
 * thumbnail grid (ported from gastwerk's `.gallery` admin document/photo
 * grid - docs/css-architecture.md's inventory table).
 *
 * Deviation from gastwerk: gastwerk's thumbnails were `<button
 * class="thumb-button">` with the image URL read from `data-src` by its
 * own lightbox script on click. lightbox.js's new contract
 * (docs/javascript.md) puts `data-gw-lightbox` directly on the `<a>`
 * itself instead (so the thumbnail is a real, working link to the full
 * image with JS disabled) - there is no separate `.thumb-button` element
 * in the new markup, so this file styles `.gallery a` directly rather
 * than porting the `.thumb-button` class name.
 */

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
  gap: var(--space-sm);
}

.gallery li {
  list-style: none;
}

.gallery a {
  display: block;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  cursor: zoom-in;
}

.gallery a:hover {
  border-color: var(--color-border-strong);
}

.gallery img {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
}

/*
 * Toast — @layer components (toast.css). Styles cotton/gw/toast_region.html's
 * `[data-gw-toast-region]` and every `[data-gw-toast]` inside it, whether
 * server-rendered (Django messages) or built at runtime by toast.js for
 * the htmx `gw:toast` path (docs/javascript.md) - both produce identical
 * markup, so one set of rules covers both.
 *
 * Deviation from mietwerk/zeitwerk/gastwerk's `ul.messages`/`li.success`
 * etc: that markup sat inline at the top of <main>, in the normal content
 * flow. toast.js's contract (docs/javascript.md) is a `[data-gw-toast]`
 * element inside one page-level `[data-gw-toast-region]`, not a `<ul>`
 * list item, and toasts now also arrive at any time via htmx (not just on
 * page load) - so this file positions the region as a fixed corner stack
 * (the conventional "toast" placement) instead of porting the old
 * inline-banner layout; the per-level colour treatment (background/
 * border/text per success/warning/danger/info) is kept from mietwerk.
 *
 * `[data-gw-toast-dismissing]` is set by toast.js right before removing a
 * toast (docs/javascript.md, toast.js) - the transition below is what its
 * `transitionend` listener waits for. Under `prefers-reduced-motion:
 * reduce`, layers/reset.css already forces every transition-duration to
 * ~0 (its own blanket rule), so no separate guard is needed here; toast.js
 * additionally skips waiting for the (now-instant) transition entirely in
 * that case, belt-and-braces against any browser quirk around a
 * near-zero-duration `transitionend`.
 */

[data-gw-toast-region] {
  position: fixed;
  top: var(--space-lg);
  right: var(--space-lg);
  z-index: var(--z-overlay);
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  width: min(22rem, calc(100vw - 2 * var(--space-lg)));
  pointer-events: none;
}

[data-gw-toast] {
  pointer-events: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-md);
  font-size: var(--font-size-sm);
  transition: opacity var(--transition), transform var(--transition);
}

[data-gw-toast-dismissing] {
  opacity: 0;
  transform: translateY(-0.5rem);
}

[data-gw-toast-message] {
  margin: 0;
}

[data-gw-toast-close] {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--target-size-min);
  height: var(--target-size-min);
  border-radius: var(--radius-sm);
  background: none;
  color: inherit;
  opacity: 0.65;
  font-size: var(--font-size-base);
  line-height: 1;
  cursor: pointer;
}

[data-gw-toast-close]:hover {
  background: color-mix(in oklch, currentColor 12%, transparent);
  opacity: 1;
}

[data-gw-toast][data-gw-toast-level="success"] {
  background: var(--color-success-bg);
  border-color: var(--color-success-border);
  color: var(--color-success);
}

[data-gw-toast][data-gw-toast-level="error"] {
  background: var(--color-danger-bg);
  border-color: var(--color-danger-border);
  color: var(--color-danger);
}

[data-gw-toast][data-gw-toast-level="warning"] {
  background: var(--color-warning-bg);
  border-color: var(--color-warning-border);
  color: var(--color-warning-text);
}

[data-gw-toast][data-gw-toast-level="info"] {
  background: var(--color-info-bg);
  border-color: var(--color-info-border);
  color: var(--color-info-text);
}

@media (max-width: 30rem) {
  [data-gw-toast-region] {
    top: var(--space-md);
    right: var(--space-md);
    left: var(--space-md);
    width: auto;
  }
}


/* Forms */
/*
 * Form fields — @layer components (form-field.css). Per
 * docs/css-architecture.md's component inventory, this ONE file covers
 * every form-field-adjacent class family ported from the forks:
 * `.form-field`/`.has-error` wrapper state, `.help-text`/`.helptext`/
 * `.errorlist`/`.errornote`, `.checkbox-grid`, `.date-quick`/
 * `.date-quick-button`, `.select-filter` - plus the new
 * `[data-gw-field-checkbox]` layout switch for <c-gw.field>'s lone-
 * checkbox case. The bare element defaults (input/select/textarea
 * borders, focus ring, `<label>`, `<fieldset>`/`<legend>`) already live
 * in layers/base.css - this file only adds the wrapper/state/helper
 * classes around them.
 *
 * Deviation - `.date-quick`/`.select-filter`: gastwerk's date_quick.js/
 * select_filter.js built a `<span class="date-quick">`/`<input
 * class="select-filter">`. The rewritten date-quick.js/select-filter.js
 * (docs/javascript.md) create `[data-gw-date-quick-bar]`/
 * `[data-gw-date-quick-button]`/`[data-gw-select-filter-input]` instead -
 * no class at all - so this file styles those attributes, not the old
 * class names, which the new markup never produces. Target file stays
 * form-field.css per the inventory; only the selectors changed to match
 * the actual JS contract this package ships (per the task brief: "your
 * markup must match what the JS expects").
 */

/* --------------------------------------------------------- Field wrapper */
.form-field {
  margin-bottom: var(--space-md);
}

.form-field:last-child {
  margin-bottom: 0;
}

/* layers/base.css's bare `label` default is `display: inline-block` with
   no bottom margin - fine for the lone-checkbox case just below (label
   sits beside its input), but a regular text/select/textarea field needs
   its label on its own line, with breathing room before the control
   (found sitting flush against the input on staging, no vertical gap at
   all). The checkbox/radio variant (`[data-gw-field-checkbox] label`,
   `.checkbox-grid label` above) both already set their own
   `margin-bottom: 0` and stay flex/inline, unaffected by this. */
.form-field > label {
  display: block;
  margin-bottom: var(--space-xs);
}

/* Lone checkbox (<c-gw.field> puts the label after the widget for this
   case - see gw_is_checkbox in templatetags/gw_forms.py): box and label
   sit side by side instead of stacked, help text/errors wrap to their
   own line below both. */
.form-field[data-gw-field-checkbox] {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
}

.form-field[data-gw-field-checkbox] label {
  font-weight: 400;
  margin-bottom: 0;
}

.form-field[data-gw-field-checkbox] .help-text,
.form-field[data-gw-field-checkbox] .helptext,
.form-field[data-gw-field-checkbox] .errorlist {
  flex-basis: 100%;
}

.has-error input,
.has-error select,
.has-error textarea {
  border-color: var(--color-danger);
}

.has-error input:focus,
.has-error select:focus,
.has-error textarea:focus {
  border-color: var(--color-danger);
  box-shadow: var(--focus-ring-danger);
}

/* ------------------------------------------------------- Help/error text
   Django's default widget templates render help text as
   <span class="helptext"> (no hyphen); both spellings are targeted so a
   help text rendered outside <c-gw.field>'s own markup (which always
   uses "help-text") is never silently unstyled. */
.help-text,
.helptext {
  color: var(--color-muted);
  font-size: var(--font-size-xs);
  margin-top: var(--space-xs);
}

.errorlist {
  list-style: none;
  color: var(--color-danger);
  padding: 0;
  margin-top: var(--space-xs);
  font-size: var(--font-size-xs);
  font-weight: 500;
}

/* A form-independent error message as its own paragraph, e.g.
   <p class="errornote">This invitation link has expired.</p> - used by
   account flows (2FA, invitation, password reset) outside any <form>
   field. */
.errornote {
  margin: 0 0 var(--space-md);
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-md);
  background: var(--color-danger-bg);
  border: 1px solid var(--color-danger-border);
  color: var(--color-danger);
  font-size: var(--font-size-sm);
}

/* ------------------------------------------------------------ Checkbox grid
   Django's CheckboxSelectMultiple renders <div class="checkbox-grid">
   <div><label><input type="checkbox"> Text</label></div>...</div> - see
   django/forms/templates/django/forms/widgets/multiple_input.html.
   auto-fill instead of a fixed column count: the number of choices
   differs per form, the grid adapts to whatever width is available. */
.checkbox-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
  gap: var(--space-xs) var(--space-md);
}

.checkbox-grid label {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: var(--target-size-min);
  font-weight: 400;
  margin-bottom: 0;
}

/* --------------------------------------------------------------- Date quick
   Inserted by date-quick.js right after the <input type="date">
   (docs/javascript.md) - never present in a template's own markup. */
[data-gw-date-quick-bar] {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: var(--space-xs);
}

[data-gw-date-quick-button] {
  min-height: var(--target-size-min);
  padding: 0.15rem var(--space-sm);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-muted);
  font: inherit;
  font-size: var(--font-size-xs);
  cursor: pointer;
}

[data-gw-date-quick-button]:hover {
  background: var(--color-surface-alt);
  color: var(--color-text);
  border-color: var(--color-border-strong);
}

/* ------------------------------------------------------------ Select filter
   Inserted by select-filter.js right before the <select>
   (docs/javascript.md) - never present in a template's own markup. */
[data-gw-select-filter-input] {
  display: block;
  width: 100%;
  max-width: 32rem;
  margin-bottom: var(--space-xs);
}

/*
 * Django's own default form rendering — @layer components.
 *
 * A project that renders a form with `{{ form }}` (Django 6.1's default
 * renderer is `django/forms/div.html`), `{{ form.as_p }}` or
 * `{{ form.as_div }}` gets NONE of `<c-gw.field>`'s markup
 * (`.form-field`/`.help-text`/`.errorlist` wrapper structure,
 * docs/components.md) - just a bare `<label>` next to its control inside
 * a `<p>`/`<div>` row. `layers/base.css` sets `label { display:
 * inline-block }` (so a lone checkbox/radio label sits next to its input
 * on the same line, the shape `.form-field[data-gw-field-checkbox]`
 * wants) and every text-like control is `width: 100%` - fine on their
 * own, but together on a Django-default row the control starts
 * immediately after the label ON THE SAME LINE, at whatever x-position
 * that label's text happens to end - a different x per row depending on
 * each label's length, then wraps unpredictably. gastwerk's own
 * booking-create form (rendered via `form.as_p`) is the reference case -
 * see the task's screenshot: every row's input starts at a different
 * horizontal offset, and help text keeps running into it. Found across
 * every project that hand-renders a form instead of going through
 * `<c-gw.form>`/`<c-gw.field>` — which most projects still do for a
 * quick admin-style CRUD form.
 *
 * Fixed centrally, here, without touching any project template: every
 * row Django's own `p.html`/`div.html` (and `field.html`'s
 * `as_field_group`, which both `{{ form }}`/`.as_div` and the `fieldset`
 * case go through) can produce becomes a one-column flex stack -
 * label/legend on top, control below at full width (up to the existing
 * `max-width: 32rem` from layers/base.css), help text and errors below
 * that - using `order` rather than re-parenting anything, since the two
 * renderers disagree on DOM order (`as_p`'s `<span class="helptext">`
 * comes AFTER the control; `as_div`/`field.html`'s `<div class=
 * "helptext">` and the field's own `<ul class="errorlist">` both come
 * BEFORE it - see django/forms/templates/django/forms/{p,div,field}.html
 * in the installed Django). `order` normalises both into the same
 * visual stack: label → control → help text → errors, matching
 * `<c-gw.field>`'s own DOM order (templates/cotton/gw/field.html).
 *
 * ONE exception `order` cannot fix: `as_p` prints a field's own
 * `<ul class="errorlist">` as a SIBLING immediately BEFORE its `<p>` row,
 * not as a descendant of it (`p.html`: `{{ errors }}` is emitted, then
 * the `<p>` opens - the two are never in the same box `order` could
 * reshuffle without flexing the whole `<form>`, which this file
 * deliberately does not do - a `<form>` can hold bu, fieldsets, hidden
 * inputs and non-field errors that must not be forced into this layout).
 * Left as Django places it - directly above the row it flags, already
 * styled by the shared `.errorlist` rule below (form-field.css: red, no
 * bullets) so it still reads unambiguously as that field's error, just
 * above instead of below. `as_div`/bare `{{ form }}`/`field.html`'s
 * fieldset case do NOT have this limitation (their errorlist is a real
 * descendant of the row) and get the full label → control → help →
 * errors order.
 *
 * Selectors are deliberately scoped OFF of every markup shape this
 * package already owns the layout for, so this file only ever touches
 * Django's own unstyled default output:
 *   - `:not(.form-field, ...)`/`:not(:is(.form-field, ...) *)` — never a
 *     `<c-gw.field>`/`<c-gw.date-field>`/`<c-gw.select-filter>` row (all
 *     three render `.form-field`) or anything inside a `.filter-bar`
 *     (`<c-gw.filter-bar>`), `.inline-form` or `.toolbar` container
 *     (layers/components/filter.css/toolbar.css) - those are compact,
 *     intentionally inline forms this file must leave untouched.
 *   - `:not(:has(> .form-field))` — never `<c-gw.fieldset>`'s own
 *     `<fieldset><legend>...</legend>{{ slot }}</fieldset>` wrapper
 *     around a set of `<c-gw.field>`s, which also has a `> legend` child
 *     and would otherwise match the row selector below by accident.
 *   - a lone checkbox row (`:has(> input[type="checkbox"])`) is excluded
 *     from the label-on-top rule entirely and gets its own rule instead
 *     (below) - side by side, matching `.form-field[data-gw-field-
 *     checkbox]`'s convention (checkbox first, label after, help/errors
 *     wrap to their own line).
 *
 * `<c-gw.error-summary>`'s own `.error-summary` div has no `<label>`
 * child at all, so it never matches either rule here.
 */

/* ---------------------------------------------------- Label-above-control
   Matches a Django-default field row: `<p>`/`<div>` (as_p/as_div/bare
   `{{ form }}`) or `<fieldset>` (field.html's `use_fieldset` case -
   RadioSelect/CheckboxSelectMultiple/MultiWidget/SelectDateWidget) with
   a `<label>` or `<legend>` as a direct child - every shape EXCEPT a
   lone checkbox row and this package's own `.form-field`-based markup
   (see the guard clause in the header comment above). */
:where(form) :where(p, div, fieldset):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> label, > legend):not(:has(> .form-field, > input[type="checkbox"])) {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  /* Same rhythm between rows as `.form-field` (form-field.css). */
  margin: 0 0 var(--space-md);
}

:where(form) :where(p, div, fieldset):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> label, > legend):not(:has(> .form-field, > input[type="checkbox"])) > label,
:where(form) :where(p, div, fieldset):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> label, > legend):not(:has(> .form-field, > input[type="checkbox"])) > legend {
  order: 1;
}

/* The control itself: a plain input/select/textarea, OR the wrapper
   `<div id="...">` Django's `multiple_input.html` (RadioSelect/
   CheckboxSelectMultiple) renders when `as_p` skips the `<fieldset>`
   (p.html never checks `field.use_fieldset`, unlike div.html/
   field.html) - excluded from "help text"/"errors" by class so it is
   never confused with either. date-quick.js/select-filter.js insert
   their own bar/input as a sibling of the control they augment
   (docs/javascript.md) - ordered alongside it so they stay directly
   under it rather than sorting before the label (their default `order`
   would otherwise be 0, ahead of the label's 1). */
:where(form) :where(p, div, fieldset):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> label, > legend):not(:has(> .form-field, > input[type="checkbox"]))
  > :is(input, select, textarea),
:where(form) :where(p, div, fieldset):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> label, > legend):not(:has(> .form-field, > input[type="checkbox"]))
  > div:not(.helptext):not(.errorlist),
:where(form) :where(p, div, fieldset):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> label, > legend):not(:has(> .form-field, > input[type="checkbox"]))
  > [data-gw-date-quick-bar],
:where(form) :where(p, div, fieldset):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> label, > legend):not(:has(> .form-field, > input[type="checkbox"]))
  > [data-gw-select-filter-input] {
  order: 2;
}

/* Help text/errors: Django's own `.helptext`/`.errorlist` are already
   styled (colour, size, no bullets) by layers/components/form-field.css
   - only spacing is adjusted here, since `gap` above already spaces
   every row child and the shared rule's own `margin-top` would double
   up. */
:where(form) :where(p, div, fieldset):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> label, > legend):not(:has(> .form-field, > input[type="checkbox"])) > .helptext,
:where(form) :where(p, div, fieldset):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> label, > legend):not(:has(> .form-field, > input[type="checkbox"])) > .help-text {
  order: 3;
  margin-top: 0;
}

:where(form) :where(p, div, fieldset):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> label, > legend):not(:has(> .form-field, > input[type="checkbox"])) > .errorlist {
  order: 4;
  margin-top: 0;
}

/* ----------------------------------------------- Multi-choice readability
   RadioSelect/CheckboxSelectMultiple's own `multiple_input.html` renders
   `<div id="..."><div><label><input>Option</label></div>...</div>` -
   the same shape `.checkbox-grid` (form-field.css) already styles, but
   without that class (Django never adds it). Applied to the control div
   identified above, narrowed with `:has()` so it only ever matches an
   actual radio/checkbox option group, never an unrelated wrapper div
   (e.g. a project's own custom widget). */
:where(form) :where(p, div, fieldset):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> label, > legend):not(:has(> .form-field, > input[type="checkbox"]))
  > div:not(.helptext):not(.errorlist):has(input[type="radio"], input[type="checkbox"]) {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
  gap: var(--space-xs) var(--space-md);
}

:where(form) :where(p, div, fieldset):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> label, > legend):not(:has(> .form-field, > input[type="checkbox"]))
  > div:not(.helptext):not(.errorlist):has(input[type="radio"], input[type="checkbox"]) label {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: var(--target-size-min);
  font-weight: 400;
  margin-bottom: 0;
}

/* ------------------------------------------------------------ Lone checkbox
   A BooleanField's `CheckboxInput` row (never `use_fieldset` - see
   `grundwerk_ui.templatetags.gw_forms.gw_is_checkbox`, the same test
   `<c-gw.field>` uses to pick its own lone-checkbox layout) - box and
   label side by side instead of stacked, matching `.form-field
   [data-gw-field-checkbox]` (form-field.css) exactly: checkbox first,
   label after, help text/errors wrap onto their own full-width line. */
:where(form) :where(p, div):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> input[type="checkbox"]) {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
  margin: 0 0 var(--space-md);
}

:where(form) :where(p, div):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> input[type="checkbox"]) > input[type="checkbox"] {
  order: 1;
}

:where(form) :where(p, div):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> input[type="checkbox"]) > label {
  order: 2;
  margin: 0;
  font-weight: 400;
}

:where(form) :where(p, div):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> input[type="checkbox"]) > .helptext,
:where(form) :where(p, div):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> input[type="checkbox"]) > .help-text,
:where(form) :where(p, div):not(
    .form-field,
    .filter-bar,
    .inline-form,
    .toolbar,
    :is(.filter-bar, .inline-form, .toolbar, .form-field) *
  ):has(> input[type="checkbox"]) > .errorlist {
  flex-basis: 100%;
  order: 3;
  margin-top: 0;
}

/*
 * Error summary — @layer components (error-summary.css). Styles
 * cotton/gw/error_summary.html (WCAG 3.3.1 error summary, new in
 * grundwerk-ui - no fork had this pattern, see docs/css-architecture.md's
 * inventory notes on <c-gw.form>). Visually close to `.errornote`
 * (form-field.css) - same danger palette - but a block with its own
 * heading and a list of links, not a single line of text.
 */

.error-summary {
  margin: 0 0 var(--space-lg);
  padding: var(--space-md) var(--space-lg);
  border: 1px solid var(--color-danger-border);
  border-radius: var(--radius-md);
  background: var(--color-danger-bg);
}

.error-summary-heading {
  margin: 0 0 var(--space-sm);
  color: var(--color-danger);
  font-size: var(--font-size-base);
}

.error-summary-heading:focus-visible {
  outline: 2px solid var(--focus-ring-color);
  outline-offset: 2px;
}

.error-summary ul {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.error-summary li {
  font-size: var(--font-size-sm);
  color: var(--color-text);
}

.error-summary a {
  color: var(--color-danger);
  font-weight: 600;
  text-decoration: underline;
}

.error-summary a:hover {
  color: var(--color-danger-hover);
}

/*
 * Fieldset — @layer components (fieldset.css). Refines the bare
 * <fieldset>/<legend> defaults already in layers/base.css for
 * cotton/gw/fieldset.html, once fieldsets are actually stacked/nested in
 * a real form (ported from gastwerk's base.css, the fork with the most
 * fieldset use - e.g. cms/admin/block_form.html, one <fieldset> per
 * block/traveller).
 */

fieldset {
  margin: 0 0 var(--space-lg);
  /* Firefox's UA default gives <fieldset> a `min-width: min-content`
     floor, which can force it wider than a narrower container (e.g. a
     .card) allows and break that container's own layout - removed the
     same way gastwerk's base.css does. */
  min-width: 0;
}

fieldset > *:last-child {
  margin-bottom: 0;
}

/* A <fieldset> nested inside another one keeps its own border but takes a
   smaller outer margin than a top-level fieldset. */
fieldset fieldset {
  margin-bottom: var(--space-md);
}

legend {
  color: var(--color-muted);
}

/*
 * Form actions — @layer components (form-actions.css). Styles
 * cotton/gw/form_actions.html - new in grundwerk-ui (the forks reused
 * their generic `.page-header-actions` bar inside a form instead of
 * having a dedicated form-actions component; proposal 4.3 lists "form
 * actions bar" as its own v1 component).
 */

.form-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-md);
  margin-top: var(--space-lg);
}

/* Opt-in (`sticky` attribute on <c-gw.form-actions>): on a narrow screen
   the bar sticks to the bottom of the viewport instead of scrolling away
   at the end of a long form, so the primary action stays reachable. Same
   48rem breakpoint layers/layout.css already uses for the mobile nav
   collapse. */
@media (max-width: 48rem) {
  .form-actions[data-gw-sticky] {
    position: sticky;
    bottom: 0;
    z-index: var(--z-sticky);
    margin-top: 0;
    margin-inline: calc(var(--space-md) * -1);
    padding: var(--space-md);
    background: var(--color-surface);
    border-top: 1px solid var(--color-border);
  }
}

/*
 * Wizard steps — @layer components (wizard.css). Styles
 * cotton/gw/wizard_steps.html (`.wizard-steps`/`.wizard-step-active`,
 * ported unchanged from mietwerk/zeitwerk - e.g. zeitwerk's
 * aidaimport/templates/aidaimport/import_upload.html). Purely a
 * horizontal progress list; see that template's own header comment for
 * why it is an <ol> here rather than mietwerk's <ul>.
 */

.wizard-steps {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  margin: 0 0 var(--space-lg);
  padding: 0 0 var(--space-sm);
  border-bottom: 1px solid var(--color-border);
  font-size: var(--font-size-sm);
  color: var(--color-muted);
}

.wizard-step-active {
  color: var(--color-primary);
  font-weight: 600;
}


}

@layer utilities {
/*
 * Utilities — @layer utilities (see ../grundwerk.css for the layer
 * order; this file is imported into that layer, it does not wrap its
 * own rules in `@layer utilities { ... }`). Highest-priority layer, so
 * every rule here wins over base/layout/components.
 *
 * Proposal section 8.1's compromise for the "no Tailwind" decision: a
 * small, Tailwind-named-like set of single-purpose helper classes for
 * one-off layout needs a component doesn't already cover - spacing and
 * simple flex helpers (the building blocks an ad hoc `.stack`/`.cluster`
 * would otherwise need, see layers/layout.css for the dedicated
 * primitives), text and visibility. Kept intentionally small (~50 rules,
 * well under the "about a dozen ad hoc utilities" review trigger from
 * the proposal) - if a project keeps reaching past what is here, that is
 * the signal to revisit 8.1, not to grow this file unboundedly.
 *
 * Every value is a token - no literal spacing/colour number, same rule
 * as every other layer (docs/css-architecture.md, "components use
 * tokens only").
 */

/* ------------------------------------------------------------- Spacing */
.mt-0 {
  margin-top: 0;
}

.mt-xs {
  margin-top: var(--space-xs);
}

.mt-sm {
  margin-top: var(--space-sm);
}

.mt-md {
  margin-top: var(--space-md);
}

.mt-lg {
  margin-top: var(--space-lg);
}

.mt-xl {
  margin-top: var(--space-xl);
}

.mb-0 {
  margin-bottom: 0;
}

.mb-xs {
  margin-bottom: var(--space-xs);
}

.mb-sm {
  margin-bottom: var(--space-sm);
}

.mb-md {
  margin-bottom: var(--space-md);
}

.mb-lg {
  margin-bottom: var(--space-lg);
}

.mb-xl {
  margin-bottom: var(--space-xl);
}

.mx-auto {
  margin-inline: auto;
}

.p-0 {
  padding: 0;
}

.p-sm {
  padding: var(--space-sm);
}

.p-md {
  padding: var(--space-md);
}

.p-lg {
  padding: var(--space-lg);
}

.gap-xs {
  gap: var(--space-xs);
}

.gap-sm {
  gap: var(--space-sm);
}

.gap-md {
  gap: var(--space-md);
}

.gap-lg {
  gap: var(--space-lg);
}

/* ------------------------------------------------------- Flex/grid one-offs
   Build an ad hoc stack/cluster without reaching for the dedicated
   `.stack`/`.cluster` primitives in layers/layout.css. */
.flex {
  display: flex;
}

.flex-col {
  display: flex;
  flex-direction: column;
}

.flex-wrap {
  flex-wrap: wrap;
}

.items-center {
  align-items: center;
}

.justify-between {
  justify-content: space-between;
}

.justify-end {
  justify-content: flex-end;
}

.grid {
  display: grid;
}

/* ------------------------------------------------------------------ Text */
.text-muted {
  color: var(--color-muted);
}

.text-xs {
  font-size: var(--font-size-xs);
}

.text-sm {
  font-size: var(--font-size-sm);
}

.text-lg {
  font-size: var(--font-size-lg);
}

.text-center {
  text-align: center;
}

.text-right {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.font-medium {
  font-weight: 500;
}

.font-semibold {
  font-weight: 600;
}

.nowrap {
  white-space: nowrap;
}

.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ------------------------------------------------------------ Visibility */
.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;
}

.hidden {
  display: none;
}

@media (max-width: 48rem) {
  .hidden-mobile {
    display: none;
  }
}

@media (min-width: 48.01rem) {
  .hidden-desktop {
    display: none;
  }
}

}
