/* ============================================================================
   KoenZ — dashboard design system
   ============================================================================
   The only stylesheet in the project. Plans 04-03, 04-04 and 04-06, and then
   Phases 5 and 6, render through this file and add no stylesheet of their own.

   Design direction: an instrument panel, not a web page. Dense tabular data,
   hairline rules instead of boxes-inside-boxes, one disciplined accent, and a
   hard typographic jump from 13px uppercase labels to 28px monospace figures.
   The numbers are the content; everything else gets out of their way.

   Rules this file obeys (from 04-UI-SPEC.md):
     - Dark only. No light theme, no prefers-color-scheme variant, no toggle.
     - Exactly 4 type sizes, exactly 2 weights (400, 600). No italics.
     - Spacing is --sp-1..--sp-7 only; every other length is a declared constant.
     - --accent is reserved for the exhaustive list in section 1. It is not a
       heading colour, not a card border, not a hover state, not an icon colour.
     - Motion is overridden in exactly one place, the reduced-motion block at
       the end of section 13, which is also the only rule permitted to force
       its declarations over everything above it.
     - No external request: nothing is imported, no font file is declared, and
       no remote address appears anywhere in this file.
     - Every layout genuinely fits 320px, rather than clipping what does not.
   ========================================================================== */


/* ============================================================================
   1. TOKENS
   ========================================================================== */

:root {
  color-scheme: dark;

  /* --- Colour: 60 / 30 / 10 -------------------------------------------- */
  --bg: #0F1115;              /* dominant 60% — page, tab bar, dialog backdrop */
  --surface: #171A21;         /* secondary 30% — cards, rows, header, filters  */
  --surface-2: #1E222B;       /* raised only — hover, details body, code, dialog */
  --border: #262B36;          /* 1px hairlines, card outlines, input borders   */

  /* --- Accent: 10%, reserved ------------------------------------------- */
  /* --accent may appear ONLY as:
       1. fill of the single primary button on a page
       2. the active navigation tab (2px top border / tab-bar icon + label)
       3. the focus ring
       4. the selected filter chip
       5. the unread-signal badge and the "Uued signaalid" divider (Phase 6)
     Plus the #laadin progress line, which the spec's interaction contract
     declares explicitly. Nothing else. */
  --accent: #4C8DFF;
  --accent-ink: #0F1115;      /* text/icon ON a filled accent surface (5.95:1) */

  /* --- Text ------------------------------------------------------------- */
  --text: #E6E9EF;            /* 15.7:1 on --bg, 14.4:1 on --surface */
  --text-muted: #9AA3B2;      /*  7.5:1 on --bg,  6.8:1 on --surface */
  /* There is no third, dimmer text colour. Anything not important enough for
     --text-muted is not shown at all. */

  /* --- Semantic data colours -------------------------------------------
     These encode DATA, not decoration, and each is always accompanied by a
     word — colour is never the sole carrier of meaning. */
  --pos: #2ECC71;             /* 8.5:1 — LONG · Värske · Õnnestus · AI SEES   */
  --warn: #F0B429;            /* 9.6:1 — Hilineb · Kordus · cached tokens     */
  --danger: #FF5C5C;          /* 5.9:1 — SHORT · Seiskunud · Viga · PEATATUD  */
  --neutral: #9AA3B2;         /* 6.8:1 — Ootel · unknown · blocked rows       */

  /* Badge fills: the tint is precomputed (12% of the hue over --surface) so
     nothing has to composite alpha at runtime. */
  --pos-bg: #1A2B22;
  --warn-bg: #2E2717;
  --danger-bg: #2E1B1E;
  --accent-bg: #182338;

  /* --- Spacing scale — multiples of 4, and the only spacing values ------ */
  --sp-1: 4px;                /* icon-to-label gap, badge vertical padding    */
  --sp-2: 8px;                /* badge row gap, label-to-value inside a card  */
  --sp-3: 16px;               /* default: page padding, card padding, gaps    */
  --sp-4: 24px;               /* card padding on desktop, sub-section gap     */
  --sp-5: 32px;               /* gap between major page sections              */
  --sp-6: 48px;               /* empty-state padding, login card padding      */
  --sp-7: 64px;               /* page bottom padding above the mobile tab bar */

  /* --- Size constants (size, not spacing) ------------------------------- */
  --tap-min: 44px;            /* WCAG 2.2 / iOS minimum tap target            */
  --tabbar-h: 56px;           /* mobile bottom tab bar (+ safe-area inset)    */
  --header-h: 44px;           /* persistent kill-switch / price strip         */
  --container: 960px;         /* max content width, horizontally centred      */
  --bp-md: 720px;             /* the one layout breakpoint — see @media below */
  --bp-lg: 1024px;            /* secondary — stat cards 3-up, 2-col details   */
  /* --bp-md and --bp-lg are declared here for reference only: a custom
     property cannot be used in a media condition, so every media query below
     repeats the literal and names the token in a comment beside it. */

  /* --- Component constants declared by the spec's component section ----- */
  /* Named so that no component rule has to carry a bare pixel literal. */
  --r-sm: 6px;                /* badge, code block, thumbnail radius          */
  --r-md: 10px;               /* card, list row, details body radius          */
  --r-pill: 16px;             /* filter chip radius                           */
  --hairline: 1px;            /* every border in the system                   */
  --focus-w: 2px;             /* focus ring width                             */
  --rule-w: 3px;              /* list-row status left border                  */
  --badge-h: 22px;
  --chip-h: 32px;             /* visually 32px, inside a 44px touch row       */
  --row-h: 48px;              /* flush desktop list row                       */
  --row-h-card: 72px;         /* mobile list row rendered as a card           */
  --pill-h: 28px;             /* header-strip state pill                      */
  --pill-r: 14px;
  --dot: 6px;                 /* status dot                                   */
  --btn-h: 48px;              /* primary button / dialog buttons              */
  --login-w: 380px;
  --dialog-w: 340px;
  --thumb: 88px;
  --bar-h: 6px;               /* proportion bar                               */
  --bar-r: 3px;
  --unread: 18px;             /* unread-count circle on the Signaalid tab     */
  --code-max-h: 60vh;         /* long model text never grows without bound    */

  /* --- Type ------------------------------------------------------------- */
  /* System stack: no font file is downloaded and no external request is made. */
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
             "Helvetica Neue", Arial, sans-serif;
  --font-num: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
              "Liberation Mono", monospace;

  --fs-label: 13px;           /* 600 / 1.3  — headers, badges, meta, labels   */
  --fs-body: 16px;            /* 400 / 1.5  — prose, values, inputs           */
  --fs-head: 20px;            /* 600 / 1.2  — page and card titles            */
  --fs-display: 28px;         /* 600 / 1.2  — cost stats, spot price, wordmark */

  --lh-label: 1.3;
  --lh-body: 1.5;
  --lh-tight: 1.2;
  --lh-code: 1.45;

  --w-regular: 400;
  --w-bold: 600;              /* there is no third weight */

  --ls-label: 0.02em;

  /* --- Motion (LISA A item 2: 150–250ms) -------------------------------- */
  --t-fast: 150ms;
  --t-base: 200ms;
  --t-slow: 250ms;
  --ease: cubic-bezier(0.2, 0, 0.2, 1);

  --z-header: 10;
  --z-tabbar: 20;
}


