/* ==========================================================================
   Edilis — interface skeleton (BL-12)
   --------------------------------------------------------------------------
   Builds exclusively on the tokens from tokens.css. No colour, spacing, radius or
   font size is hard-coded here — the rule from docs/design-system.md §1, guarded by
   tests/web/test_kostra.py::test_vlastne_styly_nepisu_hodnoty_natvrdo. The few px
   lengths that remain are graphic dimensions the design system writes down itself
   (a hairline, the 8px dot, the 32px click target); the guard lists every one of
   them with its reason, so a new number cannot slip in unnoticed.
   The components and their behaviour are described in docs/design-system.md §4;
   the screen layouts in docs/design.md.
   ========================================================================== */

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

html {
  color-scheme: light dark;
  /* A jump to an anchor lands *below* the sticky header, never behind it (BL-136).
     `.header` is `position: sticky; top: 0` and taller than a `.panel-header`, so a
     target scrolled to the top of the viewport ends up covered together with the
     heading the anchor names — the reader arrives with no confirmation of where they
     are. The offset is the header plus the gap every other sticky element leaves
     (`.calculator-summary`), taken from the tokens rather than measured. It sits on
     the document and not on the two zones of the calculator: `#content` of the skip
     link is scrolled to the same way, and a rule per target would be a list nobody
     extends. */
  scroll-padding-top: calc(var(--h-header) + var(--s-4));
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--t-base);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  /* A word with nowhere to break — an IBAN, a long e-mail, an attachment file name —
     breaks rather than pushing the page sideways (§5). The domain is full of them:
     §3 names IBAN and annex codes as mono values. `break-word` only breaks when the
     word genuinely does not fit, so ordinary prose is untouched. */
  overflow-wrap: break-word;
}

/* State is never colour alone (§6) — hence focus has its own outline, not just a hue. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

a { color: var(--accent); }

/* The control height is a token, not a floor that never binds: --h-control is what
   these actually measure, and on a coarse pointer it grows (§6). The header height
   is derived from it, so both move together. */
button, .btn, select, input[type="submit"] { min-height: var(--h-control); }

@media (prefers-reduced-motion: no-preference) {
  .nav-item, .btn { transition: background-color 120ms ease, color 120ms ease; }
}

/* Skip link (§6) — the first focusable element of the page, parked one header height
   above the viewport until it takes focus. .visually-hidden cannot do this job: it has
   no :focus branch, so the link would stay clipped to 1×1 px exactly when it is needed.
   It sits above the sticky header, which is why its z-index outranks it. */
.skip-link {
  position: absolute;
  top: calc(-1 * var(--h-header));
  left: var(--s-3);
  z-index: 30;
  padding: var(--s-2) var(--s-3);
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow);
}

.skip-link:focus-visible { top: var(--s-2); }

/* The slide is decoration; with reduced motion the link simply appears (§6). */
@media (prefers-reduced-motion: no-preference) {
  .skip-link { transition: top 120ms ease; }
}

/* --- Application layout ---------------------------------------------------
   The header is sticky; below it the sidebar and the content. Under 860 px the
   area collapses into a single column and the navigation turns horizontal (§5). */

/* `minmax(0, 1fr)`, not `1fr`: an auto-sized grid track takes its minimum from the
   content, so one long organization name in the sticky header inflated the page's
   only column past the viewport and every screen scrolled sideways — the ellipsis
   never got a chance, because the column had already grown to fit the untruncated
   string. Flooring the track at 0 lets the header shrink and the text truncate. */
.app {
  min-height: 100vh;
  display: grid;
  grid-template-rows: auto 1fr;
  grid-template-columns: minmax(0, 1fr);
}

/* The header asserts the height every sticky element offsets from, so --h-header
   is true rather than descriptive. It wraps rather than overflows: its three groups
   do not fit a phone on one line, and until BL-66 they simply pushed the page to
   636px and every screen scrolled sideways.
   It wraps in DOM order — no `order`, no grid re-placement. Moving the groups
   visually would leave Tab travelling up and down the header (WCAG 2.4.3). */
.header {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2) var(--s-4);
  min-height: var(--h-header);
  padding: var(--s-3) var(--s-5);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

/* The mark: four drawings picked by size, never one file scaled (§4.3, and the
   rules of the mark in decisions/vizualna-identita-edilis.md). The two custom
   properties are what makes the dark theme and the inverse variant free — they
   take their values from the palette, so a theme that moves the accent moves the
   stonework with it and no second set of files exists.
   Dimensions come in even numbers only: at odd sizes the vault lands on a half
   pixel and the joints smear even where they would otherwise hold. */
.mark {
  --m-stone: var(--accent);
  --m-key: var(--keystone);
  display: block;
  flex: none;
}
.mark-stone { fill: var(--m-stone); }
.mark-key   { fill: var(--m-key); }

/* A seal, a fax, a single-colour print: both properties inherit the text colour,
   so one declaration colours the whole mark. Never below grade S — in XS the
   keystone is separated by colour alone and a single-colour XS loses it. */
.mark--mono { --m-stone: currentColor; --m-key: currentColor; }

.mark--l  { width: 64px; height: 60px; }
.mark--m  { width: 32px; height: 30px; }
.mark--s  { width: 22px; height: 21px; }
.mark--xs { width: 16px; height: 15px; }

/* The lockup of mark and logotype. The clear space is a RULE of the mark, not a
   margin somebody typed into one template: one pillar wide, and the pillar is 18
   of the drawing's 100 units, so it is 0.18 of the mark's own width. */
.lockup {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--s-2) + 2px);
  padding-inline: calc(var(--s-2) + 2px);
}
.lockup--vertical {
  flex-direction: column;
  gap: var(--s-3);
  padding-block: var(--s-3);
}

/* Inter SemiBold with the identity's tracking. The word is small here, and that is
   the whole reason it is not Spectral: at this size the serifs disappear and the
   logotype reads as a defect rather than as a serif. */
.lockup-word {
  font-family: var(--sans);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: var(--ls-wordmark);
  /* Tracking is added AFTER the last letter too, so a centred word sits off-centre
     by half of it unless the indent gives it back. */
  text-indent: var(--ls-wordmark);
  text-transform: uppercase;
  color: var(--ink);
}

/* The large setting and print: Spectral, wider tracking, and here the serifs are
   the point. */
.lockup-word--print {
  font-family: var(--serif);
  font-size: var(--t-md);
  font-weight: 500;
  letter-spacing: var(--ls-wordmark-lg);
  text-indent: var(--ls-wordmark-lg);
}

/* A standalone control of the shell, not a link in a sentence — so it carries the
   click target of §6 rather than the height of its own text (it measured 23px). */
.wordmark {
  display: inline-flex;
  align-items: center;
  min-height: var(--h-control);
  color: var(--ink);
  text-decoration: none;
}

.header-right { margin-left: auto; display: flex; align-items: center; gap: var(--s-3); }

/* Language switcher (UC-69). The two codes sit tight together so they read as one
   control rather than two unrelated buttons; the active one is disabled, which is
   what says "this is the language you are in" without a second label.            */
.language-switcher { display: flex; gap: 0; }
.language-switcher .btn { padding-inline: var(--s-2); }
.language-switcher .btn[aria-current="true"] {
  /* Not --ink-muted: this is small text, which §6 keeps off the muted token. */
  color: var(--ink-soft);
  opacity: 1;
  text-decoration: underline;
}

/* Below the breakpoint the header becomes two rows: identity and the active
   organization on top, the controls beneath them. Nothing is hidden and nothing is
   re-ordered — the switcher stops forcing a wrap (`flex-basis: 0`, so it claims the
   leftover width instead of its full text width) and the controls take a row of
   their own. Reading order and DOM order stay identical, so Tab still walks the
   header top to bottom (§5.1). */
@media (max-width: 860px) {
  .org-switcher { flex: 1 1 0; }
  .header-right { flex-basis: 100%; }
}

/* Collapsible switcher — **one component with two hosts** (design-system.md §4.3).
   Every mechanic lives here; `.org-switcher` in the application header switches the
   context of the whole application, `.calculation-switcher` in the screen header
   switches one record, and each host declares only what differs. Written out twice
   the absolute panel below would drift apart at the first touch — that is how
   `.breakdown` came to be `.meter` under a second name (BL-91).

   `min-width: 0` all the way down is what makes an ellipsis inside actually work: a
   flex item defaults to `min-width: auto`, which for `white-space: nowrap` text is
   the full string, so the name refused to shrink and pushed the header off-screen. */
.switcher { position: relative; min-width: 0; }
/* The layout belongs to the <summary>, never to the <details>: with `display: flex`
   on the parent the panel becomes a flex sibling of the summary and spills sideways. */
.switcher summary { display: flex; align-items: center; gap: var(--s-2); cursor: pointer; list-style: none; min-width: 0; }

/* The switcher's panel overlays the page; it must not push it. In flow it doubled
   the sticky header (86 → 184px on a phone) and shoved every screen down, which
   also made --h-header wrong exactly while the sticky offsets were being read.
   The `position: relative` above was already here and unused — this is its purpose.
   The application header owns a stacking context (z-index 20), so z-index 1 clears
   the content below without competing with the dialog. The screen header owns none,
   and must not be given one: above the dialog the panel would outrank a confirmation. */
.switcher > .panel {
  position: absolute;
  top: calc(100% + var(--s-2));
  left: 0;
  z-index: 1;
  width: max-content;
  max-width: min(22rem, calc(100vw - 2 * var(--s-5)));
}
/* Switching is the panel's whole purpose, so its links are controls and take the
   target of §6 — they measured 22px. */
