/* Opti-Q branded internal-tool styling. Replaces Bootstrap 2 + AngularJS; no framework, nothing to
   keep up to date. Dark mode follows the operator's OS setting until they pick one with the toggle in
   the header.

   Colours and type come from the Opti-Q design system via optiq-tokens.css, which must be linked
   first. This file maps them onto the nine variables everything else reads, and that indirection is
   the point: a future re-skin edits the nine light-dark() pairs below and nothing else. Do not reach
   past them to --oq-* anywhere in this file -- the one exception is the chart palette at the bottom,
   which is validated as a set against these two surfaces and is not a brand decision. */

/* Both palettes live in one place, via light-dark(): each token is written once and the *used*
   color-scheme decides which half applies. The alternative -- a light block plus a dark block
   repeated under both a media query and an attribute selector -- means three copies of a nine-token
   palette that can silently drift apart, which is the bug this shape cannot have.

   Setting color-scheme also tells the browser to theme what CSS does not reach: scrollbars, select
   drop-downs, checkboxes and date pickers. Those previously followed the OS regardless of what the
   page was doing, so forcing light on a dark OS used to leave dark scrollbars framing a white page.

   light-dark() needs Chrome 123+/Firefox 120+/Safari 17.5+. That is a real floor, and acceptable
   here for the reason the rest of this file is hand-written: one internal operator, current Chrome.
   Verified in the browser before adopting it, not assumed. */
/* The design system is light-only -- tokens/base.css is headed "light base resets" and colors.css has
   no dark block at all -- so the right-hand half of every pair below is DERIVED, not imported, and each
   one is a decision with a reason. The contrast ratios quoted were computed against the two surfaces,
   not eyeballed; dark mode is the part of this UI most confirmed correct in a browser and it does not
   get to regress into "probably fine".

   One useful coincidence: --oq-grey-900 is #16181d, exactly the dark background this app already used.
   The dark surface does not move, so the chart palette's dark validation still holds. */