/* ============================================================================
   2. RESET AND BASE
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* Deliberately no `overflow-x: hidden`. That hides a broken layout instead of
     fixing it; the spec requires every layout to genuinely fit 320px. */
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: var(--fs-body);
  font-weight: var(--w-regular);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, p, figure, blockquote, dl, dd {
  margin: 0;
}

ul, ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

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

img {
  height: auto;
}

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

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

/* Model output, tracebacks and hashes are arbitrary and frequently unbroken.
   They wrap rather than widen the page. */
.wrap-anywhere,
.code,
.kv-value,
.err-message,
.list-row {
  overflow-wrap: anywhere;
}


/* ============================================================================
   3. TYPOGRAPHY
   ========================================================================== */

.label {
  font-size: var(--fs-label);
  font-weight: var(--w-bold);
  line-height: var(--lh-label);
  letter-spacing: var(--ls-label);
  color: var(--text-muted);
}

.label-caps {
  text-transform: uppercase;
}

.body {
  font-size: var(--fs-body);
  font-weight: var(--w-regular);
  line-height: var(--lh-body);
}

h1, .title {
  font-size: var(--fs-head);
  font-weight: var(--w-bold);
  line-height: var(--lh-tight);
  color: var(--text);
}

.display {
  font-size: var(--fs-display);
  font-weight: var(--w-bold);
  line-height: var(--lh-tight);
}

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

.strong {
  font-weight: var(--w-bold);
}

/* The numeric rule, cross-cutting and non-negotiable: every price, token count,
   cost, percentage, latency and timestamp renders here. Tabular figures are what
   make a column of costs align on the decimal point with no JavaScript. Prose
   never uses this. */
.num,
.kv-value,
.stat-value,
.code {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* Tone utilities — each is always paired with a word in the markup, never used
   as the sole carrier of meaning. */
.t-pos     { color: var(--pos); }
.t-warn    { color: var(--warn); }
.t-danger  { color: var(--danger); }
.t-neutral { color: var(--neutral); }

/* A missing measurement. Never rendered as 0 — see filters.py. */
.none {
  color: var(--text-muted);
}


/* ============================================================================
   4. LAYOUT SHELL
   ========================================================================== */

.wrap {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--sp-3);
}

/* --- Header strip: price + kill-switch state, on every authenticated page -- */
.header-strip {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  min-height: var(--header-h);
  background: var(--surface);
  border-bottom: var(--hairline) solid var(--border);
  transition: background-color var(--t-base) var(--ease),
              border-color var(--t-base) var(--ease);
}

.header-strip-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  min-height: var(--header-h);
}

.header-price {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  font-size: var(--fs-label);
  font-weight: var(--w-bold);
}

/* Status dot. Always sits beside its own word. */
.dot {
  width: var(--dot);
  height: var(--dot);
  border-radius: 50%;
  background: var(--neutral);
  flex: none;
}