.switcher > .panel a {
  display: flex;
  align-items: center;
  min-height: var(--h-control);
}

/* Host: the organization switcher — the active organization must be apparent on
   every screen. All it adds is the width of the name (the two-row break under 860px
   is the header's own rule above); the mechanics are `.switcher`. */
.org-active {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
  max-width: 18rem;
}

/* Host: the switcher of saved calculations in the *screen* header (BL-127). Its rows
   carry a calculation's name and the year of its call, so the panel is wider than the
   organization's; the row itself then stacks, which is the second and last difference
   and is reasoned about at the rule below. Everything else is the shared base, which
   is the whole point of having one. It overrides by **order**, not by weight: both
   selectors are two classes, so this rule has to stay below `.switcher > .panel`. */
.calculation-switcher > .panel {
  max-width: min(26rem, calc(100vw - 2 * var(--s-5)));
}
/* And it looks like a control, because on this screen nothing else says it is one
   (#217). In the application header the same component stands alone and its weight is
   enough; in the screen header it stood in a row of pills, and an unframed `<summary>`
   between two labels reads as a third label. The frame is therefore the **host's**, not
   the base's: on `.switcher summary` it would follow into the application header, where
   the organization switcher would suddenly grow a box it never asked for — and there is
   no `.org-switcher` rule to take it back.
   `width: max-content` sits on the `<details>`, not on the `<summary>`: the panel is
   absolutely positioned at `left: 0` of the `<details>`, so a full-width host would
   anchor the panel to the whole column. `max-width: 100%` keeps a long name from
   pushing the header out on a phone; the base's `min-width: 0` lets it shrink. */
.calculation-switcher {
  width: max-content;
  max-width: 100%;
}
.calculation-switcher > summary {
  min-height: var(--h-control);
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--surface);
}
.calculation-switcher > summary:hover { background: var(--surface-2); }
/* A row here carries three data (name, building, year of the call), so it stacks:
   the name on the first line, the `.hint` with the other two under it (BL-137). On one
   line they do not fit under 400px and what an ellipsis would eat is the **name** —
   the only thing telling two calculations of one building apart. `align-items` has to
   be restated: the base centres a one-line row on the cross axis, which in a column is
   the horizontal one, and every row would sit in the middle of the panel. The two
   permanent escapes below the list are single-line and stay where they were. */
.calculation-switcher > .panel a {
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
}

/* Same reason as .app: the content column is floored at 0 so a wide table or a long
   unbroken string cannot push the page past the viewport. Wide content scrolls
   inside its own frame (§5).

   One column by default, two only where the sidebar actually is. The navigation
   exists only inside an organization, but the two-track list used to be stated
   unconditionally — so on every screen outside one (sign-in, the organization
   picker, founding an organization, accepting an invitation) the lone .content
   auto-placed into the *sidebar's* track and the whole screen rendered 208px wide,
   with 160px left after its padding. The login card, the picker's list and the role
   pills broke letter by letter, on a 1440px display (BL-74). Stating the columns on
   the layout that has two of them keeps the fact in one place: the grid follows the
   DOM instead of predicting it. */
.body {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
}
/* The upper bound of the content column (§5.2) sits on **this** layout and not on
   .content inside it; why it is not written there stands at .content below, where the
   question is asked.

   The bounded pair is **not** centered, it is anchored to the left edge, and that
   reverses the conclusion of the BL-81 review (BL-120). With `margin-inline: auto` the
   shell let go of the left edge above the bound and then travelled right with
   every further hundred pixels of window, while the header stayed where it was — it is
   full-bleed on purpose, its background and its bottom rule are the edge of the window,
   not of the content. The interface therefore had two vertical edges, the wordmark's
   and the navigation's, and the one belonging to the navigation moved: the sidebar
   started at 0 on a 1440px display and ~950px in on a 3440px one. Empty space on the
   right is quiet; navigation that stands somewhere else on every monitor is not. The
   bound itself stays — what BL-81 was fixing, a four-field panel read across 3000px, is
   the width and not the position.

   calc() and not a track of `minmax(0, var(--w-content))`: a track with no `fr` sizes to
   its content, so a sparse screen would shrink below the measure instead of holding it.
   Capping the container keeps `1fr` doing the work — at 1440px the container is under
   the cap and the column measures the same 1232px as before, above the cap the shell is
   2000px wide and starts where the narrower window already had it.

   2000px, because 112rem of content is what the widest table of the application needs
   beside its step navigation and its rail; the measurement, column by column, is in
   §5.2 and a guard ties it to the token (#215). */
.body:has(> .sidebar) {
  grid-template-columns: 208px minmax(0, 1fr);
  max-width: calc(208px + var(--w-content));
}
@media (max-width: 860px) { .body:has(> .sidebar) { grid-template-columns: minmax(0, 1fr); } }

/* The gateway — every screen reached without navigation: sign-in, sign-up, password
   reset, signing out, and the four screens outside an organization (picking one,
   founding one, accepting an invitation). Here the content is not a column beside
   navigation, it *is* the screen, so it takes one measure and centers.

   The width sits on the layout, not on a wrapper the template has to remember: the
   card used to be a `.gate` div inside the block, so the picker and the invitation
   never got it, and the flash messages — which base.html includes above the block —
   stayed full-width beside a centered card. Stated here, the messages, the heading
   and the panel share one edge.

   `minmax(0, …)` rather than a flat width so a phone narrower than the measure gets
   the viewport instead of an overflowing track. */
.body:not(:has(> .sidebar)) {
  grid-template-columns: minmax(0, 26rem);
  justify-content: center;
}
/* An entrance screen is short; without the extra air the card sticks to the header. */
.body:not(:has(> .sidebar)) .content { padding-block-start: var(--s-10); }

/* A working screen outside an organization — the platform administrator's overview of
   the use cases (BL-52). Without a sidebar the layout would center it into the 26rem
   gateway measure above and its table would scroll inside a column narrower than its
   own heading. The width belongs to the layout, not to a wrapper the template has to
   remember; the modifier matches the specificity of the gateway rule and wins by
   coming after it. */
.body.body--wide { grid-template-columns: minmax(0, 1fr); justify-content: normal; }
/* The same screen is also the exception to the upper bound of the content column
   (§5.2): the use-case overview, the form registry and the mapping diff are
   administrator tables that want every pixel. It is already the exception to the
   gateway measure above, so being the exception to --w-content too is the consistent
   answer — and since the bound sits on the layout with the sidebar, which this screen
   has none of, the exception is now the absence of a rule rather than an override. */
.body.body--wide .content { padding-block-start: var(--s-6); }

/* The offset is the height of the sticky header — from the token, never a copied
   number, so both move together (design-system.md §5). */
.sidebar {
  position: sticky;
  top: var(--h-header);
  padding: var(--s-4) var(--s-3);
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  border-right: 1px solid var(--border);
  min-height: calc(100vh - var(--h-header));
}
@media (max-width: 860px) {
  .sidebar {
    position: static;
    min-height: 0;
    flex-direction: row;
    overflow-x: auto;
    border-right: 0;
    border-bottom: 1px solid var(--border);
  }
  /* The one place `nowrap` is right: a horizontal strip that scrolls. */
  .sidebar .nav-item { white-space: nowrap; }
}

/* The label wraps; it is never truncated (§8 — PL and HU are longer than SK). Only
   the horizontal scroller below the breakpoint wants `nowrap`, and it has somewhere
   to scroll. Everywhere else — the vertical sidebar, the workspace tabs — a long
   label used to run straight off the screen. */
.nav-item {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  color: var(--ink-soft);
  text-decoration: none;
}
.nav-item:hover { background: var(--surface-2); color: var(--ink); }
.nav-item[aria-current="page"] {
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-weight: 600;
}
/* A module that does not exist yet. It stays in the navigation so that the whole
   information architecture is visible, but it does not pretend to lead anywhere.
   The only remaining --ink-muted paint: --t-base text on --bg, 4.82:1 light and
   5.20:1 dark, so it holds AA and keeps the item visibly quieter (BL-59). */
.nav-item[aria-disabled="true"] {
  color: var(--ink-muted);
  cursor: default;
  pointer-events: none;
}
.nav-item .coming-soon {
  margin-left: auto;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: var(--ls-eyebrow);
  /* Micro text never on --ink-muted (§6); not the accent either — a label on a
     disabled item must not look interactive. */
  color: var(--ink-soft);
}

/* The width of the content column comes from the layout above (.body), never from
   here: the measure is a property of the shell, and .content is the same element on the
   gateway, in the two-column shell and on the wide workspace, each of which wants a
   different one. Until the review of BL-81 the upper bound of §5.2 stood here with a
   margin-inline: auto, and centering a column inside the track left over by the
   sidebar put 960px of nothing between the navigation and the content it belongs to. */
.content {
  padding: var(--s-6) var(--s-6) var(--s-10);
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  min-width: 0;
}

/* --- Workspace -------------------------------------------------------------
   The two-column layout of the building detail and of the application wizard
   (design-system.md §4.3): the sub-navigation of the screen on the left, its
   sections on the right. Both screens used to borrow .body — the application
   shell — and force its columns with an inline style; an inline style outranks
   @media, so under 860 px they stayed two-column and a phone got a 230px column
   next to a sliver of content (BL-56). The width lives here, in the class (§5). */

.workspace {
  display: grid;
  grid-template-columns: 230px 1fr;
  align-items: start;
  gap: var(--s-5);
}

/* Sticky under the application header — the offset is the token, never a copied
   number, so both move together (§5). */
.workspace-nav {
  position: sticky;
  top: var(--h-header);
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
}