:root {
    color-scheme: light dark;

    /* The light page stays WHITE rather than moving to the design system's --surface-page
       (--oq-grey-50). That is a deliberate deviation: the system pairs a grey page with white cards,
       and this app has no card layer -- tables sit directly on the page inside a bordered .grid-wrap --
       so a grey page would put every grid on grey. Keeping white also keeps the chart palette's light
       validation valid, which was done against #ffffff specifically. --oq-grey-50 is not wasted: it is
       the zebra stripe below. */
    --bg:        light-dark(var(--oq-white),     var(--oq-grey-900));

    /* Body text sits between the system's --text-body (grey-700) and --text-strong (grey-900). This app
       has one text token and its densest surface is a grid of small numbers, which wants the extra
       weight. Measured 14.4:1 light, 13.7:1 dark. */
    --fg:        light-dark(var(--oq-grey-800),  var(--oq-grey-200));

    /* NOT the system's --text-muted (grey-500). Muted text is measured against --row-alt as well as
       --bg, because td.wrap -- the Comment and Metadata columns -- is muted text sitting on a zebra
       stripe half the time. grey-500 is 4.5:1 on white but only 4.2:1 on the stripe, so it fails AA
       exactly where most of it appears. grey-600 is 6.9:1 and 6.3:1. Ratios computed, not eyeballed;
       the failing pair is invisible in a screenshot. */
    --muted:     light-dark(var(--oq-grey-600),  var(--oq-grey-400));   /* dark: 6.8:1 / 6.3:1 */

    /* --border-default, not --border-subtle: this is a data-dense tool and grey-200 disappears between
       table rows. */
    --border:    light-dark(var(--oq-grey-300),  var(--oq-grey-800));

    /* The brand purple, and the whole point of the re-skin. #662483 is 9.8:1 on white -- excellent for
       links -- and close to unreadable on the dark surface, so dark lifts to --oq-purple-300, which
       measures 4.9:1. That clears AA for text and is the honest number: purple-200 would reach 7:1 but
       washes out to lavender, and this colour has to carry emphasis, not just legibility.

       Three jobs, which is why the choice is constrained: links, the active sidebar pill, and the
       emphasised line in UsageLinesChart, where one highlighted line must stand clear of eight muted
       ones. Purple is well separated from all three chart hues -- blue, orange, aqua -- in both themes,
       which the blue it replaces was not. */
    --accent:    light-dark(var(--oq-purple),    var(--oq-purple-300));

    /* Text ON the accent. White on purple light (9.8:1); on the lifted purple of dark mode the legible
       choice is the deep end of the same scale, not white -- white on #a870c4 is 2.6:1 and would make
       the active nav item the least readable text on the page. purple-900 gives 5.0:1. */
    --accent-fg: light-dark(var(--oq-white),     var(--oq-purple-900));

    /* --oq-grey-50 is the system's page colour, reused here as the zebra stripe. Dark has no system
       value; #1c1f26 is a derived step between grey-900 and grey-800, kept from before because it is
       the one that reads as a stripe rather than as a separate surface. */
    --row-alt:   light-dark(var(--oq-grey-50),   #1c1f26);

    /* Brand "Mangel" pink-red, 4.9:1 on white -- AA for the alarm headlines and the expired dates it
       carries.

       Dark is a derived tint and had to be. The obvious candidate was --oq-fire (#d35083), the
       Brandschutz accent, on the reasoning that a real brand hex beats an invented one -- but measured
       it is 4.4:1 on #16181d and 4.2:1 on the dark stripe, i.e. it fails AA by a hair in both places.
       So dark is --oq-danger lightened 25% towards white: 5.3:1 on the surface and 4.9:1 on the stripe,
       same hue family. This is the "derive a dark ramp, do not import one" case the backlog warns
       about, and the failure was a rounding error away from shipping. */
    --danger:    light-dark(var(--oq-danger),    #d96592);

    /* NOT --oq-success (#5da023): 3.3:1 on white, which fails AA for the small "yes" it labels. One
       step darker on the brand's own green scale gives 5.0:1. Dark lifts to green-400 (8.8:1). */
    --ok:        light-dark(var(--oq-green-700),  var(--oq-green-400));
}

/* The operator's explicit choice, stamped onto <html> by the head script before first paint and
   again by the toggle. With the attribute absent, the declaration above keeps following the OS --
   so "follow the system" stays the default and is not a third thing to store. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }

* { box-sizing: border-box; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--fg);
    /* Open Sans for UI and body, per the design system. 14px stays: this is a dense operator tool, not
       the 16px marketing baseline the type scale is built around. */
    font: 14px/1.5 var(--font-body);
}

.page { display: flex; min-height: 100vh; }

.sidebar {
    width: 210px;
    flex: 0 0 210px;
    border-right: 1px solid var(--border);
    padding: 1rem 0;
    display: flex;
    flex-direction: column;
}

.sidebar .brand {
    display: flex;
    align-items: center;
    gap: .55rem;
    padding: 0 1rem 1rem;
    font-size: 1.05rem;
    font-family: var(--font-display);
    font-weight: var(--weight-extra);
}

/* The Opti-Q mark. No light plate and no filter in dark mode: this asset has the brand purple as its
   own background, which is exactly why the icon is used here and the wordmark is not. The wordmark is
   purple on transparent and would all but vanish on the dark sidebar. */
.sidebar .brand-icon {
    height: 1.6rem;
    width: auto;
    flex: none;
    border-radius: 3px;
}
.sidebar ul { list-style: none; margin: 0; padding: 0; }
.sidebar ul.secondary { margin-top: auto; border-top: 1px solid var(--border); padding-top: .5rem; }
.sidebar a {
    display: block;
    padding: .45rem 1rem;
    color: var(--fg);
    text-decoration: none;
}
.sidebar a:hover { background: var(--row-alt); }
.sidebar a.active { background: var(--accent); color: var(--accent-fg); }

main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }

main > header {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: 1rem;
    padding: .6rem 1.25rem;
    border-bottom: 1px solid var(--border);
    color: var(--muted);
}