.dot-pos    { background: var(--pos); }
.dot-warn   { background: var(--warn); }
.dot-danger { background: var(--danger); }

/* State pill: AI SEES / AI PEATATUD */
.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  height: var(--pill-h);
  padding-inline: var(--sp-3);
  border-radius: var(--pill-r);
  font-size: var(--fs-label);
  font-weight: var(--w-bold);
  letter-spacing: var(--ls-label);
  background: var(--pos-bg);
  color: var(--pos);
  transition: background-color var(--t-base) var(--ease),
              color var(--t-base) var(--ease);
}

.pill-off {
  background: var(--danger-bg);
  color: var(--danger);
}

/* Kill-switch escalation (KILL-02): when AI is stopped the WHOLE strip changes,
   not just a small badge. Unmistakable on every page. */
body[data-ai="off"] .header-strip {
  background: var(--danger-bg);
  border-bottom-color: var(--danger);
}

/* --- Main ------------------------------------------------------------- */
main {
  display: block;
  padding-top: var(--sp-3);
  /* Clears the fixed tab bar so the last row is never covered. */
  padding-bottom: calc(var(--sp-7) + env(safe-area-inset-bottom));
}

.page-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-bottom: var(--sp-3);
}

/* Deliberate whitespace: major sections are separated by 32px, and nothing
   smaller is allowed to imply the same break. */
.section {
  margin-top: var(--sp-5);
}

.section-title {
  font-size: var(--fs-head);
  font-weight: var(--w-bold);
  line-height: var(--lh-tight);
  margin-bottom: var(--sp-3);
}

/* --- Mobile bottom tab bar -------------------------------------------- */
.tabbar {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: var(--z-tabbar);
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  height: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
  padding-bottom: env(safe-area-inset-bottom);
  background: var(--bg);
  border-top: var(--hairline) solid var(--border);
}

.tab {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1);
  min-height: var(--tap-min);
  color: var(--text-muted);
  font-size: var(--fs-label);
  font-weight: var(--w-bold);
  letter-spacing: var(--ls-label);
  position: relative;
  transition: color var(--t-fast) var(--ease);
}

/* Accent purpose 2 of 5: the active navigation tab. */
.tab[aria-current="page"] {
  color: var(--accent);
}

/* Accent purpose 5 of 5: the unread-signal badge (Phase 6). Absent, not 0,
   when there is nothing unread. */
.tab-badge {
  position: absolute;
  top: var(--sp-1);
  left: 50%;
  margin-left: var(--sp-1);
  min-width: var(--unread);
  height: var(--unread);
  padding-inline: var(--sp-1);
  border-radius: calc(var(--unread) / 2);
  background: var(--accent);
  color: var(--accent-ink);
  font-size: var(--fs-label);
  font-weight: var(--w-bold);
  line-height: var(--unread);
  text-align: center;
}

/* --- Desktop top nav --------------------------------------------------- */
.topnav {
  display: none;
}

/* --- Segmented sub-view control (Kõned │ Kulud │ Vead) ----------------- */
.segmented {
  display: flex;
  gap: var(--sp-1);
  padding: var(--sp-1);
  background: var(--surface-2);
  border-radius: var(--r-md);
  margin-bottom: var(--sp-3);
}

.segmented a {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  border-radius: var(--r-sm);
  color: var(--text-muted);
  font-size: var(--fs-label);
  font-weight: var(--w-bold);
  letter-spacing: var(--ls-label);
  transition: background-color var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease);
}

.segmented a[aria-current="page"] {
  background: var(--surface);
  color: var(--text);
}

/* ≥720px (--bp-md): the one layout breakpoint.
   Tab bar out, top nav in, content constrained. */
@media (min-width: 720px) {
  .tabbar {
    display: none;
  }

  main {
    padding-bottom: var(--sp-6);
  }

  .topnav {
    display: flex;
    gap: var(--sp-4);
    border-top: var(--hairline) solid var(--border);
  }

  .topnav a {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    min-height: var(--tap-min);
    border-top: var(--focus-w) solid transparent;
    margin-top: calc(var(--focus-w) * -1);
    color: var(--text-muted);
    font-size: var(--fs-label);
    font-weight: var(--w-bold);
    letter-spacing: var(--ls-label);
    transition: color var(--t-fast) var(--ease),
                border-color var(--t-fast) var(--ease);
  }

  /* Accent purpose 2 of 5. */
  .topnav a[aria-current="page"] {
    color: var(--accent);
    border-top-color: var(--accent);
  }

  .segmented {
    display: inline-flex;
    gap: var(--sp-1);
  }

  .segmented a {
    flex: 0 0 auto;
    padding-inline: var(--sp-4);
  }
}


/* ============================================================================
   5. LIST — the table-to-card collapse
   ============================================================================
   ONE markup, TWO layouts. Never two DOM variants, never display:table.

   Markup contract for plans 04-04 / 05:
     <div class="list" role="table" style="--cols: 132px 92px 1fr 84px">
       <div class="list-head" role="row"><span>Aeg</span>…</div>
       <a class="list-row" role="row" href="…">
         <span data-label="Aeg">30.07 14:23</span>…
         <span class="chev">…</span>
       </a>
     </div>

   Cell helpers, applied by the page template:
     .cell-full  — spans both mobile columns (its own line)
     .cell-end   — right-aligned in the mobile card
   --cols is the ONLY per-list customisation allowed.
   ========================================================================== */