/* A named group of the sub-navigation — Evidencia / Agenda (design-system.md §4.3).
   Which section belongs where and in what order is domain data
   (`portfolio.rules.SECTION_GROUPS`); this class only puts the label above its items
   and never decides membership. */
.workspace-nav-group {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
}

/* The label lines up with the *text* of the items below it, not with their box — the
   inline padding is .nav-item's. Micro caps in the accent (§6: --t-micro never on
   --ink-muted), which also says it is not a click target. */
.workspace-nav-group > .eyebrow { padding: 0 var(--s-3); }

/* Groups stand further apart than the items inside them; the spacing is the parent's
   gap, never a margin on a child (§5). Stated through :has() because .workspace-nav is
   shared with the wizard and the voting screen, which have no groups. */
.workspace-nav:has(> .workspace-nav-group) { gap: var(--s-5); }

/* One section = one panel; the column only spaces the panels out and never lets a
   wide table stretch the grid past the screen. */
.workspace-sections {
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  min-width: 0;
}

@media (max-width: 860px) {
  .workspace { grid-template-columns: 1fr; }
  /* The sub-navigation becomes a wrapping row of tabs (§5.1). It must never simply
     disappear the way the prototype hides it — the sections of the building and the
     steps of the wizard would become unreachable on a phone. */
  .workspace-nav {
    position: static;
    flex-direction: row;
    flex-wrap: wrap;
    gap: var(--s-2);
    padding-bottom: var(--s-3);
    border-bottom: 1px solid var(--border);
  }
  /* A grouped sub-navigation keeps its groups here too: what becomes a wrapping row of
     tabs is the GROUP, not the whole navigation. Left as a row, "Evidencia" and "Agenda"
     would each collapse into a narrow strip of stacked tabs and the label would stop
     reading as the heading of the row under it — §5.1 lets the sub-navigation change
     shape, never lose a section or its grouping. */
  .workspace-nav:has(> .workspace-nav-group) { flex-direction: column; gap: var(--s-4); }
  .workspace-nav-group { flex-direction: row; flex-wrap: wrap; align-items: center; }
  /* On the tab row the label is the first item of the line, so it takes the rhythm of
     the tabs beside it instead of the padding of a heading above them. */
  .workspace-nav-group > .eyebrow { padding: 0; }
}

/* --- What is missing before submission --------------------------------------
   The permanent rail of the wizard (design.md §4, rule 2): a third column beside the
   steps and the sections, sticky under the header. It carries the CROSS-CUT — state
   and a way in — never the wording of the messages: a sticky block taller than the
   viewport has a bottom nobody can scroll to (BL-77 hit the same trap in the
   calculator's summary), so the sentences stay in the step where the fault is
   corrected (BL-141 moved them there out of a standing .readiness checklist that
   repeated this very list two columns to the left, on all nine steps). Two readers of
   one gate, with a difference that can be said in one sentence: the rail says WHERE to
   go, the step WHAT exactly is wrong.

   The column is stated only where the panel actually is (§5): .workspace is
   shared with the building's workspace, which has no rail, and an unconditional third
   track would leave an empty column on every section of every building. `:has()` reads
   the DOM that decides, so the condition is not copied into the template. 20rem and
   not a token: it is the dimension of one component, like the 230px of the
   sub-navigation, and a rail of short lines grows with the text.                */
.workspace:has(> .whats-missing) { grid-template-columns: 230px minmax(0, 1fr) 20rem; }

/* Sticky under the application header. The offset is the **same token, unwrapped**, as
   .workspace-nav uses — the two sticky columns of one grid have to lock onto the same
   line, which is why this is not the calc() of .calculator-summary (§5). It rides on
   .panel: the surface, border, radius and column layout are the panel's, only the
   position is the rail's. */
.whats-missing {
  position: sticky;
  top: var(--h-header);
}

/* One item = one thing that is missing: the dot carries the state, the name says what
   it is, the second line says where it is fixed, and the whole row is the way there —
   a two-word link is a target on a hairline (§6). */
.whats-missing-item {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  min-height: var(--h-control);
  padding: var(--s-2) 0;
  border-bottom: 1px solid var(--border);
  color: var(--ink);
  text-decoration: none;
}
.whats-missing-item:last-child { border-bottom: 0; }
/* Only the clickable variant reacts to the mouse: the item of a screen the member may
   not open is not a link (§6, the same rule as .list-row--link). */
a.whats-missing-item:hover { background: var(--surface-2); }
/* The context is a line of its own, not a tail of the name. */
.whats-missing-item .hint { display: block; }

/* §5.1: below 1180 px the rail drops under the content and stops being sticky — a
   block the page has already scrolled past has nothing left to stick to. Its place in
   the grid has to be said: auto-placement would put it in the second row of the
   **navigation's** 230px track and render the panel 230px wide. */
@media (max-width: 1180px) {
  .workspace:has(> .whats-missing) { grid-template-columns: 230px minmax(0, 1fr); }
  .whats-missing { position: static; grid-column: 1 / -1; }
}
/* Under 860 px the whole workspace is one column (§5.1). The rail is **not** hidden
   and never becomes a disclosure: what is missing before submission is the one thing
   that must not need opening. Restated for the `:has()` selector because its
   specificity (0,2,0) outranks the unconditional one-column rule above. */
@media (max-width: 860px) {
  .workspace:has(> .whats-missing) { grid-template-columns: minmax(0, 1fr); }
}

/* --- Calculator plane ------------------------------------------------------
   The informative calculator: the input panels on the left, the summary of the
   result on the right and sticky under the header. It exists because these numbers
   are read *while* they are typed — two panels lower the user corrects a cost
   without seeing what the correction did, which is the blindness the guardrails
   are there to end (design-system.md §4.3). It is not .workspace: that one
   carries the sub-navigation of a screen on the left, here the left column is the
   content itself.                                                            */

.calculator {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 26rem;
  align-items: start;
  gap: var(--s-6);
}
/* `minmax(0, 1fr)` above and `min-width: 0` here are one rule stated twice: a grid
   item's automatic minimum is its content, so the table of purposes would widen the
   column instead of scrolling inside its own frame (§5.1). */
.calculator-inputs { display: flex; flex-direction: column; gap: var(--s-6); min-width: 0; }
/* The save bar of the calculator stands **outside** a panel: what it saves is the whole
   form, not one section of it, and a panel around it would put a fourth, unnumbered
   card into a column of three numbered ones — the contradiction BL-128 removed. Out
   there it has no frame to borrow, so it draws its own: the shorthand replaces the
   top hairline the bar wears as a panel's footer, and the radius closes all four
   corners instead of the bottom two. */
.calculator-inputs > .save-bar {
  border: 1px solid var(--border);
  border-radius: var(--r);
}
.calculator-summary {
  position: sticky;
  top: calc(var(--h-header) + var(--s-4));
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  min-width: 0;
}
/* A guardrail card standing directly in the summary needs a plane of its own: the
   column sits on --bg and .guard is nothing but a left bar. A summary that wraps its
   cards in a .panel is untouched — the cards are then no longer its children. */
.calculator-summary > .guard {
  background: var(--surface);
  border-radius: 0 var(--r) var(--r) 0;
  padding: var(--s-3) var(--s-4);
  box-shadow: var(--shadow);
}
/* The summary drops under the inputs before either column gets too narrow to read —
   26rem of summary beside a five-column table of purposes is the floor. Below the
   break it stops being sticky: a block the page has already scrolled past has
   nothing left to stick to. */
@media (max-width: 1180px) {
  .calculator { grid-template-columns: minmax(0, 1fr); }
  .calculator-summary { position: static; }
}

/* --- Screen typography ----------------------------------------------------- */

.eyebrow {
  font-size: var(--t-micro);
  text-transform: uppercase;
  /* The identity's tracking for a section eyebrow, from tokens — a component that
     writes the number by hand is that value's second home (§1). */
  letter-spacing: var(--ls-eyebrow);
  /* The accent is prescribed by §3: an identity element, and the only shade that
     holds contrast at --t-micro in both themes. */
  color: var(--accent);
}
.page-h {
  font-family: var(--serif);
  font-size: var(--t-lg);
  font-weight: 600;
  margin: 0;
  text-wrap: balance;
}
.section-h {
  font-size: var(--t-md);
  font-weight: 600;
  margin: 0;
  text-wrap: balance;
}
.subtext { color: var(--ink-soft); max-width: 65ch; }

.screen-header { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-5); align-items: baseline; }
/* The screen heading is where the longest names land — a building's, an
   organization's. `overflow-wrap` on its own cannot save them: a flex item's
   automatic minimum is its content, so an unbroken IBAN or e-mail widened the group
   before the text was ever offered a chance to break. */
.screen-header > * { min-width: 0; }

/* --- Panel and form section ------------------------------------------------ */

/* `min-width: 0`: a panel is a flex item, and a flex item's automatic minimum size
   is its content — so a wide table inside stretched the panel past the screen
   instead of scrolling in its own frame. */
.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.panel-header {
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--border);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  align-items: baseline;
}
/* The sentence that says what follows from the panel takes a line of its own: in the
   header's wrap it would otherwise be squeezed between the heading and the pill. It
   keeps the measure of prose (§3) and loses the paragraph's default margin, which the
   header's baseline row does not expect. */
.panel-header .hint { flex-basis: 100%; margin: 0; max-width: 65ch; }
.panel-body { padding: var(--s-5); display: flex; flex-direction: column; gap: var(--s-4); }