/* Theme toggle, in the header beside Sign out.

   Both icons are always in the DOM, stacked in one grid cell, and which one is visible is decided
   by light-dark() -- the same mechanism as the palette. That matters for more than tidiness: the
   button is server-rendered markup, so anything that swapped its contents in JS would be undone the
   next time Blazor re-rendered the header. Nothing here is stateful, so there is nothing to resync,
   which is the same reasoning that made the item 13 warning panels <details>.

   The icon shows the mode you would switch *to*: a moon while light, a sun while dark.

   Inline SVG rather than an emoji, deliberately -- B3 was an emoji with no glyph in the operator's
   font that drew as an empty box, and the control the page's whole appearance hangs off is the
   worst possible place to repeat that. */
.theme-toggle {
    display: grid;
    place-items: center;
    padding: .3rem;
    border: 1px solid var(--border);
    border-radius: 5px;
    background: none;
    cursor: pointer;
    line-height: 0;
}
.theme-toggle:hover { border-color: var(--muted); }
.theme-toggle svg { grid-area: 1 / 1; width: 16px; height: 16px; }
.theme-toggle .i-sun { color: light-dark(transparent, var(--muted)); }
.theme-toggle .i-moon { color: light-dark(var(--muted), transparent); }
.theme-toggle:hover .i-sun { color: light-dark(transparent, var(--fg)); }
.theme-toggle:hover .i-moon { color: light-dark(var(--fg), transparent); }

/* Language picker, in the header beside the theme toggle.

   A <details> menu like Grid settings, and styled off the same rules, so there is one dropdown idiom in
   this app rather than two. Anchored to its right edge for the same reason .col-menu-list is: it sits
   near the right edge of the viewport, and a box growing rightwards from there would widen the document
   and scroll the whole page sideways. */
.lang { position: relative; }
.lang > summary {
    display: flex;
    align-items: center;
    gap: .4rem;
    cursor: pointer;
    padding: .3rem .55rem;
    border: 1px solid var(--border);
    border-radius: 5px;
    list-style: none;
    font-size: 13px;
}
.lang > summary::-webkit-details-marker { display: none; }
.lang > summary:hover { border-color: var(--muted); }
.lang-list {
    position: absolute;
    right: 0;
    z-index: 5;
    margin-top: .25rem;
    padding: .3rem;
    min-width: 9rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 6px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, .18);
    display: grid;
    gap: .15rem;
}
.lang-list a {
    display: flex;
    align-items: center;
    gap: .45rem;
    padding: .3rem .45rem;
    border-radius: 4px;
    color: var(--fg);
    text-decoration: none;
    white-space: nowrap;
}
.lang-list a:hover { background: var(--row-alt); }
.lang-list a.active { background: var(--accent); color: var(--accent-fg); }

/* 2:1, the proportion both flags are drawn at. The hairline keeps the German flag's white-adjacent
   edges and the Union Jack's blue field from bleeding into the surface behind them. */
.flag {
    flex: none;
    width: 18px;
    height: 9px;
    border: 1px solid var(--border);
    border-radius: 1px;
}

/* Present for screen readers, absent from the page. The toggle's label cannot be visible text
   without the button growing into the header. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

main > article { padding: 1.25rem; min-width: 0; }

/* Nunito for headings, Open Sans everywhere else -- the split the design system specifies. Bold rather
   than the marketing --weight-extra: at 1.4rem on a working screen, 800 reads as shouting. */
h1, h2, h3 {
    font-family: var(--font-display);
    font-weight: var(--weight-bold);
    letter-spacing: -0.01em;
}

h1 { font-size: 1.4rem; margin: 0 0 1rem; }
h2 { font-size: 1.1rem; }

a { color: var(--accent); }

.notice { padding: 2rem; max-width: 40rem; }

/* Wide grids scroll inside their own container; the page never scrolls sideways. */
.grid-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 6px; }

table { border-collapse: collapse; width: 100%; font-size: 13px; }

/* table-layout: fixed is what makes columns resizable at all. With the default `auto`, the browser
   re-derives every width from content on each render, so a width set by dragging is thrown away and
   one long device id stretches its column past the viewport. Fixed means a th width is honoured. */
