Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-ui-web/styles/console.css

/* monetize console — the same design system as the gunnar console, so the two
 * read as siblings. COLOUR MEANS STATUS: surfaces are warm near-black, ink is
 * warm bone, and the only saturated pixels are the ladder — paid / grace /
 * suspended / live. Primary actions are expressed by contrast, not hue.
 * Dark only, like its sibling. */

:root {
  color-scheme: dark;
  --bg: oklch(0.155 0.006 70);
  --surface: oklch(0.198 0.007 70);
  --surface-raised: oklch(0.238 0.008 70);
  --surface-hover: oklch(0.268 0.008 70);
  --line: oklch(0.30 0.008 70);
  --line-strong: oklch(0.40 0.009 70);
  --ink: oklch(0.95 0.006 75);
  --ink-dim: oklch(0.775 0.007 75);
  --ink-mute: oklch(0.635 0.007 75);
  --pass: oklch(0.79 0.15 155);
  --warn: oklch(0.83 0.15 85);
  --bad: oklch(0.71 0.19 25);
  --live: oklch(0.80 0.11 240);
  --pass-wash: oklch(0.79 0.15 155 / 0.13);
  --warn-wash: oklch(0.83 0.15 85 / 0.13);
  --bad-wash: oklch(0.71 0.19 25 / 0.14);
  --live-wash: oklch(0.80 0.11 240 / 0.13);
  --radius: 8px;
  --radius-sm: 5px;
  --sans: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --mono: ui-monospace, "JetBrains Mono", SFMono-Regular, Menlo, Consolas, monospace;
  --sidebar-w: 220px;
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --fast: 150ms;
}

*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body { background: var(--bg); color: var(--ink); font-family: var(--sans); font-size: 14.5px; line-height: 1.55; -webkit-font-smoothing: antialiased; }
:where(h1, h2, h3, h4, p, figure, ul, ol, dl, dd) { margin: 0; }
:where(ul, ol) { padding: 0; list-style: none; }
a { color: var(--ink); text-decoration-color: var(--line-strong); text-underline-offset: 3px; }
a:hover { text-decoration-color: var(--ink); }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; border-radius: var(--radius-sm); }
code, .mono { font-family: var(--mono); font-size: 0.92em; font-variant-numeric: tabular-nums; }
.num { font-variant-numeric: tabular-nums; text-align: right; }

/* ── Shell ─────────────────────────────────────────────────────────── */
/* THE ESTATE IS THE TOP LEVEL (owner 2026-10-02): a top bar (brand · the
   estate picker · Overview · You), then either the Overview, which spans every
   estate, or ONE estate: its left menu — the same six for every estate — and
   its page. */
.app { display: flex; flex-direction: column; height: 100dvh; }
.topbar { display: flex; align-items: center; gap: 0.9rem; padding: 0 1.25rem; min-height: 52px; flex: none; border-bottom: 1px solid var(--line); background: oklch(0.135 0.006 70); }
.topbar .spacer { flex: 1; }
.brand { display: flex; align-items: center; gap: 0.55rem; font-size: 1rem; font-weight: 640; letter-spacing: -0.015em; text-decoration: none; }
/* The horn, small, beside the wordmark — the same shape gunnar-ui's badger
   has. Shipped at 512px and 21 kB; drawn at 28px high. */