/* A **modifier of .panel, never a second component** (§4.1): a destructive panel is a
   panel read differently, not a different anatomy. The border and the header carry
   --bad; the body deliberately does not, because a surface tinted throughout would read
   as an error state of the whole screen — and this is not an error. It is the one place
   on the screen where something irreversible can be done, and it says so before the
   click. The heading pair is the one .pill--bad already uses (--bad on --bad-soft). */
.panel--dangerous { border-color: var(--bad); }
.panel--dangerous > .panel-header {
  border-bottom-color: var(--bad);
  background: var(--bad-soft);
  border-radius: var(--r) var(--r) 0 0;
}
.panel--dangerous > .panel-header .section-h { color: var(--bad); }

/* A panel that folds away — again a **modifier of .panel, never a second component**
   (§4.1): the anatomy is unchanged, `<details>` merely carries it and the header is
   its `<summary>`. Native, so it opens with no JavaScript at all, which is the whole
   reason the reference table of the call may be closed by default.

   It is for a panel the reader typically does **not** read — reference beside work,
   uploaded attachments beside the ones still missing. Never over anything that blocks
   submission: closing a blocker hides it, and design.md §5 forbids a blockage without
   a way out, not merely a blockage.

   The affordance is drawn and not written: a glyph in `content` would be spoken by the
   very screen reader that already announces the open state of <details>. A square with
   two borders turned 45° is the chevron; the open state turns it up rather than
   swapping a second shape in.

   Dropping the native triangle takes **two** declarations, not one, because the engines
   draw it in two different places. Blink and Gecko give the <summary> `display:
   list-item` and the triangle is its `::marker`, so `.panel-header`'s own `display:
   flex` already removes it and `list-style: none` is what says so out loud; WebKit draws
   it as `::-webkit-details-marker`, a pseudo-element child no `display` on the summary
   reaches. */
.panel--collapsible > summary { cursor: pointer; list-style: none; }
.panel--collapsible > summary::-webkit-details-marker { display: none; }
/* The disclosure arrow, drawn rather than written: a glyph in `content` would be read
   out by a screen reader on top of the open/closed state `<details>` announces itself.
   **Two consumers, one drawing** — the collapsible panel and the framed switcher of
   saved calculations (#217). A second copy would drift the first time one of them
   changed its ink. */
.panel--collapsible > summary::after,
.calculation-switcher > summary::after {
  content: "";
  align-self: center;
  margin-left: auto;
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--ink-soft);
  border-bottom: 2px solid var(--ink-soft);
  transform: rotate(45deg);
}
.panel--collapsible[open] > summary::after,
.calculation-switcher[open] > summary::after { transform: rotate(-135deg); }
/* Closed, the header **is** the bottom edge of the panel, and its own hairline right
   above the panel's border read as a double line. */
.panel--collapsible:not([open]) > summary { border-bottom-width: 0; }

/* The ordinal of a panel that answers one question of a series (design-system.md §4.3).
   The number is the **reading order**, not a mandatory sequence — the calculator computes
   from the purposes and the maturity alone. Until BL-128 a sentence above the panels said
   so; now the column says it by its shape: exactly three numbered panels and nothing else,
   the name field riding in the save bar beneath them. The circle claims no state either:
   --accent is brand and order, never a state (§1.1), which is why it needs no second
   modality.

   The digit is --accent-ink and not --accent on this plane: both hold AA (5.00 light /
   4.64 dark for --accent), but 11.5px bold is not where a ratio without reserve is
   trusted (§6), and --accent-ink on --accent-soft is the pair the active navigation item
   already uses (10.45 / 10.26:1).

   min-width and not width: a two-digit ordinal would overflow a fixed circle, and 999px
   turns it into a stadium instead of clipping it. The size is in rem because a badge
   grows with its text — the same reason --w-field-* are (§1.4). */
.step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  min-width: 1.5rem;
  height: 1.5rem;
  padding-inline: var(--s-1);
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-family: var(--mono);
  font-size: var(--t-xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Section save bar — confirms that the section is saved on its own (§4). */
.save-bar {
  padding: var(--s-3) var(--s-5);
  border-top: 1px solid var(--border);
  background: var(--surface-2);
  border-radius: 0 0 var(--r) var(--r);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  align-items: center;
}
/* Small text on a recessed plane: --ink-soft, never --ink-muted (§6). */
.save-bar .cas { font-size: var(--t-xs); color: var(--ink-soft); }
/* Only the offset is contextual here — the group itself is a block of its own. */
.save-bar .actions { margin-left: auto; }
/* A panel merged from two records of one section carries a bar per record (§4.3), and
   a bar that is not the panel's foot must not draw one: the bottom radius is the
   panel's own corner, so mid-panel the --surface of the panel showed through both of
   them and the strip read as the panel ending. The hairline closes it against the
   record that follows. */
.panel > form:not(:last-child) > .save-bar {
  border-radius: 0;
  border-bottom: 1px solid var(--border);
}

.field { display: flex; flex-direction: column; gap: var(--s-1); }
.field > label { font-size: var(--t-sm); color: var(--ink-soft); }
/* A field is a user interface component, so its boundary falls under WCAG 1.4.11
   (3:1) — hence --border-strong, not the decorative --border, plus a plane of its
   own so it never dissolves into the --surface of the panel (BL-59). */
.field input:not([type="checkbox"]):not([type="radio"]), .field select, .field textarea {
  font: inherit;
  color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  padding: var(--s-2) var(--s-3);
  width: 100%;
  /* The click target is asserted here, not left to the padding. The reset above
     floors the button, the select and the submit, so until BL-81 the text field was
     the one control whose height was a coincidence — and a narrowed field (below)
     would have dropped under the target in both dimensions at once (§6). */
  min-height: var(--h-control);
}

/* --- The size of a field -----------------------------------------------------
   The width of a field is part of what it says (§4.3, Velikost pole): an input for two
   digits as wide as one for a 120-character name looks like it expects a sentence.
   Every field used to take the whole 15rem track of .grid, because `width: 100%`
   above was the only statement about width in the system.

   max-width over width:100%, never a fixed width: a ceiling can never make a field
   overflow its track, so a track narrower than the size — a one-column .grid on a
   phone — gets a field the width of the track and not a field sticking out of the
   panel. The narrowing is a visual signal and never a limit on the input either: a
   value longer than expected is still typed and read, the field scrolls within itself.
   The click target does not shrink with the width; --h-control above floors it and §6
   grows it on a coarse pointer. The class rides on the input itself (that is what a looped
   {% for field in form %} can be given), not on the .field wrapper. Inputs in
   .purposes get no size: their width is the table column's and a second bound would
   argue with it. */
.field .field-number   { max-width: var(--w-field-number); }
.field .field-amount { max-width: var(--w-field-amount); }
.field .field-code     { max-width: var(--w-field-code); }

/* --- A value that came from somewhere else -----------------------------------
   The carrier of "derived is not edited" (design.md §4.3). Every field already stands
   on --surface-2 (BL-59), so what says "not yours to correct" is the dashed boundary.
   Only the STYLE of the border changes: a field is still a component boundary under
   WCAG 1.4.11, so it keeps --border-strong and its contrast (§6), and the text keeps
   --ink because the whole point is that it is READ.

   `readonly`, NEVER `disabled` — a disabled field leaves the tab order and a screen
   reader walks past it, so the value the user is meant to read would disappear for
   exactly the user who cannot see it (§6). The attribute is set together with this
   class in web/forms.py (`DerivedField`); `readonly` is advisory, so the server
   ignores what the browser sends back instead of trusting it.

   The selector repeats the shape of the base field rule above on purpose: `:not()`
   carries the specificity of its argument, so that rule counts (0,3,1) and a plain
   `.field .field--derived` (0,2,0) would lose to the `border` shorthand it declares.
   The class rides on the widget, like the sizes above — a form rendered by a loop
   cannot be handed a class per field. A derived `select` or `textarea` does not exist
   yet; the selector grows with the first one instead of guessing now.           */
.field input.field--derived:not([type="checkbox"]):not([type="radio"]) { border-style: dashed; }

/* A checkbox is not a text field. `width: 100%` used to catch these too, so every
   sworn declaration rendered as a 732 × 13px bar — the shape of an input, the target
   of a hairline. The box keeps its intrinsic size and the whole label row becomes the
   target, which is what §6 sizes with --h-control. Clicking the text already toggles
   the box; this only makes the area worth aiming at. */
.field > label:has(> input[type="checkbox"]), .field > label:has(> input[type="radio"]),
.group label:has(> input[type="checkbox"]), .group label:has(> input[type="radio"]),
.purposes th label:has(> input[type="checkbox"]) {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  min-height: var(--h-control);
  cursor: pointer;
}
.field input[type="checkbox"], .field input[type="radio"],
.group input[type="checkbox"], .group input[type="radio"],
.purposes input[type="checkbox"] {
  flex: none;
  accent-color: var(--accent);
}
.hint { font-size: var(--t-xs); color: var(--ink-soft); }
/* An error says what is wrong and what to do (§7) — never just a coloured border. */
.error { font-size: var(--t-xs); color: var(--bad); }
.field input[aria-invalid="true"], .field select[aria-invalid="true"], .field textarea[aria-invalid="true"] { border-color: var(--bad); }

/* A group of controls that answer one question together (a checkbox list). Only a
   <fieldset> carries the group's name to a screen reader (§6) — the individual
   checkboxes have labels of their own, the group has none. The browser's frame,
   margin and padding are not part of the design system, hence the reset; the
   min-inline-size keeps a fieldset from refusing to shrink inside a grid. It is not
   `.field`: that class dresses every input it contains as a text field, which turns a
   checkbox into a full-width box. */
.group {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  border: 0;
  margin: 0;
  padding: 0;
  min-inline-size: 0;
}
/* A group whose options each say something of their own — the three forms of security
   and the attachments each of them adds to the file (BL-146). A modifier and not a
   second component (§4.1): the fieldset, the nested labels and the name from
   aria-labelledby are unchanged, only the distance between the options is. The base
   gap is a hairline because the base group is a column of single-line checkboxes; an
   option two lines tall needs the gap to separate options rather than the lines
   inside one, otherwise the sentence of the first choice reads as the second's. */
.group--explained { gap: var(--s-3); }
/* The option and the sentence about it are a column with a `gap` (§5), never a
   paragraph carrying the browser's margin: `.hint` sets the size of the type and
   nothing else, so without this the distance between a choice and its price came from
   the UA stylesheet and stood outside the --s-* scale. */
.group--explained > div { display: flex; flex-direction: column; gap: var(--s-1); }
.group--explained > div > p { margin: 0; }

/* `min(15rem, 100%)` and not a bare `15rem`: a track floor wider than the box it sits
   in overflows it, and at 320px the box really is narrower — 320 less the padding of
   .content, the panel border and the padding of .panel-body leaves about 230px for a
   240px floor, so the single column stuck out and the **page** scrolled sideways,
   which §5 forbids. `min()` lets the last column give up its floor instead of the
   viewport giving up its width; above the floor nothing changes, so the number of
   columns is still the browser's answer and never a fixed count (§4.3). Not a fourth
   breakpoint (§5.2) — the same rule holds at every width. */
.grid { display: grid; gap: var(--s-4); grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr)); }