.list {
  --cols: 1fr;
  /* Optional. A list that fits at every width simply never sets this and gets
     --cols everywhere, keeping --cols the only customisation it needs. See the
     column budget note in the 720px block for when it is required. */
  --cols-md: var(--cols);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.list-head {
  display: none;
}

.list-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  min-height: var(--row-h-card);
  padding: var(--sp-3);
  background: var(--surface);
  border: var(--hairline) solid var(--border);
  /* Transparent by default so the status modifiers below never shift layout. */
  border-left: var(--rule-w) solid transparent;
  border-radius: var(--r-md);
  color: var(--text);
  transition: background-color var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease);
}

/* Below 720px the data-label is not displayed: these values are self-evident
   and printing the label would double the height of every row. The attribute
   stays in the DOM as the accessible column name. */
.list-row > .cell-full {
  grid-column: 1 / -1;
}

.list-row > .cell-end {
  justify-self: end;
  text-align: right;
}

.list-row .chev {
  color: var(--text-muted);
  justify-self: end;
}

/* Status modifiers. The colour is never alone — a badge carries the word. */
.list-row.is-error {
  border-left-color: var(--danger);
}

.list-row.is-blocked {
  border-left-color: var(--neutral);
}

/* Hover only where there is a real pointer. On a touch screen this would leave
   a highlight stuck under the finger after a tap.

   Scoped to `a.list-row` (LISA C item 4): /kulud renders its breakdown rows with
   this same component and they are NOT links, so an unscoped rule promised a
   click that does nothing. A hover affordance is a promise about what happens
   next. */
@media (hover: hover) {
  a.list-row:hover {
    background: var(--surface-2);
  }
}

/* ≥720px (--bp-md): the same markup becomes a flush column table. */
@media (min-width: 720px) {
  .list {
    gap: 0;
    border: var(--hairline) solid var(--border);
    border-radius: var(--r-md);
    /* Deliberately NOT overflow: hidden. That would make .list a scroll
       container, which makes it the sticky head's nearest scrolling ancestor —
       the head would then offset 44px from the top of the list rather than
       from the viewport and sit exactly on top of the first row, hiding it.
       The spec's rounding is per-row anyway: first and last only. */
  }

  /* COLUMN BUDGET — read this before choosing a --cols for a new list.
     At exactly 720px the container gives 688px of content. The rule is:

         sum(fixed tracks) + 8 x (number of tracks - 1)  <=  688px

     The gap is --sp-2 rather than --sp-3 here for that reason; --sp-3 is
     restored at 1024px where there is room. A cell that does not fit is
     ellipsised, not scrolled, so an over-budget --cols does not break the page
     layout — it silently hides the right-hand columns instead, which is worse.

     A track holding a badge must fit the badge's longest token: the badge has
     16px of horizontal padding and its text is 13px/600 uppercase, so 72px
     truncates PEATATUD and ÕNNESTUS. 96px holds both.

     Eight columns do not comfortably fit in 688px: the /ai-logi set below is
     616px of fixed track, which leaves a 1fr column almost nothing between
     720px and roughly 860px. When that happens, mark the least critical cells
     .cell-wide (they are hidden from 720px to 1023px) and give the list a
     --cols-md without those tracks. The track count and the visible cell count
     must match, which is the whole reason both are declared together:

         --cols:    132px 92px 96px 1fr 92px 84px 96px 24px   (>= 1024px)
                    Aeg   Tüüp Mudel Tok Kulu Kest Staatus ›
         --cols-md: 132px 92px 1fr 92px 96px 24px            (720-1023px)
                    Aeg   Tüüp Tok  Kulu Staatus ›
                    ...with Mudel and Kestus marked .cell-wide

     Hiding a column is better than shipping one that is 16px wide: the reader
     can still get Mudel and Kestus from the detail page, but a silently
     collapsed column looks like missing data. */
  .list-head {
    position: sticky;
    top: var(--header-h);
    z-index: 1;
    display: grid;
    grid-template-columns: var(--cols-md);
    align-items: center;
    gap: var(--sp-2);
    min-height: var(--tap-min);
    padding-inline: var(--sp-3);
    /* The head is inset by the same rule width the rows reserve, so the
       columns line up with the cells beneath them. */
    padding-left: calc(var(--sp-3) + var(--rule-w));
    background: var(--surface-2);
    border-bottom: var(--hairline) solid var(--border);
    color: var(--text-muted);
    font-size: var(--fs-label);
    font-weight: var(--w-bold);
    letter-spacing: var(--ls-label);
    border-top-left-radius: var(--r-md);
    border-top-right-radius: var(--r-md);
  }

  .list-row {
    grid-template-columns: var(--cols-md);
    gap: var(--sp-2);
    min-height: var(--row-h);
    padding-block: 0;
    border: 0;
    border-left: var(--rule-w) solid transparent;
    border-bottom: var(--hairline) solid var(--border);
    border-radius: 0;
  }

  /* Radius on the first and last row only, so the run of rows reads as one
     table rather than a stack of separate cards. */
  .list-row:last-child {
    border-bottom: 0;
    border-bottom-left-radius: var(--r-md);
    border-bottom-right-radius: var(--r-md);
  }

  .list-head + .list-row {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
  }

  .list-row > .cell-full {
    grid-column: auto;
  }

  .list-row > .cell-end {
    justify-self: auto;
    text-align: left;
  }

  /* Dropped only in the cramped 720-1023px band; restored at 1024px below. */
  .list-head > .cell-wide,
  .list-row > .cell-wide {
    display: none;
  }

  /* Truncate rather than grow the row: a table row is a fixed 48px line.
     min-width: 0 is the load-bearing declaration — a grid track's automatic
     minimum is its content, so without this a long cell (or column heading)
     refuses to shrink and pushes the row past the container. The head needs it
     for exactly the same reason the rows do. */
  .list-row > *,
  .list-head > * {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}


/* ============================================================================
   6. BADGES — the only badge component
   ============================================================================
   Five tones and no more: pos, warn, danger, neutral, accent. Text comes from
   the full-strength colour, background from the precomputed tint.
   ========================================================================== */

.badge {
  display: inline-flex;
  align-items: center;
  height: var(--badge-h);
  padding-inline: var(--sp-2);
  border-radius: var(--r-sm);
  font-size: var(--fs-label);
  font-weight: var(--w-bold);
  line-height: 1;
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  white-space: nowrap;
  background: var(--surface-2);
  color: var(--text-muted);
  animation: badge-in var(--t-base) var(--ease) both;
}

.badge-pos     { background: var(--pos-bg);    color: var(--pos); }
.badge-warn    { background: var(--warn-bg);   color: var(--warn); }
.badge-danger  { background: var(--danger-bg); color: var(--danger); }
.badge-neutral { background: var(--surface-2); color: var(--text-muted); }
.badge-accent  { background: var(--accent-bg); color: var(--accent); }

.badge-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
}