table[data-grid-key] { table-layout: fixed; }

th, td { padding: .45rem .6rem; text-align: left; }

/* Truncate rather than nowrap. Every cell used to be `white-space: nowrap`, which is why the Usages
   grid was unusable: 14 columns each as wide as its longest value pushed the interesting ones far
   off-screen behind a horizontal scrollbar. Now a cell clips with an ellipsis and the full value is
   in its title attribute. */
table[data-grid-key] th,
table[data-grid-key] td {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

thead th {
    background: var(--row-alt);
    border-bottom: 1px solid var(--border);
    font-weight: 600;
    position: relative;   /* anchors the resize handle */
}

/* Headers stay visible while scrolling a long grid. */
.grid-wrap thead th { position: sticky; top: 0; z-index: 1; }

tbody tr:nth-child(even) { background: var(--row-alt); }

td.wrap { font-size: 12px; color: var(--muted); }

/* Monospace for opaque identifiers: it makes a truncated guid comparable at a glance. */
td.id { font-family: var(--font-mono); font-size: 12px; }

/* Resize handle. Sits in the header's right padding, so it never overlaps the label. */
.col-resize {
    position: absolute;
    top: 0;
    right: 0;
    width: 7px;
    height: 100%;
    cursor: col-resize;
    user-select: none;
}
.col-resize:hover,
body.col-resizing .col-resize { background: var(--accent); opacity: .6; }

/* While dragging, suppress text selection across the whole page. */
body.col-resizing { cursor: col-resize; user-select: none; }

/* .col-hidden is gone with item 12. A hidden column used to be a header and a set of nth-child-matched
   body cells with display:none; since order and visibility moved into Blazor, a hidden column is simply
   not rendered. That also removed the latent bug in the matching: it assumed cell order equalled header
   order, which stopped being true the moment columns could move. */

/* Sortable headers. The label is a button so it is keyboard reachable; the resize handle sits beside
   it and stops its own events, so dragging an edge never triggers a sort. */
thead th .sort {
    all: unset;
    cursor: pointer;
    font: inherit;
    font-weight: 600;
    color: inherit;
    display: block;
    width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
}
thead th .sort:hover { color: var(--accent); }
thead th.sorted { color: var(--accent); }
thead th .sort:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Filter bar */
.filters {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
    align-items: center;
    margin-bottom: .75rem;
    padding: .6rem .7rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--row-alt);
}
.filters label { display: flex; gap: .35rem; align-items: center; font-size: 12px; color: var(--muted); }
.filters select, .filters input { font-size: 13px; }
.filters .spacer { flex: 1; }

/* Grid settings menu. Rendered by GridSettings.razor from the page's column list since item 12 -- it
   was built from the table's headers by grid.js before that, which could not offer reordering because
   order has to be part of Blazor's render. */
.col-menu { position: relative; display: inline-block; }
.col-menu > summary {
    cursor: pointer;
    padding: .35rem .7rem;
    border: 1px solid var(--border);
    border-radius: 5px;
    list-style: none;
}
.col-menu > summary::-webkit-details-marker { display: none; }
.col-menu-list {
    position: absolute;
    /* Anchored to the trigger's RIGHT edge, not its left. With neither offset set, an absolutely
       positioned box starts where it would have sat statically and grows rightwards, so a 14rem menu
       whose trigger is near the right edge pushes the document wider -- and then the whole page
       scrolls sideways, which is the one thing item 1 said must never happen. Measured on Usages,
       whose toolbar is the longest and pushes the trigger furthest right: at a 933px viewport the
       page overflowed by 18px and at 883px by 68px. Anchoring right is enough on its own because the
       trigger's right edge is inside the viewport by definition, so the menu can only grow inwards. */
    right: 0;
    z-index: 5;
    margin-top: .25rem;
    padding: .6rem;
    min-width: 14rem;
    max-height: 60vh;
    overflow-y: auto;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 6px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, .18);
    display: grid;
    gap: .3rem;
}
.col-menu-list label { display: flex; gap: .45rem; align-items: center; font-weight: 400; }
.col-menu-reset { margin-top: .4rem; }