/* --- List of rows inside a panel -------------------------------------------
   A pending invitation, a choice of organization. These rows used to wear
   .nav-item, which belongs to the sidebar: it lit up under the mouse where
   nothing was clickable (§6), muted the very text that mattered and never wrapped,
   so a long e-mail overflowed the panel instead of breaking (§4.1).            */

.list { display: flex; flex-direction: column; gap: var(--s-1); }
/* The row keeps the plane of the panel: the neutral pill is drawn on --surface-2 and
   would dissolve into a row painted with the same shade. */
/* Wrapping, because a control in the row is not text and does not shrink: a file input
   beside a name and a sentence overflowed the panel instead of moving to a second line
   (BL-148, the Prílohy step). Text alone never reaches the break — `overflow-wrap`
   above lets it shrink first — so the older rows keep the single line they had. */
.list-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  color: var(--ink);
  overflow-wrap: anywhere;
}
/* Whatever the row says about itself is pushed away from the text: in a row of text
   only, that puts the pill on the right edge. In a row that carries a control the
   auto margin moves the pill **and everything after it** — the pill then sits just
   before the control and the right edge is held by the control itself, which is what
   the Prílohy step looks like (BL-148). Both are the same rule: the pill leaves the
   sentence, it does not claim the edge. */
.list-row .pill { margin-left: auto; }
/* The clickable variant — and only it reacts to the mouse. */
.list-row--link { text-decoration: none; }
.list-row--link:hover { background: var(--surface-2); }
/* The register variant: caption, sentence, control — one shape for every property of
   one record (BL-139). A grid and not the flex of the base row, because "one shape"
   is a claim about alignment **across siblings**: every .list-row is its own
   container, so a `max-content` caption comes out a different width in each row and
   the three rows drift apart again — which is the sequence of shapes this replaced.
   Only a fixed track shares geometry between siblings. Declared after --link on
   purpose: same specificity, so order decides (the reason written down at
   .guard--ok/--bad). The two never meet — --link makes the whole row an anchor, and a
   <select> or a <button> inside an anchor is a nested interactive element (§6), not a
   variant. */
.list-row--register {
  display: grid;
  grid-template-columns: minmax(0, 9rem) minmax(0, 1fr) minmax(0, auto);
  gap: var(--s-2) var(--s-4);
  align-items: center;
}
/* One column below the narrow break (§5.2): three tracks on a phone leave the
   sentence a word wide. */
@media (max-width: 860px) { .list-row--register { grid-template-columns: minmax(0, 1fr); } }

/* --- Buttons --------------------------------------------------------------- */

/* Group of buttons in a row. A block of its own: styled only inside .save-bar
   and the confirmation dialog, the same class did nothing anywhere else — and a
   screen that borrowed it got no layout at all (§4.1, §9). */
.actions { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.actions--right { justify-content: flex-end; }

/* One input and the single action that operates on it, on one line (#218). It is not
   `.actions` — that is a group of buttons and says nothing about a field — and not
   `.save-bar`, which would claim the section saves itself. The bound on the field
   block is what keeps the pair readable: the field carries its own `.hint` in the
   column below the control, and at full panel width that sentence would run to the
   far edge while the control sat at 14rem. The action centres against the block for
   the same reason — the block is taller than the control it belongs to, and a top or
   bottom alignment would put the button beside the label or beside the hint instead
   of beside the input. Under 400px the pair stacks, which is the only sane end. */
.field-action {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3) var(--s-4);
}
.field-action > .field { flex: 0 1 28rem; }

.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-4);
  border-radius: var(--r-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  cursor: pointer;
  text-decoration: none;
}
.btn:hover { background: var(--surface-2); }
.btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--surface);
  font-weight: 600;
}
.btn--primary:hover { background: var(--accent-strong); }
.btn--quiet { border-color: transparent; background: transparent; color: var(--ink-soft); }
.btn--quiet:hover { background: var(--surface-2); color: var(--ink); }
/* The button of an irreversible action: outlined at rest and filled on hover — the shape
   .btn--primary has, in the state colour instead of the accent. A destructive action must
   never sit in --accent, which everywhere else in the product means "the ordinary way
   on" (§1.1); and it is not filled at rest either, because the loudest thing on a panel
   should not be the thing nobody normally wants. */
.btn--destructive { border-color: var(--bad); color: var(--bad); }
.btn--destructive:hover { background: var(--bad); border-color: var(--bad); color: var(--surface); }
.btn[disabled], .btn[aria-disabled="true"] { opacity: .55; cursor: not-allowed; }

/* --- State elements -------------------------------------------------------
   A semantic colour is not the accent (§1) and state is never carried by colour
   alone (§6): the dot has text next to it, the pill carries a label.        */

.dot {
  width: 8px; height: 8px; border-radius: 50%;
  display: inline-block; flex: none;
  background: var(--border-strong);
}
.dot--ok   { background: var(--ok); }
.dot--warn { background: var(--warn); }
.dot--bad  { background: var(--bad); }
.dot--info { background: var(--info); }

.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  /* --t-xs is exactly this size; writing the number again only hid it from the
     scale (§1.2). The vertical padding goes on the 4px grid for the same reason (§1.3). */
  font-size: var(--t-xs);
  font-weight: 700;
  padding: var(--s-1) var(--s-2);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--ink-soft);
  /* A pill wraps rather than overflows. It carries a state name or a role, and those
     grow in translation (§8) — `nowrap` pushed a long one 495px wide on a phone.
     `max-width` keeps it inside its parent even as one unbroken word. */
  max-width: 100%;
  /* Between words, never inside one. `.list-row` sets `overflow-wrap: anywhere`
     so a long unbroken organization name cannot blow the row, and the pill inherited
     it — which made its own min-content one character, so flex shrank it first and
     the role read "Administráto / r" while the name beside it kept a whole line.
     Only the row's free text needs breaking anywhere; a label of a closed vocabulary
     is what should push back (BL-74). */
  overflow-wrap: break-word;
}
.pill--ok   { background: var(--ok-soft);   color: var(--ok); }
.pill--warn { background: var(--warn-soft); color: var(--warn); }
.pill--bad  { background: var(--bad-soft);  color: var(--bad); }
.pill--info { background: var(--info-soft); color: var(--info); }

/* Operation messages. A confirmation says what happened (§7). */
.messages { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-2); }
.message {
  display: flex; align-items: flex-start; gap: var(--s-2);
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-sm);
  border-left: 4px solid var(--border-strong);
  background: var(--surface-2);
}
.message--success { border-left-color: var(--ok);   background: var(--ok-soft); }
.message--warning { border-left-color: var(--warn); background: var(--warn-soft); }
.message--error   { border-left-color: var(--bad);  background: var(--bad-soft); }
.message--info    { border-left-color: var(--info); background: var(--info-soft); }

/* Readiness checklist — a standing list of what is still missing. Deliberately not
   a .message: a message is feedback after an action, this state was there before the
   user arrived, and painting it as a warning cries wolf on every visit (§4.3).
   The state of an item is carried by the dot next to the text, never by colour
   alone (§6). */
.readiness { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.readiness-item {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) 0;
  border-bottom: 1px solid var(--border);
}
.readiness-item:last-child { border-bottom: 0; }
/* What exactly is wrong is a line of its own, not a tail of the name: the item carries a
   whole sentence, and a sentence beside the name would squeeze it (BL-95). Consumers are
   the building and the organization profile — the application wizard dropped this list in
   BL-141, where each sentence moved into the step it is corrected in. */
.readiness-item .subtext { display: block; }

/* Head of the checklist — one number over the track (BL-123). The list alone does not
   say how far the whole is; the head does, and its denominator is never the count of
   sections on the screen: on the building it is the "Evidencia" group of the
   sub-navigation, in the wizard's rail the thirteen sections of the submission gate
   (§4.3). It is **not** `.meter`: a meter is a track with a limit marker and there is no
   limit here, the whole track is the group — which is also why the track is composed
   from `.bar` and these rules declare no geometry of their own.

   The track is the third child and takes the whole next line, the same flex-basis idiom
   `.panel-header .hint` uses. Since BL-141 it stands **inside** `.whats-missing` as well:
   the rail used to say the same ratio as a percentage in a pill, and one screen writes a
   ratio one way. It stays the part that gives way if the rail ever outgrows the viewport —
   a sticky block with a bottom nobody can scroll to would lose links, not a headline. */