/* ============================================================================
   7. FILTER BAR AND CHIPS
   ========================================================================== */

.filters {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-3);
  background: var(--surface);
  border: var(--hairline) solid var(--border);
  border-radius: var(--r-md);
}

.filter-group {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap-min);
}

.filter-label {
  flex: none;
  font-size: var(--fs-label);
  font-weight: var(--w-bold);
  letter-spacing: var(--ls-label);
  color: var(--text-muted);
}

/* The ONE element in the entire application permitted to scroll horizontally,
   and only because a chip strip is a control, not content. */
.chips {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap-min);
  overflow-x: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
  padding-right: var(--sp-3);
}

.chips::-webkit-scrollbar {
  display: none;
}

.chip {
  display: inline-flex;
  align-items: center;
  flex: none;
  height: var(--chip-h);
  padding-inline: var(--sp-3);
  border: var(--hairline) solid transparent;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  color: var(--text-muted);
  font-size: var(--fs-label);
  font-weight: var(--w-bold);
  letter-spacing: var(--ls-label);
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease);
}

/* Accent purpose 4 of 5: the selected filter chip. */
.chip[aria-pressed="true"],
.chip.is-selected {
  background: var(--accent-bg);
  border-color: var(--accent);
  color: var(--accent);
}


/* ============================================================================
   8. CARDS, STATS, KEY-VALUE BLOCKS, PROPORTION BARS
   ========================================================================== */

.card {
  padding: var(--sp-3);
  background: var(--surface);
  border: var(--hairline) solid var(--border);
  border-radius: var(--r-md);
}

.card + .card {
  margin-top: var(--sp-3);
}

.card-title {
  font-size: var(--fs-head);
  font-weight: var(--w-bold);
  line-height: var(--lh-tight);
  margin-bottom: var(--sp-2);
}

.stat-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-3);
}

.stat-card {
  padding: var(--sp-4);
  background: var(--surface);
  border: var(--hairline) solid var(--border);
  border-radius: var(--r-md);
  transition: background-color var(--t-base) var(--ease),
              border-color var(--t-base) var(--ease);
}

.stat-label {
  font-size: var(--fs-label);
  font-weight: var(--w-bold);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--text-muted);
}

.stat-value {
  display: block;
  margin-top: var(--sp-1);
  font-size: var(--fs-display);
  font-weight: var(--w-bold);
  line-height: var(--lh-tight);
}

.stat-sub {
  margin-top: var(--sp-1);
  font-size: var(--fs-label);
  font-weight: var(--w-regular);
  color: var(--text-muted);
}

/* --- Key-value rows ---------------------------------------------------- */
.kv {
  display: flex;
  flex-direction: column;
}

.kv-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--sp-3);
  min-height: var(--tap-min);
  border-bottom: var(--hairline) solid var(--border);
}

.kv-row:last-child {
  border-bottom: 0;
}

.kv-label {
  font-size: var(--fs-label);
  font-weight: var(--w-bold);
  letter-spacing: var(--ls-label);
  color: var(--text-muted);
}