/* One row per column: the visibility checkbox on the left, the two move buttons pushed right. The
   label takes the slack and truncates, so a long column name cannot widen the menu -- which is what
   made the page scroll sideways before the right-anchoring above. */
.col-menu-row { display: flex; align-items: center; gap: .5rem; }
.col-menu-row label { flex: 1; min-width: 0; }
.col-menu-row label span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The list is vertical and the columns are horizontal, so the arrows need the one-line explanation
   above them; see the hint in GridSettings.razor. */
.col-menu-hint { margin: 0 0 .1rem; font-size: 11px; }

.col-menu-move { display: flex; gap: .15rem; flex: none; }
.col-menu-move button {
    padding: 0 .3rem;
    min-width: 1.4rem;
    line-height: 1.4;
    font-size: 11px;
}
/* Disabled at the ends of the row. Kept in place rather than hidden, so the buttons do not jump
   around as a column is walked from one end to the other. */
.col-menu-move button:disabled { opacity: .35; cursor: default; }

/* Footer under a paged grid. */
.pager { display: flex; gap: .75rem; align-items: center; margin-top: .6rem; font-size: 13px; }

.toolbar { display: flex; gap: .5rem; align-items: center; margin-bottom: 1rem; flex-wrap: wrap; }
.toolbar input[type=search] { min-width: 16rem; }

button, .btn {
    font: inherit;
    padding: .35rem .7rem;
    border: 1px solid var(--border);
    border-radius: 5px;
    background: var(--bg);
    color: var(--fg);
    cursor: pointer;
    text-decoration: none;
    display: inline-block;
}
button.primary, .btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
button.danger { color: var(--danger); }
button:disabled { opacity: .5; cursor: default; }

/* A square button carrying one icon, for row furniture that has no room for a word.

   The feature rows on the license page are why it exists: a select, a value box and a "Remove" button
   in a .toolbar, which wraps -- and once the label became "Entfernen" the button wrapped onto its own
   line for every row, so a three-feature license was six lines tall. The icon is the same width in
   every language.

   Inline SVG rather than an emoji or a font glyph, and the label lives in title + aria-label: B3 was an
   emoji with no glyph in the operator's font that drew as an empty box, and an unlabelled icon button
   is a button nobody can identify. */
button.icon-btn {
    display: grid;
    place-items: center;
    flex: none;
    width: 2rem;
    height: 2rem;
    padding: 0;
    line-height: 1;
    color: var(--muted);
}
button.icon-btn:hover { color: var(--fg); border-color: var(--muted); }
button.icon-btn.danger:hover { color: var(--danger); border-color: var(--danger); }
button.icon-btn svg { width: 15px; height: 15px; }

/* One row of the license page's feature editor: the key, its value, and the delete button.

   Not a .toolbar, which is what it used to be, because a toolbar wraps -- and the row it wraps inside is
   `form.stack`, which is max-width: 34rem whatever the window is doing. A select holding 26 characters of
   feature name plus a value box fills that on its own, so the last item dropped onto a line of its own.
   That was true of the "Entfernen" button and it stayed true of a 2rem icon, which is what made the
   button look like the problem when the wrapping was.

   So: never wrap, and let the two fields shrink instead. min-width: 0 is the load-bearing part -- a flex
   item will not shrink below its intrinsic content width without it, and a <select> holding
   "License.AllowMultipleUsagesPerUser" has a very wide one. The select ellipsises what it cannot show,
   and its full text is in the dropdown anyway. */
.feature-row {
    display: flex;
    gap: .5rem;
    align-items: center;
    margin-bottom: .5rem;
    flex-wrap: nowrap;
}
.feature-row select { flex: 1 1 12rem; min-width: 0; }
.feature-row input { flex: 1 1 5rem; min-width: 0; }

input, select, textarea {
    font: inherit;
    padding: .35rem .5rem;
    border: 1px solid var(--border);
    border-radius: 5px;
    background: var(--bg);
    color: var(--fg);
}