.readiness-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-1) var(--s-4);
}
.readiness-head .bar { flex-basis: 100%; }
/* The number is the headline of the panel, so it takes the section-heading step (§1.2) —
   readable at a glance, not a display number that would cost a row of its own. */
.readiness-head .mono { font-size: var(--t-md); }

/* On the building every item is the way to its own fix, so the whole row is the target
   and not the two words of the name (§6). Written as a descendant of the item rather
   than on it: the organization profile renders this same list with **no** link — the way
   in there is the form below it — and it may not grow a 40px row for a target it does
   not have. */
.readiness-item > a {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex: 1;
  min-width: 0;
  min-height: var(--h-control);
  color: inherit;
  text-decoration: none;
}
/* With the whole row a target, the padding belongs to the target: a hover surface that
   stops short of the hairline above and below reads as a row that is broken in half. */
.readiness-item:has(> a) { padding: 0; }
.readiness-item > a:hover { background: var(--surface-2); }

/* --- Data table -----------------------------------------------------------
   Every number is mono + tabular-nums and right-aligned (§3). Wide content
   scrolls inside its own frame, the page never horizontally (§5).          */

/* The frame only ever scrolls horizontally: it has no height limit, so nothing
   scrolls inside it vertically and a sticky thead would have nothing to stick to —
   the head would stay a promise the layout cannot keep (BL-64). A table long enough
   to need one is a pagination problem first; only then a frame of its own. */
/* `min-width: 0` is what makes the horizontal scroll actually happen: without it the
   frame is a flex item sized to the table's own min-content, so it grew to the width
   of the widest row and pushed the page sideways instead of scrolling inside itself.
   The members table did exactly that on every phone width. */
.table-frame { overflow-x: auto; min-width: 0; border: 1px solid var(--border); border-radius: var(--r); }
table.data { border-collapse: collapse; width: 100%; background: var(--surface); }
table.data thead th {
  background: var(--surface-2);
  text-align: left;
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: .09em;
  font-weight: 600;
  color: var(--ink-soft);
  padding: var(--s-2) var(--s-3);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
table.data tbody td { padding: var(--s-2) var(--s-3); border-bottom: 1px solid var(--border); }
table.data tbody tr:last-child td { border-bottom: 0; }
table.data tfoot td {
  padding: var(--s-2) var(--s-3);
  border-top: 1px solid var(--border-strong);
  background: var(--surface-2);
  font-weight: 600;
}
table.data .number, table.data th.number {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}
table.data tr.notice td { background: var(--warn-soft); }
/* The row the others are read against — the maturity actually chosen in the
   sensitivity table (§4.3). The state lives in `aria-current`, so the style is read
   from it: a second class beside the attribute is two homes for one fact and they
   part ways. The row carries a text anchor as well; the tint alone would say
   "current" only to those who see it (§6). */
table.data tbody tr[aria-current="true"] > * { background: var(--accent-soft); }
table.data tbody tr[aria-current="true"] > *:first-child {
  box-shadow: inset 4px 0 0 var(--accent);
}

/* The purposes of the call, grouped by the cap they share (design-system.md §4.3).
   The grouping is data of the call, so nothing here may assume how many groups
   arrive or which one comes first — one <tbody> per group, whatever they are. */
.purposes th, .purposes td { vertical-align: top; }
/* The name of the purpose is the row's header, and a header inside the body is
   styled nowhere else in the file — it would otherwise be bold and unpadded. */
.purposes tbody th {
  padding: var(--s-2) var(--s-3);
  border-bottom: 1px solid var(--border);
  text-align: left;
  font-weight: 400;
}
.purposes tbody tr:last-child th { border-bottom: 0; }
/* The row that opens a group: a colgroup header, not data. Recessed plane plus
   accent is the pair .eyebrow uses, so the table reads as named sections. */
.purposes-header th {
  background: var(--surface-2);
  padding: var(--s-2) var(--s-3);
  font-size: var(--t-xs);
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--accent);
}
/* The sentence that says what holds the group together is prose again, not a label. */
.purposes-header .hint { display: block; text-transform: none; letter-spacing: normal; }
/* The narrow columns wrap, they never truncate (§7): a longer language mutation has
   to stay readable rather than end in an ellipsis. The head is restated with the
   table's own class because `table.data thead th` is the more specific selector and
   would keep its `nowrap`. */
table.data.purposes thead th { white-space: normal; }
/* The cost and the count are typed inside the cell, so they wear the field skin of
   §4 without a .field wrapper — a bare browser input in a designed table reads as a
   defect, and the numeric columns want the mono of §3. The vertical padding is one
   step smaller so the row stays a table row, but the target is not: --h-control is
   the floor §6 sets, and these are the most used inputs on the screen — beside a
   checkbox that already measures 40/44px they must not be the smallest target in
   the application. */
.purposes input[type="number"] {
  min-height: var(--h-control);
  font: inherit;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  padding: var(--s-1) var(--s-2);
  text-align: right;
  width: 100%;
  max-width: 100%;
}
.purposes input[aria-invalid="true"] { border-color: var(--bad); }
/* The message belongs under its input, not beside it — the cell is already narrow. */
.purposes .error { display: block; }

/* Findings inside a table cell — a list, not prose, because each one is its own
   link to the step that fixes it.                                              */
.list-findings { margin: 0; padding-left: var(--s-4); display: grid; gap: var(--s-1); }

.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* --- KPI tile ---------------------------------------------------------------
   One number that **measures** something and has no ceiling. A number *against*
   a ceiling is `.meter` (a track with a marker) and a number that **guards**
   something is `.guard`; if the difference cannot be said in one sentence, the
   three belong merged rather than multiplied (design-system.md §4.3).

   The tile used to live under the meter's name, which is why it got written
   twice — the dashboard as `a.meter`, /platforma/rozpracovanost/ as a plain
   `<div class="meter">` — and the second one rendered its number at body size.
   One name, one component (BL-90); the single home of the markup is
   `web/komponenty/kpi.html`.                                                  */

.kpi { display: flex; flex-direction: column; gap: var(--s-1); }

/* A tile that is also the way into what it counts (BL-31). The whole tile is
   the target, not just the number — a 2-digit link is a hairline to hit (§6).
   `color: inherit` is not cosmetic: the global `a` rule would otherwise repaint
   the caption in the accent.                                                  */
a.kpi {
  color: inherit;
  text-decoration: none;
  padding: var(--s-3);
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--surface);
}
a.kpi:hover { border-color: var(--border-strong); }
/* The value the tile exists for, at the same size as .guard's. It hangs on the
   element and not on a descendant `.mono` for the reason `.guard > .mono` is
   scoped to a direct child: a `.mono` nested in the caption must not inflate. */
.kpi-value { font-size: var(--t-xl); }
.kpi-label { font-size: var(--t-xs); color: var(--ink-soft); }

/* A module that does not exist yet: named, never shown as a zero. Quieter than a
   live tile and deliberately not a link — there is nowhere to go. `--ink-soft` and
   not `--ink-muted`: the sentence inside is small text, which §6 keeps off the
   muted token (BL-59).                                                          */
.kpi--unavailable {
  padding: var(--s-3);
  border: 1px dashed var(--border);
  border-radius: var(--r);
  color: var(--ink-soft);
}

/* --- Track of one whole, optionally in two named parts ---------------------
   Extracted from `.meter` and `.breakdown`, which held the same four rules under
   two names (BL-89 recorded the duplication, BL-91 removes it). The track knows
   no domain object: the caller passes the widths, the state and the captions.

   Two segments are drawn as **two widths, not a width and an offset**: `--druhy`
   carries the whole filled length and the first segment lies on top of it, so
   only the part beyond the first one shows. Independently rounded percentages
   would otherwise leave a hairline of track between the parts. **Two parts is
   the limit** — three is a stacked chart and belongs to `.chart` (§4.4).      */

.bar {
  position: relative;
  height: 10px;
  background: var(--surface-3);
  border-radius: 999px;
  /* The limit marker overhangs the track; clipping would hide the one thing the
     bar is about (§4.3). */
  overflow: visible;
}
.bar-fill {
  position: absolute; inset: 0 auto 0 0;
  background: var(--ok);
  border-radius: 999px;
}
.bar-fill--warn { background: var(--warn); }
.bar-fill--bad  { background: var(--bad); }
/* The second part is never a second state token: both parts are equally
   legitimate, so it takes `--info` — derived by the system, a neutral fact
   (§1.1). Its width is the whole of the fill, not the width of the second part;
   the first segment lies **on** it. */
.bar-fill--second { background: var(--info); }
/* The boundary between the parts needs a line of its own. `--info` against `--ok`
   is 1.23:1 in the light theme and **1.01:1 in the dark one** — two adjacent fills
   of one track would read as a single bar. The hairline is the colour of the track,
   which every fill clears 4.13:1 against, and it is a shadow rather than a border
   so it costs no width (tests/web/test_kontrast_tokenov.py). The selector is why
   the DOM order is load-bearing: `--druhy` first, the state segment on top. */
.bar-fill--second + .bar-fill { box-shadow: 1px 0 0 0 var(--surface-3); }
/* `left` is inline, computed from the data — the marker is at the end of the track
   for a plan's limit and at 85 % for the repayment rule. It is painted after the
   fills in the DOM, so a full track never buries it. */