.kv-value {
  font-size: var(--fs-body);
  font-weight: var(--w-regular);
  text-align: right;
}

/* The row that matters — cost, entry, stop. */
.kv-row.is-strong .kv-value {
  font-weight: var(--w-bold);
}

/* --- Proportion bar: the only chart-like element in v1, pure CSS -------- */
.bar {
  height: var(--bar-h);
  border-radius: var(--bar-r);
  background: var(--surface-2);
  overflow: hidden;
}

.bar-fill {
  height: 100%;
  border-radius: var(--bar-r);
  /* --text-muted, not --accent: this is data, not decoration. */
  background: var(--text-muted);
  transition: width var(--t-slow) var(--ease);
}


/* ============================================================================
   9. DISCLOSURE AND CODE
   ========================================================================== */

.disclosure {
  background: var(--surface);
  border: var(--hairline) solid var(--border);
  border-radius: var(--r-md);
}

.disclosure + .disclosure {
  margin-top: var(--sp-2);
}

.disclosure > summary {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap-min);
  padding-inline: var(--sp-3);
  font-size: var(--fs-body);
  font-weight: var(--w-bold);
  cursor: pointer;
  list-style: none;
}

/* Suppress the platform marker in favour of the chevron below. */
.disclosure > summary::-webkit-details-marker {
  display: none;
}

.disclosure > summary::marker {
  content: "";
}

/* The `> summary .icon` half is the correction recorded as LISA C item 3:
   ui.disclosure renders its chevron through icon(), which emits
   <svg class="icon">, so there is no .disclosure-chevron element anywhere in the
   document and the original selector matched nothing — every open disclosure
   kept a chevron pointing right, exactly like the collapsed ones beside it.
   Component 7 always said "chevron rotates 90° when open"; only the selector was
   wrong. Both are kept so a future markup that does carry the class still works. */
.disclosure-chevron,
.disclosure > summary .icon {
  flex: none;
  color: var(--text-muted);
  /* transform only — never animate width/height/top, which would relayout. */
  transition: transform var(--t-base) var(--ease);
}

.disclosure[open] > summary .disclosure-chevron,
.disclosure[open] > summary .icon {
  transform: rotate(90deg);
}

.disclosure-meta {
  margin-left: auto;
  font-size: var(--fs-label);
  font-weight: var(--w-regular);
  color: var(--text-muted);
}

.disclosure-body {
  padding: var(--sp-3);
  background: var(--surface-2);
  border-bottom-left-radius: var(--r-md);
  border-bottom-right-radius: var(--r-md);
}

/* Read-only model output: wraps, never scrolls sideways, never unbounded. */
.code {
  margin: 0;
  padding: var(--sp-3);
  background: var(--surface-2);
  border-radius: var(--r-sm);
  font-size: var(--fs-label);
  font-weight: var(--w-regular);
  line-height: var(--lh-code);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: var(--code-max-h);
  overflow-y: auto;
}


/* ============================================================================
   9b. ERROR ROWS — /vead (04-UI-SPEC.md LISA C item 1)
   ============================================================================
   Page 6 draws an error as a card at EVERY width, which the .list component in
   section 5 cannot express: above 720px a .list-row is a 48px single-line grid
   row with white-space: nowrap, and an inline <details> inside one is not a
   layout, it is a clipped rectangle. Hence a component of its own — composed
   entirely from tokens already declared in section 1, with no new colour, size
   or spacing value.

   .err-message was already named in section 2's overflow-wrap group: 04-02
   reserved the class and left the component to whoever built the page.
   ========================================================================== */

.err-row {
  padding: var(--sp-3);
  background: var(--surface);
  border: var(--hairline) solid var(--border);
  /* The same 3px rule the list rows use for a failed call, so "something went
     wrong here" looks identical everywhere in the application. The colour is
     never alone: the component badge and the message carry the words. */
  border-left: var(--rule-w) solid var(--danger);
  border-radius: var(--r-md);
}

.err-row + .err-row {
  margin-top: var(--sp-2);
}

.err-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  color: var(--text-muted);
  font-size: var(--fs-label);
}

/* Two lines in the row, the whole message in the DOM. Clamping is presentation:
   a truncated traceback message is a truncated clue, so nothing is dropped from
   the document — only from the first glance. */
.err-message {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  margin-top: var(--sp-1);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
}

/* The trace expands inside a card that is already a card. Borderless and
   transparent, so the row does not read as a box inside a box, and unpadded so
   the .code block itself supplies the --surface-2. */
.err-row .disclosure {
  margin-top: var(--sp-2);
  background: none;
  border: 0;
}

.err-row .disclosure > summary {
  padding-inline: 0;
  font-size: var(--fs-label);
  color: var(--text-muted);
}

.err-row .disclosure-body {
  padding: 0;
  background: none;
}


/* ============================================================================
   10. FORMS AND BUTTONS
   ========================================================================== */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}

.field + .field {
  margin-top: var(--sp-3);
}

.field > label {
  font-size: var(--fs-label);
  font-weight: var(--w-bold);
  letter-spacing: var(--ls-label);
  color: var(--text-muted);
}