form.stack { display: grid; gap: .85rem; max-width: 34rem; }
form.stack label { display: grid; gap: .25rem; font-weight: 600; }
form.stack .hint { font-weight: 400; color: var(--muted); font-size: 12px; }

.validation-message, .error { color: var(--danger); }
.success { color: var(--ok); }
.muted { color: var(--muted); }
.expired { color: var(--danger); font-weight: 600; }

dl.detail { display: grid; grid-template-columns: max-content 1fr; gap: .4rem 1rem; }
dl.detail dt { color: var(--muted); }
dl.detail dd { margin: 0; word-break: break-all; }

/* Opti-Q wordmark on the sign-in page.

   Dark mode gets a white HALO around the letterforms, not a plate behind them. The plate worked but
   looked like a sticker: a hard white rectangle floating on a near-black card.

   A glow via drop-shadow is brand-safe in the way that matters -- it adds light around the glyphs and
   leaves every brand hex untouched. brightness() or invert() would have been easier and would have
   altered the purple, which the design system explicitly forbids.

   This is the one place the file cannot use light-dark(): that function produces a COLOR, and filter
   takes a filter list. So the dark case is stated three times -- once for the OS preference, once for
   the explicit dark choice, and once to force it off for an operator who picked light on a dark machine.
   The order matters: the attribute selectors must come after the media query to win. This is exactly the
   duplication the palette comment at the top of the file avoids, and it is unavoidable for a
   non-colour property. */
.brand-wordmark {
    display: block;
    width: 11rem;
    max-width: 100%;
    height: auto;
    /* Left, not centred: the heading, the labels and the form below are all left-aligned, and a
       centred mark was the only centred thing on the card. */
    margin: 0 0 .9rem;
}