.bar-marker {
  position: absolute; top: -3px; bottom: -3px;
  width: 2px;
  background: var(--ink);
}
/* The parts named. Two shades with no legend are colour without meaning (§6), so a
   divided track always carries this row. It wraps: a purpose's row may name the
   part, the remainder and the whole, and the cell it sits in is narrow. */
.bar-parts {
  display: flex; justify-content: space-between; gap: var(--s-2);
  flex-wrap: wrap;
  font-size: var(--t-xs); color: var(--ink-soft);
}

/* --- Meter with a limit marker --------------------------------------------
   A guardrail per §5 of design.md: a bare "compliant" is not enough, it must
   be visible where the limit is and how much remains. The component knows no
   domain object: the seats of a plan put the marker at the end of the track,
   the 85 % repayment rule puts it partway along it, and both are the same
   meter — the caller passes the position, the state and the sentence.

   A value with **no** limit is not a meter: `components/meter.html` hands that
   branch to `kpi.html`, so `.meter` always draws a `.bar` with `-ryska`. That
   is why it needs no rule for the size of its value — the number in `--t-xl` is
   the tile's, and this component's number lives in the scale.

   The track itself is `.bar` (BL-91). What is left here is the wrapper and its
   caption rows: `-skala` in mono/`--t-micro` for the two ends of the track, and
   `-popis` for the sentence; the names of a divided track are `.bar-parts`. */

.meter { display: flex; flex-direction: column; gap: var(--s-1); }

.meter-scale {
  display: flex; justify-content: space-between;
  /* The two ends of the scale never run into each other: the right one may be a
     whole phrase ("1 200 € tvorby"), not just the bare limit of a plan. */
  gap: var(--s-2);
  /* Numbers, not an identity element — --ink-soft, so the scale does not read as
     clickable; --ink-muted is out at --t-micro (§6). */
  font-size: var(--t-micro); color: var(--ink-soft);
  font-family: var(--mono);
}
.meter-label { font-size: var(--t-xs); color: var(--ink-soft); }

/* --- Cost against the legal cap -------------------------------------------
   One bar per row of the calculation: the acquisition cost drawn against the
   cap of the act, the cap itself a marker on the track. The denominator is
   shared by the whole table, because two bars of equal length have to mean
   equal money (design-system.md §4.3) — and that shared denominator is this
   component's own rule, computed in the view.

   After BL-91 this is a wrapper and nothing else: the track is `.bar` and the
   captions under it are `.bar-parts`. Its four geometry rules used to be
   `.meter`'s copied under a second name.                                    */

.breakdown { display: flex; flex-direction: column; gap: var(--s-1); min-width: 0; }

/* --- Empty and error state -------------------------------------------------- */

.state-block {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-3);
  padding: var(--s-8) var(--s-6);
  background: var(--surface);
  border: 1px dashed var(--border-strong);
  border-radius: var(--r);
}
.state-block h2 { margin: 0; font-size: var(--t-md); font-weight: 600; }
.state-block p { margin: 0; color: var(--ink-soft); max-width: 60ch; }
.state-block--error { border-style: solid; border-color: var(--bad); background: var(--bad-soft); }

/* --- Confirmation dialog --------------------------------------------------
   A native <dialog>: focus and Esc are handled by the browser, which is more
   reliable than a hand-rolled focus trap.                                   */

dialog.confirmation {
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--surface);
  color: var(--ink);
  padding: 0;
  max-width: 32rem;
  box-shadow: var(--shadow-lg);
}
/* The scrim has a token of its own — the dark theme needs a denser one, which a
   literal colour here could never express (tokens.css, §1.4). */
dialog.confirmation::backdrop { background: var(--overlay); }
dialog.confirmation .dialog-body { padding: var(--s-5); display: flex; flex-direction: column; gap: var(--s-3); }

/* --- HTMX ----------------------------------------------------------------
   The conventions are described in docs/decisions/fe-pipeline-a-htmx.md. The
   indicator is the only thing HTMX needs from the styles, and it is the loading
   state of design-system.md §10 — it stays defined even while the library itself
   is not loaded, so the first fragment finds the state ready.               */

.htmx-indicator { opacity: 0; }
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { opacity: 1; }

/* `left: 0` is not cosmetic. Without it the box keeps its static position, and with
   no positioned ancestor that position is measured against the page — so a label
   hidden inside a horizontally scrolled table sat at x=436 and stretched the
   document to 437px on every phone width, while the table itself scrolled correctly.
   Pinning it costs nothing: a screen reader reads it from the DOM, not from where
   the box lands. */
.visually-hidden {
  position: absolute; left: 0; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Guardrail card — a left bar in the state colour, label → value → derivation
   (design-system.md §4). */
.guard {
  border-left: 4px solid var(--border-strong);
  padding: var(--s-2) var(--s-4);
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
}
/* The value the card exists for (design-system.md §4.3: label → value in mono at
   --t-xl → derivation). Scoped to a direct child so the mono spans inside a nested
   .derivation keep the smaller size of the derivation — the origin of a number must
   not compete with the number. */
.guard > .mono { font-size: var(--t-xl); }
/* The state of the card is its bar, not a pill dropped inside it: the calculator's
   summary carried it in `.pill--bad`, so the state was told by a nested element
   instead of by the component that is supposed to carry it. The word stays — the
   bar is the second modality, never the replacement of the sentence (§6).
   Specificity is 0,1,0, the same as `.guard`, so these have to stand *after* the
   base declaration. `--warn` is deliberately absent: "needs attention" has no
   consumer, and a modifier without one is another dead `.dot--gap` (§4.3). */
.guard--ok  { border-left-color: var(--ok); }
.guard--bad { border-left-color: var(--bad); }
/* The way out of the state the card reports (BL-136). Where the number is a problem, the
   card carries one anchor into the zone that resolves it — the way out itself is three
   sentences long and stays there, never in a sticky column. The anchor is a control and
   takes the target of §6, exactly as `.switcher > .panel a` does; its colour it already
   has from `a`. No class of its own: `.guard` is a column with a gap and lays the link
   out unchanged, so a class would be bought for nothing but a name. */
.guard > a {
  display: flex;
  align-items: center;
  min-height: var(--h-control);
}

/* Where a number came from (design-system.md §4.3). The intermediate steps are read
   downwards as one sentence, not as a table: columns would promise a comparison
   between the steps that makes no sense. It stands inside a .guard or right under
   one — the card carries the number, this carries its origin. */
.derivation {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  padding: var(--s-3);
  background: var(--surface-2);
  border-radius: var(--r);
  font-size: var(--t-sm);
  min-width: 0;
}
.derivation-row { display: flex; justify-content: space-between; gap: var(--s-3); color: var(--ink-soft); }
/* The step that actually bound the result. The accent is emphasis; what says it is
   the word „← obmedzujúce" in the label, because colour never carries a state on
   its own (§6). */
.derivation-row--binding { color: var(--accent); font-weight: 600; }
/* What follows from the steps above — hence a rule over it rather than a gap. */
.derivation-result { color: var(--ink); border-top: 1px solid var(--border); padding-top: var(--s-1); }

/* --- Import preview ---------------------------------------------------------
   What an import would change, shown before anything is written (UC-28,
   design-system.md §4.3). Three parts read left to right as one sentence: the value
   the register holds, the value the file carries, and what the difference means.

   Colour never carries the state on its own (§6): the old value is struck through,
   the new one is named by a word in the row's own label, and `--ok` / `--info` are
   emphasis on top of that. A struck-through value keeps `--ink-muted` rather than a
   state colour — it is the value that is *going away*, and painting it red would read
   as an error where nothing is wrong.                                            */

.change-preview { display: flex; flex-direction: column; gap: var(--s-1); min-width: 0; }
.change-preview-old {
  color: var(--ink-muted);
  text-decoration: line-through;
}
.change-preview-new { color: var(--ink); font-weight: 600; }
.change-preview-diff { color: var(--ink-soft); font-size: var(--t-sm); }
/* The difference is a value the import will write. */
.change-preview-diff--ok { color: var(--ok); }
/* …and this one it will not: an area kept because a better source already filled it,
   or an owner the cadastre stated and this file must not touch. The sentence beside it
   says which — the colour only tells the eye where to look. */
.change-preview-diff--info { color: var(--info); }

/* --- Key–value summary of one run ------------------------------------------
   Metadata of a single run or record — commit, branch, count, duration
   (design-system.md §4.3). Not `.guard`: that carries **one** number that guards
   something, this carries several that guard nothing. And not `table.data`: the
   row is the only one there is, so columns would promise a comparison between runs
   that this is not.                                                            */

.summary { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-6); }
.summary > div { display: flex; flex-direction: column; gap: var(--s-1); min-width: 0; }
/* The key is the small-caps micro-caption `.eyebrow` carries elsewhere; the value is
   mono, because every one of them is an identifier or a number. */
.summary .mono { font-size: var(--t-base); overflow-wrap: anywhere; }

/* --- Photos attached to a fault report -------------------------------------
   At most three (`maintenance.photos.MAX_PHOTOS`), so a wrapping row and not
   `.grid`: three auto-fit tracks would stretch a single photo across the panel and
   make one report look like a gallery. Each thumbnail links to the full photo,
   because the small one answers "is there water" and the full one answers "where
   is it coming from".

   `aspect-ratio` with `object-fit: cover` on purpose: photos arrive portrait and
   landscape from the same entrance, and a row of mixed heights reads as damage to
   the layout rather than as evidence. The intrinsic size travels in the HTML
   `width`/`height` attributes (the row stores them), so the box is reserved before
   the image arrives — geometry from data, the §4.4 exception, and the only thing
   that keeps the panel from jumping when three photos load.                     */