input[type="text"],
input[type="password"],
input[type="search"],
select,
textarea {
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface-2);
  border: var(--hairline) solid var(--border);
  border-radius: var(--r-sm);
  color: var(--text);
  /* 16px is not a style choice: anything smaller makes iOS Safari zoom the
     viewport on focus, which breaks the one-handed requirement. */
  font-size: var(--fs-body);
  transition: border-color var(--t-fast) var(--ease);
}

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

select {
  cursor: pointer;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: var(--tap-min);
  padding-inline: var(--sp-4);
  border: var(--hairline) solid transparent;
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text);
  font-size: var(--fs-body);
  font-weight: var(--w-bold);
  cursor: pointer;
  transition: background-color var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease),
              transform var(--t-fast) var(--ease);
}

.btn-block {
  width: 100%;
  min-height: var(--btn-h);
}

/* Accent purpose 1 of 5: the single primary button on a page. */
.btn-primary {
  background: var(--accent);
  color: var(--accent-ink);
}

.btn-quiet {
  background: var(--surface);
  border-color: var(--border);
  color: var(--text);
}

/* Resuming spend is the action that costs money — it is never accent-filled. */
.btn-danger {
  background: var(--danger);
  color: var(--accent-ink);
}

.btn-danger-quiet {
  background: transparent;
  border-color: var(--danger);
  color: var(--danger);
}

/* A text button, used by empty states and Lähtesta filtrid. */
.btn-text {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  background: none;
  border: 0;
  color: var(--accent);
  font-size: var(--fs-body);
  font-weight: var(--w-bold);
  cursor: pointer;
}

/* Tactile feedback (LISA A item 2): the button acknowledges the press itself. */
@media (hover: hover) {
  .btn:hover {
    background: var(--surface-2);
  }

  .btn-primary:hover,
  .btn-danger:hover {
    /* The fill is a reserved colour and must not shift hue on hover, so the
       affordance is a lift instead. */
    transform: translateY(-1px);
  }
}

.btn:active {
  transform: translateY(1px);
}

/* --- Login ------------------------------------------------------------- */
.login {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100dvh;
  padding: var(--sp-6) var(--sp-3);
}

.login-brand {
  text-align: center;
  margin-bottom: var(--sp-5);
}

.login-wordmark {
  font-size: var(--fs-display);
  font-weight: var(--w-bold);
  line-height: var(--lh-tight);
  letter-spacing: -0.01em;
}

.login-tagline {
  font-size: var(--fs-label);
  font-weight: var(--w-bold);
  letter-spacing: var(--ls-label);
  color: var(--text-muted);
}

.login-card {
  width: 100%;
  max-width: var(--login-w);
  padding: var(--sp-4);
  background: var(--surface);
  border: var(--hairline) solid var(--border);
  border-radius: var(--r-md);
}

/* The failure message is inserted above the button. The slot is always present
   so the card does not jump when it appears. */
.login-error {
  min-height: var(--sp-4);
  margin-top: var(--sp-3);
  color: var(--danger);
  font-size: var(--fs-label);
  font-weight: var(--w-bold);
}

.login-card .btn-block {
  margin-top: var(--sp-4);
}


/* ============================================================================
   11. STATES — empty, error, flash, loading
   ========================================================================== */

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding-block: var(--sp-6);
}

.empty-heading {
  font-size: var(--fs-head);
  font-weight: var(--w-bold);
  line-height: var(--lh-tight);
}

.empty-body {
  max-width: 40ch;
  margin-top: var(--sp-2);
  color: var(--text-muted);
}

.error-card {
  padding: var(--sp-4);
  background: var(--danger-bg);
  border: var(--hairline) solid var(--danger);
  border-radius: var(--r-md);
  text-align: center;
}

.error-card .btn {
  margin-top: var(--sp-3);
}

.flash {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap-min);
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--fs-label);
  font-weight: var(--w-bold);
  background: var(--surface-2);
  color: var(--text);
}

.flash-warn   { background: var(--warn-bg);   color: var(--warn); }
.flash-danger { background: var(--danger-bg); color: var(--danger); }
.flash-pos    { background: var(--pos-bg);    color: var(--pos); }

/* --- HTMX progress line ------------------------------------------------
   Shown only after 300ms, so a fast response never produces a flash of
   progress bar. The delay is on the way in and removed on the way out. */
#laadin {
  height: var(--focus-w);
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: 0 50%;
  opacity: 0;
  transition: transform var(--t-slow) var(--ease), opacity var(--t-fast) var(--ease);
}

#laadin.htmx-request,
.htmx-request #laadin {
  transform: scaleX(1);
  opacity: 1;
  transition-delay: 300ms;
}

/* --- Skeleton loading (LISA A item 1) ---------------------------------- */
.skeleton {
  position: relative;
  overflow: hidden;
  min-height: var(--row-h);
  border-radius: var(--r-md);
  background: var(--surface);
}

.skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    transparent 0%,
    var(--surface-2) 50%,
    transparent 100%
  );
  transform: translateX(-100%);
  animation: shimmer 1200ms var(--ease) infinite;
}