.brand-mark { height: 28px; width: auto; border-radius: 5px; display: block; }
.estate-picker select { font-family: var(--mono); max-width: 16rem; }
.estate-picker optgroup { font-family: var(--sans); color: var(--ink-mute); }
.nav-link.top { padding: 0.35rem 0.6rem; }
.you { position: relative; }
.you > summary { list-style: none; cursor: pointer; }
.you > summary::-webkit-details-marker { display: none; }
.you-menu { position: absolute; right: 0; top: calc(100% + 6px); z-index: 10; display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem; min-width: 12rem; padding: 0.75rem; background: var(--surface-raised); border: 1px solid var(--line-strong); border-radius: var(--radius); box-shadow: 0 8px 24px oklch(0 0 0 / 0.35); }
.estate { flex: 1; min-height: 0; display: grid; grid-template-columns: var(--sidebar-w) minmax(0, 1fr); }
.sidebar { display: flex; flex-direction: column; gap: 1.25rem; padding: 1.25rem 0.75rem; background: oklch(0.135 0.006 70); border-right: 1px solid var(--line); overflow-y: auto; }
.estate-name { display: flex; flex-direction: column; align-items: flex-start; gap: 0.3rem; padding: 0 0.6rem; }
.estate-name strong { font-size: 0.98rem; overflow-wrap: anywhere; }
.estate-name .badge { white-space: normal; max-width: 100%; }
.nav-group { display: flex; flex-direction: column; gap: 1px; }
.nav-group > h2 { padding: 0 0.6rem; margin-bottom: 0.4rem; color: var(--ink-mute); font-size: 0.68rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; }
.nav-link { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; padding: 0.42rem 0.6rem; border-radius: var(--radius-sm); color: var(--ink-dim); font-size: 0.9rem; text-decoration: none; white-space: nowrap; transition: background var(--fast) var(--ease), color var(--fast) var(--ease); }
.nav-link:hover { background: var(--surface); color: var(--ink); }
.nav-link[aria-current="page"] { background: var(--surface-raised); color: var(--ink); font-weight: 560; }
.nav-link.sub { padding-left: 1.4rem; font-size: 0.84rem; }
.page { flex: 1; min-width: 0; overflow-y: auto; padding: 2rem 1.75rem 4rem; }
.vh { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
.section-title { margin-top: 0.5rem; font-size: 1.05rem; font-weight: 600; }

/* ── Overview: one row per estate, its map as a thumbnail ──────────────
   Never a blank square: the board, or the words that say why there is none. */
table.overview td { vertical-align: middle; }
.thumb { display: flex; align-items: center; justify-content: center; width: 128px; height: 72px; overflow: hidden; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); background: var(--bg); text-decoration: none; }
.thumb img { width: 100%; height: 100%; object-fit: contain; display: block; }
.thumb:hover { border-color: var(--ink-dim); }
.thumb.placeholder { padding: 0.3rem; border-style: dashed; color: var(--ink-mute); font-size: 0.72rem; line-height: 1.25; text-align: center; }
.thumb.placeholder.asking { color: var(--ink-mute); }
.estate-link { font-weight: 600; text-decoration: none; }
.test-money { white-space: nowrap; }
/* OVW-CLEAN: the New-estate slot (the last row) and the one quiet line of what ended. */
.slot-form { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.9rem; }
.slot-form input.slot-name { min-width: 14ch; }
.slot-form input, .slot-form select { max-width: 100%; }
details.ended { padding: 0.5rem 1rem 0.7rem; }
details.ended summary { cursor: pointer; }
details.ended ul.ended-list { margin: 0.4rem 0 0; padding-left: 1.1rem; }
.total { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem 1rem; padding: 0.9rem 1rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
.total strong { font-size: 1.25rem; }
.total p { flex-basis: 100%; }

/* ── Page header, panels, layout ───────────────────────────────────── */
.page-header { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 1rem; padding-bottom: 0.9rem; margin-bottom: 1.75rem; border-bottom: 1px solid var(--line); }
.page-header h1 { font-size: 1.4rem; font-weight: 640; letter-spacing: -0.02em; }
.page-header p { margin-top: 0.15rem; color: var(--ink-mute); font-size: 0.88rem; }
.page-header .actions { margin-left: auto; display: flex; gap: 0.5rem; }
.panel { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
.panel > header { display: flex; align-items: center; gap: 0.75rem; padding: 0.8rem 1rem; border-bottom: 1px solid var(--line); }
.panel > header h2 { font-size: 0.94rem; font-weight: 600; }
.panel > header .actions { margin-left: auto; display: flex; gap: 0.4rem; }
.panel-body { padding: 1rem; }
.stack { display: flex; flex-direction: column; gap: 1.25rem; }
.split { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: 1.25rem; align-items: start; }
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.25rem; }
.card { display: flex; flex-direction: column; gap: 0.75rem; padding: 1.1rem 1.2rem; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
.card h3 { font-family: var(--mono); font-size: 1.1rem; font-weight: 600; }
.card .meters { color: var(--ink-mute); font-size: 0.82rem; }
.card .meters li { display: flex; gap: 0.5rem; }
.card .meters code { color: var(--ink-dim); }
.card .badges { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.metric { padding: 0.85rem 1rem; background: var(--surface); }
.metric dt { color: var(--ink-mute); font-size: 0.76rem; font-weight: 500; letter-spacing: 0.04em; text-transform: uppercase; }
.metric dd { margin-top: 0.2rem; font-family: var(--mono); font-size: 1.32rem; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }
.metric dd small { margin-left: 0.25rem; color: var(--ink-mute); font-size: 0.75rem; }

/* ── Tables ────────────────────────────────────────────────────────── */
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
th, td { padding: 0.55rem 1rem; text-align: left; border-bottom: 1px solid var(--line); vertical-align: top; }
th { color: var(--ink-mute); font-size: 0.74rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; white-space: nowrap; }
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover td { background: var(--surface-hover); }
td.num, th.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
tr.attention td { background: var(--warn-wash); }

/* A SECTION HEADING INSIDE A TABLE. The Plans table groups by the file each
   plan was loaded from (`monetize::Plan::source`): four rehearsal rungs and
   three real plans used to sit in one flat list, told apart only by a naming
   convention in the id, and a rehearsal plan sold to a real customer costs
   1.00 SEK for a year of storage. The heading is a rule across the table rather
   than a coloured band, because colour on this console is a claim about whether
   something is in trouble and a filename is not one. */
tr.group th { padding: 0.9rem 1rem 0.35rem; background: var(--surface-raised); border-bottom: 1px solid var(--line-strong); text-transform: none; letter-spacing: 0; font-size: 0.82rem; }
tr.group th .mono { color: var(--ink); font-size: 0.86rem; }
tr.group th .hint { margin-left: 0.6rem; }
tr.group:hover th { background: var(--surface-raised); }
tbody + tbody tr.group th { padding-top: 1.2rem; }

/* Several facts in ONE cell, one per line. A comma-joined run of caps is
   unreadable the moment a plan has two of them, and "what it grants" and "what
   backs it" are both lists by nature. */
.cell-list { display: flex; flex-direction: column; gap: 0.25rem; }
.cell-list li { display: flex; align-items: baseline; gap: 0.4rem; white-space: nowrap; }
.cell-list .num { text-align: left; }
.plans td { vertical-align: top; }

/* ── Badges: the whole colour budget ───────────────────────────────── */
.badge { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.12rem 0.55rem; border-radius: 999px; font-size: 0.76rem; font-weight: 560; letter-spacing: 0.01em; white-space: nowrap; border: 1px solid transparent; }
.badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.badge.pass { color: var(--pass); background: var(--pass-wash); }
/* Grace and Suspended are the two rungs an operator must never scroll past, so
   they get a ring as well as a wash — colour alone is the thing tired eyes and
   colour-blind eyes both lose first. The word is always beside it either way. */
.badge.warn { color: var(--warn); background: var(--warn-wash); border-color: oklch(0.83 0.15 85 / 0.45); }
.badge.bad { color: var(--bad); background: var(--bad-wash); border-color: oklch(0.71 0.19 25 / 0.5); font-weight: 620; }
.badge.live { color: var(--live); background: var(--live-wash); }
.badge.plain { color: var(--ink-dim); border-color: var(--line-strong); }
.badge.plain::before { display: none; }

/* ── Bars: one track each, and the colour is a CLAIM ──────────────────
   `ui::Track` is the one renderer — the tenant bars, the product rows and
   the big fleet bar all draw through it. The decision is `fmt::bar_state`,
   pure and tested at every boundary; nothing here re-decides it. Read the classes as a sentence:

     ok / near / at-limit   a WALL. The product refuses over this cap, so the
                            escalation is real and earns green → amber → red.
     metered                counted and billed, never refused. It fills, it is
                            legible, and it does not escalate — a tenant at 97%
                            of a metered cap is not in trouble, and colouring
                            them as if they were teaches an operator to ignore
                            red everywhere else.
     provisioned            bought iron. The bar is a picture of a PURCHASE, so
                            it is drawn as a solid quiet block rather than as a
                            budget being eaten.
     uncapped               there is no percentage. Striped empty track, and
                            the words "no cap" beside the number.
     unknown                this monetize did not say. Said, not guessed.

   Colour never carries meaning alone: every non-`ok` state also has a word or
   a texture, and each track has a `title` naming what it claims.            */
.bars { display: grid; grid-template-columns: max-content minmax(0, 1fr) max-content; gap: 0.5rem 1rem; align-items: center; }
.bars .name { font-family: var(--mono); font-size: 0.82rem; color: var(--ink-dim); }
.track { height: 9px; background: var(--surface-raised); border-radius: 5px; overflow: hidden; box-shadow: inset 0 0 0 1px var(--line); }
.fill { height: 100%; border-radius: 5px; background: var(--ink-mute); transition: width 320ms var(--ease); }

/* A wall, and how close to it. */
.fill.ok { background: linear-gradient(90deg, oklch(0.72 0.13 155), var(--pass)); }
.fill.near { background: linear-gradient(90deg, var(--pass), var(--warn)); }
.fill.at-limit { background: linear-gradient(90deg, var(--warn), var(--bad)); }
/* At the wall the row earns the one piece of motion on this page: it is the
   only state where something is being REFUSED right now. */
.track:has(.at-limit) { box-shadow: inset 0 0 0 1px var(--bad); }

/* Billed, never refused: blue, which this console already uses for "live, not
   wrong". It fills to 100% without ever becoming an alarm. */
.fill.metered { background: linear-gradient(90deg, oklch(0.72 0.09 250), var(--live)); }

/* Bought iron. Flat and quiet — nothing is being consumed here. */
.fill.provisioned { background: repeating-linear-gradient(135deg, var(--line-strong) 0 6px, var(--surface-hover) 6px 12px); }

/* No cap: an empty striped track, so it can never be mistaken for 0% or 100%. */
.track.uncapped { background: repeating-linear-gradient(135deg, transparent 0 5px, oklch(1 0 0 / 0.045) 5px 10px), var(--surface-raised); }
.fill.uncapped { width: 0; }
.bars .no-cap { color: var(--ink-mute); font-size: 0.72rem; letter-spacing: 0.03em; text-transform: uppercase; }

/* The console does not know what this cap is: amber and striped, never green. */
.fill.unknown { background: repeating-linear-gradient(135deg, var(--warn) 0 5px, oklch(0.83 0.15 85 / 0.45) 5px 10px); }

.bars .value { font-family: var(--mono); font-size: 0.82rem; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ── What the tenant is standing on ────────────────────────────────────
   `provisioned` is iron monetize BOUGHT and a reclaim gives back;
   `on-hand` is capacity the pool already had. Both are real; only one is
   monetize's to return, and the row says which without needing the legend. */
.holdings { display: grid; grid-template-columns: max-content minmax(0, 1fr) max-content; gap: 0.4rem 1rem; align-items: baseline; font-size: 0.86rem; }
.holdings .what { font-family: var(--mono); }
.holdings .ref { color: var(--ink-mute); font-family: var(--mono); font-size: 0.76rem; overflow-wrap: anywhere; }

/* ── Controls ──────────────────────────────────────────────────────── */
.btn { display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.42rem 0.85rem; background: var(--surface-raised); color: var(--ink); border: 1px solid var(--line-strong); border-radius: var(--radius-sm); font: inherit; font-size: 0.86rem; font-weight: 540; cursor: pointer; text-decoration: none; transition: background var(--fast) var(--ease); }
.btn:hover { background: var(--surface-hover); }
.btn.primary { background: var(--ink); color: var(--bg); border-color: var(--ink); }
.btn.primary:hover { background: var(--ink-dim); }
.btn.danger { border-color: var(--bad); color: var(--bad); }
.btn.quiet { background: none; border-color: transparent; color: var(--ink-dim); }
.btn.quiet:hover { background: var(--surface); color: var(--ink); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.row { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; }
.field { display: flex; flex-direction: column; gap: 0.3rem; min-width: 9rem; }
.field label { color: var(--ink-mute); font-size: 0.76rem; font-weight: 500; letter-spacing: 0.04em; text-transform: uppercase; }
input, select, textarea { padding: 0.45rem 0.6rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line-strong); border-radius: var(--radius-sm); font: inherit; font-size: 0.9rem; }
textarea { font-family: var(--mono); min-height: 5.5rem; resize: vertical; }
input:focus, select:focus, textarea:focus { outline: 2px solid var(--ink); outline-offset: 1px; }
.hint { color: var(--ink-mute); font-size: 0.8rem; }
/* NO DATA under the SILENT list: the tenants nobody measured. Set apart from
   the rows above it by a rule and inset, and deliberately NOT given the alarm
   colours — an absence of measurement is not a finding about a customer, and
   painting it red would put the plugin's gap on the page as the customer's
   problem. See `fmt::no_flow_data_line`. */
.flow-list + .no-data, .no-data { margin: 0.7rem 0 0; padding: 0.6rem 0 0; border-top: 1px solid var(--line); line-height: 1.45; }
dl.facts { display: grid; grid-template-columns: max-content 1fr; gap: 0.35rem 1.25rem; font-size: 0.9rem; }
dl.facts dt { color: var(--ink-mute); }
dl.facts dd { font-family: var(--mono); overflow-wrap: anywhere; }

/* ── States of an answer ───────────────────────────────────────────── */
.failure { padding: 1rem; border: 1px solid var(--bad); border-radius: var(--radius); background: var(--bad-wash); }
.failure h3 { font-size: 0.95rem; font-weight: 600; }
.failure .detail { margin-top: 0.3rem; font-family: var(--mono); font-size: 0.82rem; color: var(--ink-dim); overflow-wrap: anywhere; }
.failure footer { margin-top: 0.6rem; display: flex; gap: 1rem; align-items: center; color: var(--ink-mute); font-size: 0.78rem; }
.notice { padding: 0.75rem 1rem; border-radius: var(--radius); background: var(--pass-wash); color: var(--pass); font-size: 0.88rem; }
.empty { padding: 2.5rem 1rem; text-align: center; color: var(--ink-mute); }
.empty h3 { color: var(--ink-dim); font-size: 1rem; font-weight: 600; margin-bottom: 0.3rem; }
.skeleton { height: 0.9rem; margin: 0.55rem 0; border-radius: 4px; background: linear-gradient(90deg, var(--surface-raised), var(--surface-hover), var(--surface-raised)); background-size: 200% 100%; animation: shimmer 1.4s infinite; }
.skeleton.w-70 { width: 70%; }
.skeleton.w-40 { width: 40%; }
@keyframes shimmer { to { background-position: -200% 0; } }
.timeline { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; font-family: var(--mono); font-size: 0.8rem; }
.timeline .step { padding: 0.1rem 0.5rem; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); }
.timeline .arrow { color: var(--ink-mute); }
.login { max-width: 26rem; margin: 6rem auto; }

/* ── The four screens (UI.md) ───────────────────────────────────────────
   One number per screen is big; everything else is a row. A paragraph
   lives behind something you open, never on the page.                  */

/* Screen 1: a product is a row. dot | id | tenants | figure | bar | rate | cloud */
.product-list { display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
.product-row { display: grid; grid-template-columns: 1.2rem minmax(6rem, max-content) minmax(5rem, max-content) minmax(0, max-content) minmax(8rem, 1fr) max-content max-content; gap: 0 1.25rem; align-items: center; padding: 0.85rem 1rem; border-bottom: 1px solid var(--line); text-decoration: none; transition: background var(--fast) var(--ease); }
.product-row:last-child { border-bottom: 0; }
.product-row:hover { background: var(--surface-hover); }
.product-row .id { font-size: 1.05rem; font-weight: 600; }
.product-row .tenants { color: var(--ink-dim); font-size: 0.88rem; }
.product-row .figure, .product-row .rate { font-size: 0.86rem; white-space: nowrap; }
.product-row .rate, .product-row .cloud { color: var(--ink-dim); }
.product-row .cloud { font-size: 0.86rem; white-space: nowrap; }
/* provider · default marker (empty when not) · the words · the bar; the first two
   fixed so two clouds' lines read as one table. */
.spend-line { display: grid; grid-template-columns: 6rem 11rem max-content minmax(8rem, 12rem); gap: 0 1.25rem; align-items: center; padding: 0.6rem 1rem; color: var(--ink-dim); font-size: 0.86rem; white-space: nowrap; }
.spend-line.unbounded { color: var(--bad); }

/* The dot: colour is the reachability claim, the glyph carries it without colour. */
.dot { font-size: 0.8rem; line-height: 1; }
.dot.pass { color: var(--pass); }
.dot.warn { color: var(--warn); }
.dot.bad { color: var(--bad); }
.dot.live { color: var(--live); }
.dot.plain { color: var(--ink-mute); }

/* Screen 2 and 3: ONE number is big. */
.big { display: flex; flex-direction: column; align-items: center; gap: 0.6rem; padding: 1.5rem 1rem 1.25rem; text-align: center; }
.big .number { font-size: 2rem; font-weight: 600; letter-spacing: -0.02em; }
.big .number .of { color: var(--ink-mute); font-weight: 400; }
.big-bar { width: min(28rem, 100%); }
.big-bar .track { height: 12px; border-radius: 6px; }
.big .trend { color: var(--ink-dim); font-size: 0.9rem; }
.big .at-cap { color: var(--bad); font-weight: 620; letter-spacing: 0.04em; }

/* A paragraph lives behind something you open (rule 2). The summary is the
   badge; the paragraph is the product's own words, verbatim. */
details.holder > summary { list-style: none; cursor: pointer; display: inline-flex; }
details.holder > summary::-webkit-details-marker { display: none; }
details.holder > .detail { max-width: 38rem; margin: 0.6rem auto 0; padding: 0.75rem 1rem; text-align: left; border: 1px solid oklch(0.83 0.15 85 / 0.45); border-radius: var(--radius); background: var(--warn-wash); color: var(--ink-dim); font-size: 0.86rem; }

/* Screen 2: a tenant is a row. id | figure | bar | word | state | → */
.tenant-list { display: flex; flex-direction: column; }
.tenant-row { display: grid; grid-template-columns: minmax(7rem, max-content) max-content minmax(6rem, 1fr) max-content max-content 1rem; gap: 0 1rem; align-items: center; padding: 0.6rem 1rem; border-bottom: 1px solid var(--line); text-decoration: none; font-size: 0.88rem; }
.tenant-row:last-child { border-bottom: 0; }
.tenant-row:hover { background: var(--surface-hover); }
.tenant-row .figure { white-space: nowrap; }
.tenant-row .word { color: var(--ink-dim); font-size: 0.78rem; letter-spacing: 0.03em; white-space: nowrap; }
.tenant-row .word.at-cap { color: var(--bad); font-weight: 620; }
.tenant-row .go { color: var(--ink-mute); }
details.more > summary { padding: 0.6rem 1rem; color: var(--ink-dim); cursor: pointer; font-size: 0.86rem; }
details.more > summary:hover { color: var(--ink); }

/* Screen 2: SILENT and FRICTION — who stopped, and who is being refused.
   A row is id | the words | state | →: no bar, because neither list is a
   standing against a cap, and a track here would invite reading one. */
.flow-list { display: flex; flex-direction: column; }
.flow-row { display: grid; grid-template-columns: minmax(7rem, max-content) minmax(0, 1fr) max-content 1rem; gap: 0 1rem; align-items: center; padding: 0.6rem 1rem; border-bottom: 1px solid var(--line); text-decoration: none; font-size: 0.88rem; }
.flow-row:last-child { border-bottom: 0; }
.flow-row:hover { background: var(--surface-hover); }
.flow-row .figure { color: var(--ink-dim); white-space: nowrap; }
.flow-row .go { color: var(--ink-mute); }

/* Outcomes, not steps (rule 6): when | what | price | mark */
.sale-list { display: flex; flex-direction: column; }
.sale-row { display: grid; grid-template-columns: 3.5rem minmax(0, 1fr) max-content max-content; gap: 0 1rem; align-items: center; padding: 0.6rem 1rem; border-bottom: 1px solid var(--line); font-size: 0.88rem; }
.sale-row:last-child { border-bottom: 0; }
.sale-row .price { white-space: nowrap; }

/* Screen 2: CLOUD — one row per owned resource: id | size | zone · server | → tenant | reference | badge.
   An orphan or a missing resource is a task under NEEDS YOU, in the warn wash;
   what is not ours is one quiet line. */
.cloud-list { display: flex; flex-direction: column; }
.cloud-row { display: grid; grid-template-columns: minmax(8rem, max-content) max-content minmax(0, max-content) max-content minmax(0, 1fr) max-content; gap: 0 1rem; align-items: center; padding: 0.6rem 1rem; border-bottom: 1px solid var(--line); font-size: 0.88rem; }
.cloud-row:last-child { border-bottom: 0; }
.cloud-row .size, .cloud-row .tenant { white-space: nowrap; }
.cloud-row .ref { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.panel > .foreign { padding: 0.5rem 1rem 0.7rem; border-top: 1px solid var(--line); }
.cloud-tasks { border-top: 1px solid var(--line); }
.cloud-tasks > h3 { padding: 0.6rem 1rem 0.3rem; color: var(--ink-dim); font-size: 0.72rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; }
.cloud-task { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; padding: 0.6rem 1rem; background: var(--warn-wash); border-bottom: 1px solid var(--line); font-size: 0.88rem; }
.cloud-task:last-child { border-bottom: 0; }

/* Screen 2: ESTATE — the board, as monetize draws it. Full width of the panel
   and its own aspect ratio: the SVG carries only a viewBox, so an <img> with no
   width would fall back to 300px and render the operator a postage stamp. */
.estate-board { display: block; width: 100%; height: auto; border-radius: 8px; }
.panel-body > .estate-board + .hint { margin-top: 0.6rem; }

/* Screen 3: BACKED BY — one line per thing the tenant stands on, the purchase
   behind it one click away. */
.holding-list { display: flex; flex-direction: column; }
.holding-list details { border-bottom: 1px solid var(--line); }
.holding-list details:last-child { border-bottom: 0; }
.holding-list summary { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 1rem; cursor: pointer; font-size: 0.88rem; }
.holding-list summary:hover { background: var(--surface-hover); }
.holding-list dl.facts { padding: 0.5rem 1rem 0.9rem 2rem; }
.bars .at-cap { color: var(--bad); font-size: 0.72rem; font-weight: 620; letter-spacing: 0.04em; }

/* Screen 3: ACTIVITY — what MOVED, one strip of days per series. Same three
   columns as USING (name | the drawing | the value) so the two panels read as
   one page. Each day is a full-height cell holding a bar of its own height:
   the CELL is what carries the refusal mark, so a day the product said no on
   is still marked when nothing moved and the bar is zero high. A window whose
   maximum is 0 sets every height to 0 and therefore draws nothing at all —
   `fmt::day_bars` decides that, not this file. */
.days { display: grid; grid-template-columns: max-content minmax(0, 1fr) max-content; gap: 0.5rem 1rem; align-items: center; }
.days .name { font-family: var(--mono); font-size: 0.82rem; color: var(--ink-dim); }
.days .value { font-family: var(--mono); font-size: 0.82rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
.strip { display: flex; align-items: flex-end; gap: 2px; height: 34px; }
.strip .day { flex: 1 1 0; min-width: 2px; height: 100%; display: flex; align-items: flex-end; border-bottom: 2px solid transparent; }
/* Only a day that MOVED has a bar element at all (`fmt::day_bars` gives an
   empty day width 0 and the page draws none), so the 1px floor here can make
   a one-percent day visible without inventing a hairline for thirty empty
   ones. */
.strip .day > i { display: block; width: 100%; min-height: 1px; border-radius: 2px 2px 0 0; background: linear-gradient(180deg, var(--live), oklch(0.62 0.11 250)); }
.strip .day.refused { border-bottom-color: var(--bad); }
.days + .axis { display: flex; justify-content: space-between; margin-top: 0.5rem; font-family: var(--mono); }
.axis .refused-key { color: var(--bad); }
details.entitlement { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
details.entitlement > summary { display: flex; align-items: center; gap: 0.5rem; padding: 0.8rem 1rem; cursor: pointer; font-size: 0.94rem; font-weight: 600; }
details.entitlement > dl.facts { padding: 0.75rem 1rem; border-top: 1px solid var(--line); }
details.entitlement > .panel { border: 0; border-top: 1px solid var(--line); border-radius: 0; }

/* Screen 4: NEEDS YOU is the only task; everything else is history behind ⌄. */
.sale-row.needs-you { background: var(--warn-wash); }
.sale-row.needs-you .mark { color: var(--ink-dim); text-decoration: none; }
details.sale-row.disclosed { display: block; padding: 0; }
details.sale-row.disclosed > summary { display: grid; grid-template-columns: 3.5rem minmax(0, 1fr) max-content max-content; gap: 0 1rem; align-items: center; padding: 0.6rem 1rem; cursor: pointer; list-style: none; }
/* A sale row that shows its VAT per order (owner 2026-09-28): one column more. */
.sale-row.taxed, details.sale-row.disclosed.taxed > summary { grid-template-columns: 3.5rem minmax(0, 1fr) max-content max-content max-content; }
.sale-row .tax { white-space: nowrap; }
details.sale-row.disclosed > summary::-webkit-details-marker { display: none; }
details.sale-row.disclosed > summary:hover { background: var(--surface-hover); }
details.sale-row.disclosed > .steps { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; padding: 0.4rem 1rem 0.9rem 4.5rem; }
.timeline .step.pass { border-color: var(--pass); color: var(--pass); }
.timeline .step.warn { border-color: var(--warn); color: var(--warn); }
.timeline .step.bad { border-color: var(--bad); color: var(--bad); }

/* The login's red bulb: lit only when the page was reached by a refusal
   (`?why=session`), never from the menu. It pulses because a still red dot
   beside calm text reads as decoration; the pulse is what says "this is why
   you are here". Reduced-motion viewers get it lit and still.              */
.login-prompt { display: flex; align-items: center; gap: 0.6rem; color: var(--bad); font-weight: 620; background: var(--bad-wash); border-bottom: 1px solid oklch(0.71 0.19 25 / 0.35); }
.bulb { display: inline-block; width: 0.7rem; height: 0.7rem; border-radius: 50%; flex: none; }
.bulb.bad { background: var(--bad); box-shadow: 0 0 0 0 oklch(0.71 0.19 25 / 0.6); animation: bulb 1.1s ease-out infinite; }
@keyframes bulb { 70% { box-shadow: 0 0 0 0.6rem oklch(0.71 0.19 25 / 0); } 100% { box-shadow: 0 0 0 0 oklch(0.71 0.19 25 / 0); } }
@media (prefers-reduced-motion: reduce) { .bulb.bad { animation: none; box-shadow: 0 0 0 0.25rem var(--bad-wash); } }

/* ── Phone width (390 px): one column, and NO horizontal page scroll ─────
   The top bar stays one line; the estate's menu wraps under it (the estate's
   name is already in the picker and in every page heading); the Overview's
   rows become cards; the grid rows of the older screens become wrapping
   lines rather than pushing the page sideways. */
@media (max-width: 760px) {
  .topbar { gap: 0.5rem; padding: 0.4rem 16px; }
  .brand span { display: none; }
  .brand-mark { height: 24px; }
  .estate-picker { flex: 1 1 0; min-width: 0; }
  .estate-picker select { width: 100%; max-width: none; }
  .topbar .spacer { display: none; }
  .nav-link.top { padding: 0.3rem 0.4rem; }
  .estate { display: flex; flex-direction: column; }
  .sidebar { gap: 0.4rem; padding: 0.4rem 16px; border-right: 0; border-bottom: 1px solid var(--line); overflow: visible; flex: none; }
  .estate-name { display: none; }
  .nav-group { flex-direction: row; flex-wrap: wrap; gap: 2px 0.25rem; }
  .nav-link { padding: 0.3rem 0.5rem; }
  .nav-link.sub { padding-left: 0.5rem; }
  .page { padding: 1.25rem 16px 3rem; }
  .page-header { margin-bottom: 1.1rem; }
  .page-header .actions { margin-left: 0; flex-wrap: wrap; }
  .split { grid-template-columns: minmax(0, 1fr); }
  dl.facts { grid-template-columns: minmax(0, 1fr); }
  dl.facts dd { margin-bottom: 0.4rem; }
  .badge { white-space: normal; max-width: 100%; }
  .big .number { font-size: 1.5rem; }
  table.overview thead { display: none; }
  table.overview, table.overview tbody, table.overview tr, table.overview td { display: block; width: auto; }
  table.overview tr { display: grid; grid-template-columns: 128px minmax(0, 1fr); gap: 0.2rem 0.9rem; padding: 0.8rem 1rem; border-bottom: 1px solid var(--line); }
  table.overview td { padding: 0; border: 0; text-align: left; }
  table.overview td.thumb-cell { grid-row: span 6; }
  table.overview tr.estate-slot td[data-label="New estate"]:not(.thumb-cell)::before { content: none; }
  table.overview td[data-label]:not(.thumb-cell):not([data-label="Estate"])::before { content: attr(data-label) " "; color: var(--ink-mute); font-family: var(--sans); font-size: 0.74rem; }
  .product-row, .tenant-row, .flow-row, .sale-row, .sale-row.taxed, .cloud-row, .spend-line, details.sale-row.disclosed > summary, details.sale-row.disclosed.taxed > summary { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.75rem; }
  details.sale-row.disclosed { display: block; }
  .product-row > *, .tenant-row > *, .flow-row > *, .sale-row > *, .cloud-row > *, .spend-line > *, details.sale-row.disclosed > summary > * { min-width: 0; overflow-wrap: anywhere; }
  .sale-row .what, .product-row .bar, .tenant-row .bar, .spend-line .bar, .cloud-row .ref, .flow-row .figure { flex-basis: 100%; }
  .sale-row .what { white-space: normal; }
  .bars, .days, .holdings { grid-template-columns: minmax(0, 1fr); }
  .bars .value, .days .value { white-space: normal; }
  details.sale-row.disclosed > .steps { padding-left: 1rem; }
  .you-menu { right: -8px; }
  .table-wrap { max-width: 100%; }
}

/* CODE RED (2026-10-18): what an automatic path left down, on every page, until seen. */
.code-red { background: oklch(0.45 0.2 25); color: white; padding: 0.6rem 1rem; border-bottom: 3px solid oklch(0.3 0.18 25); animation: code-red-blink 1.2s steps(2, jump-none) infinite; }
.code-red-line { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; align-items: center; }
.code-red-line + .code-red-line { margin-top: 0.4rem; }
.code-red-next { opacity: 0.95; }
.code-red-ack { margin-left: auto; }
.code-red-failed { margin-top: 0.3rem; font-size: 0.9em; }
@keyframes code-red-blink { 50% { background: oklch(0.58 0.22 25); } }
@media (prefers-reduced-motion: reduce) { .code-red { animation: none; } }