:root {
    /* Two stops: a tight one to separate the glyph edges from the background, and a wider soft one so
       the mark sits in its own light rather than looking outlined. */
    --wordmark-glow: drop-shadow(0 0 1px #fff) drop-shadow(0 0 5px rgba(255, 255, 255, .75));
}

@media (prefers-color-scheme: dark) {
    .brand-wordmark { filter: var(--wordmark-glow); }
}

:root[data-theme="dark"] .brand-wordmark { filter: var(--wordmark-glow); }
:root[data-theme="light"] .brand-wordmark { filter: none; }

/* ---- dashboard warnings -------------------------------------------------------------------

   The dashboard leads with what is wrong and is silent when nothing is. These carry the only
   colour on the page, so they have to earn it: .urgent means devices are on a license that is
   about to run out (or just did), and nothing else turns red. .quiet is the same panel when the
   expiring licenses have nobody on them -- present, readable, not shouting. */
details.alarm {
    margin: 0 0 1rem;
    border: 1px solid var(--border);
    border-left: 4px solid var(--border);
    border-radius: 6px;
    padding: .6rem .8rem;
    background: var(--row-alt);
}
details.alarm > summary {
    cursor: pointer;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .25rem .75rem;
}
details.alarm .alarm-head { font-weight: 600; }
details.alarm .alarm-sub { font-size: 12px; }
details.alarm[open] > summary { margin-bottom: .7rem; }

details.alarm.urgent { border-left-color: var(--danger); }
details.alarm.urgent .alarm-head { color: var(--danger); }

/* The all-clear. A line, not a badge -- it is the absence of news. */
.all-clear { margin: 0 0 1rem; }

/* Row weight follows the tier, and it is the ranking that does the work here: a license with
   devices on it reads first and fully, one that has merely gone quiet recedes. */
tbody tr.tier-noted td { color: var(--muted); }
tbody tr.tier-dormant td { color: var(--muted); }

/* The never-activated pile, folded inside the panel. Two clicks deep on purpose. */
details.dormant { margin-top: .6rem; }
details.dormant > summary { cursor: pointer; font-size: 12px; }
details.dormant[open] > summary { margin-bottom: .5rem; }

code { font-family: var(--font-mono); font-size: 12px; }

/* ---- charts ------------------------------------------------------------------------------

   Three categorical hues plus a muted step for "we could not tell". Validated as a set against
   this app's own two surfaces (#ffffff light, #16181d dark), not against a generic default:
   lightness band, chroma floor, adjacent CVD separation and normal-vision separation all pass in
   both modes. On the light surface aqua and orange sit below 3:1 against white, which is why the
   legend prints each series' value next to its name -- the number, not the hue, is what carries
   the reading there. Dark is its own set of steps, not an automatic flip. */
.viz {
    --viz-1: light-dark(#2a78d6, #3987e5);      /* blue    — Windows */
    --viz-2: light-dark(#eb6834, #d95926);      /* orange  — iOS */
    --viz-3: light-dark(#1baf7a, #199e70);      /* aqua    — Android */
    --viz-none: #898781;                        /* muted   — Unknown, one value for both */
}

.viz h3 { font-size: 1rem; margin: 1.25rem 0 .5rem; }

/* A window picker sitting in a heading, so it reads as part of the title. */
select.inline { font-size: .8rem; font-weight: 400; vertical-align: middle; }

.viz-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem 1.25rem; }
.viz-legend { display: flex; flex-wrap: wrap; gap: .25rem 1rem; font-size: 12px; color: var(--muted); }
.viz-key { display: inline-flex; align-items: center; gap: .35rem; }
.viz-key strong { color: var(--fg); font-variant-numeric: tabular-nums; }
.viz-swatch { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }

.viz-chart { width: 100%; height: auto; max-width: 60rem; display: block; }

/* The dashboard's version. Narrower as well as shorter, so it reads as context beside the
   warnings rather than competing with them -- but not so narrow that the 10px tick labels in a
   720-wide viewBox scale below legibility (42rem/720 keeps them at ~9.3px). */
.viz-chart.compact { max-width: 42rem; }

/* ---- customer lines ----------------------------------------------------------------------

   Eight or nine customers is past what a categorical palette can carry, so identity is not
   carried by colour at all: every line is the same muted ink, hovering dims the set and lifts
   one to the accent, and the names are in the table underneath. Adding more hues would be the
   wrong fix -- see the series-count ladder. */
.viz-lines { width: 100%; height: auto; max-width: 60rem; display: block; }
.viz-lines.compact { max-width: 34rem; }

.viz-lines .viz-line path {
    fill: none;
    stroke: var(--viz-none);
    stroke-width: 1.5;
    stroke-linejoin: round;
    stroke-linecap: round;
}

/* "Other" is the summed tail, not a customer. Dashed so it is not read as one company. */
.viz-lines .viz-line.other path { stroke-dasharray: 4 3; }

/* A fat invisible stroke under each line: 1.5px is not something anyone can hover on purpose. */
.viz-lines .viz-line-hit { stroke: transparent !important; stroke-width: 12; }

.viz-lines:hover .viz-line path { opacity: .25; }
.viz-lines .viz-line:hover path { opacity: 1; stroke: var(--accent); stroke-width: 2.25; }

/* Fold: the small chart is the summary, so clicking the picture opens the detail. */
details.viz-fold > summary { cursor: pointer; display: block; }
details.viz-fold > summary::marker { color: var(--muted); }
details.viz-fold .viz-fold-label { display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap; }
details.viz-fold .viz-fold-label h3 { display: inline; }
details.viz-fold .viz-fold-label span { font-size: 12px; }
/* Once open, the small preview in the summary would be a second copy of the same chart. */
details.viz-fold[open] > summary .viz-lines { display: none; }

.viz-grid { stroke: var(--border); stroke-width: 1; }
.viz-axis { stroke: var(--border); stroke-width: 1; }
.viz-tick { fill: var(--muted); font-size: 10px; font-variant-numeric: tabular-nums; }

.viz-spark { width: 140px; height: 26px; display: block; }
.viz-spark-line { fill: none; stroke: var(--viz-1); stroke-width: 1.5; }
.viz-spark-fill { fill: var(--viz-1); opacity: .16; stroke: none; }

.viz-table td { font-variant-numeric: tabular-nums; }
.viz-table { table-layout: fixed; }
.viz-table th, .viz-table td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