/* ============================================================================
   12. MICRO-INTERACTIONS (LISA A item 2)
   ============================================================================
   Defined here so Phase 6 attaches behaviour to existing classes rather than
   inventing new ones. Every duration is 150–250ms; nothing animates a property
   that forces layout.
   ========================================================================== */

@keyframes badge-in {
  from { opacity: 0; transform: translateY(2px); }
  to   { opacity: 1; transform: none; }
}

/* A newly arrived row (Phase 6 SSE) slides in rather than appearing abruptly,
   so the eye can tell an insertion from a re-render. */
@keyframes slide-in {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: none; }
}

/* A changed numeric value pulses once. This is the one place where the accent
   touches a number, and only transiently. */
@keyframes pulse-accent {
  0%   { background-color: var(--accent-bg); }
  100% { background-color: transparent; }
}

@keyframes shimmer {
  100% { transform: translateX(100%); }
}

.is-new {
  animation: slide-in var(--t-slow) var(--ease) both;
}

.is-changed {
  animation: pulse-accent var(--t-slow) var(--ease);
  border-radius: var(--r-sm);
}

/* Accent purpose 5 of 5: the "Uued signaalid" divider (Phase 6). */
.new-divider {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-block: var(--sp-3);
  color: var(--accent);
  font-size: var(--fs-label);
  font-weight: var(--w-bold);
  letter-spacing: var(--ls-label);
}

.new-divider::before,
.new-divider::after {
  content: "";
  flex: 1;
  height: var(--hairline);
  background: var(--accent);
}


/* ============================================================================
   13. ACCESSIBILITY
   ========================================================================== */

/* Accent purpose 3 of 5: the focus ring. Nothing in this file ever suppresses
   an outline — a keyboard user must always be able to see where they are. */
:focus-visible {
  outline: var(--focus-w) solid var(--accent);
  outline-offset: var(--focus-w);
  border-radius: var(--r-sm);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The single permitted use of !important in this file. It must win over every
   animation and transition declared above, whatever their specificity. */
@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;
  }
}


/* ============================================================================
   14. FORWARD DECLARATIONS FOR PHASES 5 AND 6
   ============================================================================
   Declared now, from the same tokens, so those phases add no new visual
   vocabulary. Every value here is already in the spec's component section.
   ========================================================================== */

/* --- Chart thumbnails and album strip (spec component 9) --------------- */
.thumbs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

.thumb {
  width: var(--thumb);
  height: var(--thumb);
  border-radius: var(--r-sm);
  border: var(--hairline) solid var(--border);
  object-fit: cover;
  cursor: pointer;
}

.album {
  display: flex;
  gap: var(--sp-2);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}

.album::-webkit-scrollbar {
  display: none;
}

.album > img {
  flex: none;
  width: 100%;
  scroll-snap-align: center;
  border-radius: var(--r-md);
}

.album-dots {
  display: flex;
  justify-content: center;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
}

.album-dot {
  width: var(--dot);
  height: var(--dot);
  border-radius: 50%;
  background: var(--border);
}

.album-dot.is-active {
  background: var(--text-muted);
}

/* --- Native dialog: image zoom and the "Luba AI" confirm (components 9, 10) */
dialog {
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text);
}

dialog::backdrop {
  background: var(--bg);
  opacity: 0.96;
}

.dialog-panel {
  width: 100%;
  max-width: var(--dialog-w);
  padding: var(--sp-4);
  background: var(--surface-2);
  border-radius: var(--r-md);
}

.dialog-title {
  font-size: var(--fs-head);
  font-weight: var(--w-bold);
  line-height: var(--lh-tight);
}

.dialog-body {
  margin-top: var(--sp-2);
  color: var(--text-muted);
}

.dialog-actions {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  margin-top: var(--sp-4);
}

.dialog-zoom {
  width: 100dvw;
  height: 100dvh;
  max-width: 100dvw;
  max-height: 100dvh;
}

.dialog-zoom img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.dialog-close {
  position: fixed;
  top: env(safe-area-inset-top);
  right: env(safe-area-inset-right);
  width: var(--tap-min);
  height: var(--tap-min);
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 0;
  color: var(--text);
  cursor: pointer;
}

/* --- Icons ------------------------------------------------------------- */
.icon {
  flex: none;
  /* Inherits its colour from the surrounding text; icons are never accent. */
  color: currentColor;
}


/* ============================================================================
   15. WIDE VIEWPORT REFINEMENTS
   ========================================================================== */

/* ≥720px (--bp-md) */
@media (min-width: 720px) {
  .card {
    padding: var(--sp-4);
  }

  .filters {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-4);
  }

  main {
    padding-top: var(--sp-4);
  }
}

/* ≥1024px (--bp-lg): stat cards go 3-up, detail pages get a 2-column split. */
@media (min-width: 1024px) {
  .stat-grid {
    grid-template-columns: repeat(3, 1fr);
  }

  /* Room for the roomier gap and the full column set again — see the column
     budget note in section 5. */
  .list-head,
  .list-row {
    grid-template-columns: var(--cols);
    gap: var(--sp-3);
  }

  .list-head > .cell-wide,
  .list-row > .cell-wide {
    display: block;
  }

  .split {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--sp-4);
    align-items: start;
  }
}