.photo-strip { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.photo-strip img {
  display: block;
  width: 12rem;
  max-width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  /* The plate under a photo that has not loaded — and under a transparent PNG that
     was flattened onto white by the re-encode. */
  background: var(--surface-2);
}

/* --- Grid of panels ---------------------------------------------------------
   Two panels that are **read as a pair** (coverage against the floor × the same
   coverage against the size of the module). Not `.grid` — that hands tracks to
   fields inside a panel, this lays panels out on the screen. The breakpoint is
   1080 px from §5.1, not a fourth number of its own.                           */

.panel-grid { display: grid; gap: var(--s-4); align-items: start; }
@media (min-width: 1080px) {
  .panel-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The ratio belongs to a modifier, never to an inline style (§5). */
  .panel-grid--two { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); }
}

/* --- Chart ------------------------------------------------------------------
   Inline SVG over tokens, never `<canvas>` and without a library
   (design-system.md §4.4): the theme switches on a `data-theme` attribute and
   `var()` inside SVG repaints itself, whereas a canvas would have to listen on a
   `MutationObserver` and redraw. A library would bring its own palette and with it
   a second home for colour.

   The ink of the chart does **not** come from the state scale: the grid and the
   axis have a pair of tokens of their own, derived from the neutrals, because a
   guide line carrying a hue reads as data. The mark is `--accent`; `--bad` on it
   means what it means everywhere else (§1.1).

   **The user units of the viewBox are CSS pixels**, which is what lets the captions
   inside the SVG take a `--t-*` token instead of a number picked to look right at
   one size. A viewBox of 0..100 would have forced a hand-tuned `font-size`, i.e. a
   value with a second home (§1).

   Geometry is computed in the **view**, not here and not in the template: the
   shared denominator is a property of the whole set, so two bubbles of the same
   size have to mean the same thing.                                            */

.chart { display: block; width: 100%; height: auto; }
.chart-grid { stroke: var(--chart-grid); stroke-width: 1; }
.chart-axis { stroke: var(--chart-axis); stroke-width: 1.5; }
.chart-label { fill: var(--ink-soft); font-size: var(--t-micro); font-family: var(--mono); }
.chart-value { fill: var(--ink); font-size: var(--t-xs); font-family: var(--mono); }
.chart-bubble { fill: var(--accent); fill-opacity: .55; stroke: var(--accent); stroke-width: 1; }
/* A module below the risk line. The hatch is the second modality — `--warn` and
   `--bad` are 1,5 ΔE apart for a deuteranope, so a colour on its own would say
   nothing to them (§6). The legend carries the same fill *and* the same hatch. */
.chart-bubble--risk { fill: var(--bad); stroke: var(--bad); fill-opacity: .5; }

/* --- Legend -----------------------------------------------------------------
   Explains the colours and the textures of a chart, of a tile grid and of the
   state dots alike — one component, not three. A legend that leaves out the hatch
   contradicts the second modality; a legend hidden on a phone leaves the colours
   without meaning, so it never hides under a breakpoint (§6).                  */

.legend {
  display: flex; flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  font-size: var(--t-xs); color: var(--ink-soft);
}
.legend-item { display: flex; align-items: center; gap: var(--s-2); }
/* The swatch is the 8px of the state dot (§4.3), not a size of its own: both are
   "a sample of a colour beside a word", and a second number would be a second home. */
.legend-item i {
  width: 8px; height: 8px; flex: none;
  border-radius: var(--r-sm);
  background: var(--accent); opacity: .55;
  border: 1px solid var(--accent);
}
.legend-item--risk i {
  background: var(--bad); opacity: .5; border-color: var(--bad);
  background-image: repeating-linear-gradient(45deg, transparent 0 2px, var(--surface) 2px 4px);
}
.legend-item--exception i {
  background: var(--surface-2); border-style: dashed; border-color: var(--border-strong); opacity: 1;
}

/* --- Tile grid --------------------------------------------------------------
   **One tile = one thing**, never an aggregate: one invariant, one claim, one flat
   in a vote. A tile that cannot be described in one word does not belong in the
   grid — that is what `.kpi` and `.guard` are for.

   An exemption is not silence: it looks different (`--vynimka`), it is in the
   legend and it **carries its reason** in the table beside the grid. Without that
   it turns into a permanent alibi.                                             */

.tiles { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.tiles-item {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: var(--h-control); min-height: var(--h-control);
  padding: 0 var(--s-2);
  border: 1px solid var(--ok);
  border-radius: var(--r-sm);
  background: var(--ok-soft);
  color: var(--ink);
  font-family: var(--mono); font-size: var(--t-xs);
}
/* The hatch, not only the fill (§6). `--surface` shows through the stripes, so the
   texture reads on both themes without a token of its own. */
.tiles-item--bad {
  border-color: var(--bad);
  background-color: var(--bad-soft);
  background-image: repeating-linear-gradient(45deg, transparent 0 2px, var(--surface) 2px 4px);
}
.tiles-item--exception {
  border-style: dashed; border-color: var(--border-strong);
  background: var(--surface-2); color: var(--ink-soft);
}
/* Hundreds of items (128 claims) overflow the panel at full size. A modifier of the
   grid, not a second component — the difference is the dimension (§4.1). The click
   target of `--h-control` goes with it, which is why the tiles are **not** links:
   they are labels, and §6 governs targets, not text. */
.tiles--small { gap: var(--s-1); }
.tiles--small .tiles-item {
  min-width: 0; min-height: 0;
  padding: 0 var(--s-1);
  font-size: var(--t-micro);
}

/* --- The strip of votes -----------------------------------------------------
   The tile grid with a domain meaning of the tile (design.md §5): **one tile = one
   unit of the building**, in the order of the units, never a cluster by choice — two
   equally long groups would have to mean the same thing, and here they mean nothing.

   Which is why the boundary of the quorum is **not** drawn in here. A line at the 23rd
   tile of a strip ordered by flat number points at a flat, not at a threshold. The
   strip answers "who voted how"; whether it is enough is the `.meter` above it, where
   the fill is the votes for and the marker is the threshold. Two statements, two
   components (BL-98).

   The fill says what the owner **said**, not what it does to the arithmetic: for and
   against are the two opinions, an abstention and an invalid paper are taking part
   without one, and no tile filled at all is not taking part. For the threshold the last
   three are the same thing — the strip is not the place that says so, the meter is.

   Always `--small`, whatever the size of the building: the set is the building's units
   and that has no upper bound, so a strip picking its own size would need a threshold
   nobody owns, and two buildings drawn at two sizes would stop being comparable.

   "Za" is the plain tile and "proti" is `--bad`, i.e. the fill and the hatch of the grid
   itself; the three below are this strip's own. Each is declared **once** for the tile
   and for its swatch in the legend, because a legend whose sample differs from the mark
   explains a picture that is not on the screen (§4.4).                              */

/* Took part without an opinion. Not `--bad`: an abstention is not a vote against, and
   one fill for both would make the strip lie about the result. There is no token for
   neutral (§1.1), so it wears the pair a pill without a state wears. */
.tiles--votes .tiles-item--abstained,
.legend-item--abstained i {
  border-color: var(--border-strong);
  background: var(--surface-2);
  color: var(--ink-soft);
  opacity: 1;
}
/* A paper returned with nothing marked on it [CL-BYT-32]: it counts into the base and
   into nothing else, which is worth noticing — `--warn` plus a hatch of its own. The
   angle is the mirror of the one `--bad` wears, so the two textures are told apart
   without the colour (§6). */
.tiles--votes .tiles-item--invalid,
.legend-item--invalid i {
  border-color: var(--warn);
  background-color: var(--warn-soft);
  background-image: repeating-linear-gradient(-45deg, transparent 0 2px, var(--surface) 2px 4px);
  opacity: 1;
}
/* No vote at all — the absence of a row, not a choice (`voting.models.VoteChoice`). It
   is drawn from the difference against the units of the building, so an empty tile is
   the only honest picture. Dotted, not dashed: dashed is an exemption with a reason,
   and a flat that did not vote owes nobody one. */
.tiles--votes .tiles-item--not-voted,
.legend-item--not-voted i {
  border-style: dotted; border-color: var(--border-strong);
  background: var(--surface);
  color: var(--ink-soft);
  opacity: 1;
}
/* The swatch of the plain tile. The legend's own sample is the accent of a chart bubble,
   which is not what a tile is filled with. */
.legend-item--ok i {
  border-color: var(--ok);
  background: var(--ok-soft);
  opacity: 1;
}

/* --- "Where to start" list --------------------------------------------------
   An ordered list of what is worth opening first. **The order is carried by the
   `<ol>`**, not by the rendered number — otherwise a screen reader never hears it
   and a filter throws it off. Not `.readiness`: that says what is missing, this
   says what to begin with, and those are different statements.                 */

.list-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.step-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--s-2) var(--s-3);
  padding: var(--s-2) 0;
  border-top: 1px solid var(--border);
}
.list-steps > .step-row:first-child { border-top: 0; }
/* The ordinal is decoration for a screen reader — the `<ol>` already carries the
   order, and reading it twice is noise. It is not `.step`: that is the numbered
   circle in a panel header of the calculator (§4.3). */
.step-row > .mono:first-child { color: var(--ink-muted); font-size: var(--t-xs); }
.step-row .hint { grid-column: 2 / -1; }
