/* ==========================================================================
   main.css — the project layer

   SmoothCSS lives in smooth.css (unmodified) and smooth-extras.css (fixes and
   three blocks: .skip-link, .menu, .table-scroll). Both are vendored: to
   upgrade, replace the files. Everything the project itself decides lives
   here.

     <link rel="stylesheet" href="smooth.css">
     <link rel="stylesheet" href="smooth-extras.css">
     <link rel="stylesheet" href="main.css">

   One rule for the two layers this file writes into:

     @layer theme  – tokens only, on :root. It is a new layer name, so it
                     lands after everything SmoothCSS declares and its values
                     win everywhere.
     @layer skins  – everything else, elements and blocks alike. The layer
                     already exists, so these rules beat SmoothCSS's own by
                     source order while `utilities` still beats them.

   Keeping element rules out of `theme` matters in both directions. A rule
   like `label { font-size: … }` in `theme` would outrank the utility classes
   a page uses to override it, and `.button { --button-border-color: … }`
   there would outrank .button.primary's own — which SmoothCSS declares in
   `utilities`, later than the base rule in `skins`.

   Never write unlayered CSS: it beats every layer, including utilities, so a
   stray `.my-list { margin: 0 }` silently kills the `.stack-5` around it.

   ---------------------------------------------------------------------------
   THE LOOK: one typeface, no radius, no filled surfaces.

   Blocks are grouped by distance, never by a box around them: the gap inside
   a group is small, the gap between groups is large. Hairlines exist only
   where the eye needs row help — table rules, the page frame. The page is
   white, black and four greys. Three accent colours are added, and each has
   exactly one job:

     rust  – error. Something is wrong now.
     ochre – warning. Something is about to go wrong.
     green – an event that went well: a command that ran, a login, a save.

   Green marks an event, never a steady state. If "active" were green, most
   rows in a table would be coloured and the one row that is not would drown —
   the whole point of colouring is that there are few of them.

   None of the three is ever used on an action: a red button and a red number
   would mean two different things. Everything coloured also carries a word,
   so colour is never the only channel.
   ========================================================================== */

@layer theme {
  :root {
    --font-family-heading: var(--font-sans-system);
    --font-family-body: var(--font-sans-system);
    --font-weight-heading: var(--font-weight-medium);
    --font-weight-body-emphasis: var(--font-weight-medium);
    --heading-letter-spacing: -0.02ch;
    --line-height-standard: 1.5;
    --step-7: clamp(1.75rem, 1.4rem + 1.5vw, 2.5rem);
    --max-content-inline-size: 60ch;

    --border-radius-1: 0;
    --border-radius-2: 0;
    --border-radius-3: 0;
    --border-radius-4: 0;
    --border-radius-form-input: 0;
    --gutter: var(--space-5-6);
    --button-letter-spacing: 0;

    --color-light-surface-lowest: #ffffff;
    --color-light-surface-mid: #f4f4f4;
    --color-light-surface-highest: #e6e6e6;
    --color-light-on-surface: #111111;
    --color-light-on-surface-variant: #6b6b6b;
    --color-light-outline: #9a9a9a;
    --color-light-outline-variant: #e0e0e0;
    --color-light-primary: #111111;
    --color-light-on-primary: #ffffff;
    --color-light-primary-container: #f4f4f4;
    --color-light-on-primary-container: #111111;
    --color-light-on-primary-container-variant: #6b6b6b;
    --color-light-primary-outline: #9a9a9a;

    --color-dark-surface-lowest: #0f0f0f;
    --color-dark-surface-mid: #1a1a1a;
    --color-dark-surface-highest: #262626;
    --color-dark-on-surface: #f0f0f0;
    --color-dark-on-surface-variant: #9c9c9c;
    --color-dark-outline: #6a6a6a;
    --color-dark-outline-variant: #2c2c2c;
    --color-dark-primary: #f0f0f0;
    --color-dark-on-primary: #0f0f0f;
    --color-dark-primary-container: #1a1a1a;
    --color-dark-on-primary-container: #f0f0f0;
    --color-dark-on-primary-container-variant: #9c9c9c;
    --color-dark-primary-outline: #6a6a6a;

    /* The three accents. Each pair is picked to land in the same contrast
       band in both schemes — around 6:1 on white and 9:1 on black — so no
       one of them shouts louder than the others. */
    --color-light-error: #a3271c;
    --color-light-on-error: #ffffff;
    --color-light-error-container: #f6e8e6;
    --color-light-on-error-container: #6d1810;
    --color-light-on-error-container-variant: #8b2b20;
    --color-light-error-outline: #d5a49d;
    --color-dark-error: #ff9c8f;
    --color-dark-on-error: #4a1009;
    --color-dark-error-container: #33140f;
    --color-dark-on-error-container: #ffd6d0;
    --color-dark-on-error-container-variant: #efa79e;
    --color-dark-error-outline: #6f3129;

    --color-warn: light-dark(#8a6200, #d3a442);
    --color-ok: light-dark(#2d6a4f, #7cc49a);

    --border-form-input: 1px solid var(--color-outline);
    --focus-color: var(--color-primary);
    --shadow-4: none;
    --shadow-5: none;
  }
}

@layer skins {
  /* A short page left the footer floating in the middle of the screen with
     nothing under it, which reads as a page that stopped early. Push it to
     the bottom of the viewport instead. */
  body {
    display: flex;
    flex-direction: column;
    min-block-size: 100dvh;
  }
  /* Only the growing. <main> carries no wrapper of its own — the wrapper is a
     child of it, as in the header and the footer — so it stretches to the
     body's width like any flex item and needs nothing else. Putting .wrapper
     on <main> itself is what made this hard: its margin-inline: auto ate the
     free space instead of stretching, and correcting that with a width
     dragged the gutters inside the max-inline-size, leaving <main> a gutter
     narrower on each side than the two landmarks around it. */
  body > main {
    flex: 1;
  }

  .button {
    --button-font-weight: var(--font-weight-medium);
    --button-radius: 0;
    --button-padding: 0.6em 1em;
    --button-border-color: var(--color-on-surface);
  }
  /* Row actions inside a table, where a full-size button would out-weigh the
     row it belongs to. */
  .button.compact {
    --button-padding: 0.35em 0.7em;
    font-size: var(--step-2);
  }

  /* The badge is words, not a pill: no fill, no border, no padding. The
     tones are the inline counterpart of .alert[data-tone] and follow the same
     rule: green for an event that went well, ochre for a warning, rust for an
     error. */
  .badge {
    white-space: nowrap;
    --badge-background: transparent;
    --badge-color: var(--color-on-surface-variant);
    --badge-padding: 0;
    font-weight: var(--font-weight-regular);
  }
  .badge.ok {
    --badge-color: var(--color-ok);
  }
  .badge.warn {
    --badge-color: var(--color-warn);
  }
  .badge.err {
    --badge-color: var(--color-error);
  }

  hr {
    border-block-start: 1px solid var(--color-outline-variant);
  }

  /* The underline is derived from the text rather than from a grey token: an
     outline-variant hairline is nearly invisible on the dark background
     (#2c2c2c on #0f0f0f), and it stayed grey inside coloured text. Mixing
     with currentColor keeps it subordinate to the word in both schemes, and
     a link inside an error message underlines in the error colour. */
  a {
    text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
    text-underline-offset: 0.25ex;
  }
  a:hover {
    text-decoration-color: currentColor;
  }

  /* One typeface for all text. Monospace only where the markup says the
     machine is speaking — and then without a slab of fill under it. */
  code {
    background: transparent;
    padding-inline: 0;
  }

  input:not([type='checkbox'], [type='radio'], [type='color']),
  select,
  textarea {
    border: 1px solid var(--color-outline);
    background: transparent;
    padding: 0.6em 0.75em;
  }
  /* Focus is a heavier border rather than a ring: the ring would be the only
     rounded thing left on a page with no radius. */
  :where(
    input:not([type='checkbox'], [type='radio'], [type='color'], [readonly]),
    select,
    textarea
  ):focus-visible {
    outline: none;
    border-color: var(--color-on-surface);
    box-shadow: inset 0 0 0 1px var(--color-on-surface);
  }
  label {
    font-size: var(--step-2);
    color: var(--color-on-surface-variant);
    font-weight: var(--font-weight-regular);
  }

  /* SmoothCSS draws row rules as a border-block-start on every row after
     the first, and closes the table with its own border-block-end. The
     hairline under the last row is that closing border, and it closes
     nothing: what follows a table is either a note in a smaller, greyer
     voice or the next section's space, and either separates better than a
     rule does. */
  table {
    --table-border: 1px solid var(--color-outline-variant);
    border: none;
  }
  th {
    background: transparent;
    font-weight: var(--font-weight-regular);
    color: var(--color-on-surface-variant);
  }
  :where(th, td) ~ :where(th, td) {
    border-inline-start: none;
  }
  td,
  th {
    padding-inline: 0 var(--space-4);
    padding-block: var(--space-2);
  }
  :where(td, th):last-child {
    padding-inline-end: 0;
  }
  /* Every cell on this site is structured data — a name, a state, a count,
     a date — and none of it means anything broken across lines: "ventar på
     trafikk" over two rows reads as two things. A table that outgrows the
     column scrolls sideways inside .table-scroll instead. */
  th,
  td {
    white-space: nowrap;
  }


  dialog {
    border-radius: 0;
    box-sizing: border-box;
    inline-size: min(100% - 2rem, var(--wrapper-max-inline-size));
  }
  dialog,
  dialog form {
    --stack-space: 1em;
  }

  /* --- page frame --------------------------------------------------------
     Head and foot are hairlines, not bars: no fill, no shadow. */
  .site-head {
    --region-space: var(--space-4);
    border-block-end: 1px solid var(--color-outline-variant);
  }
  .site-head__brand {
    font-weight: var(--font-weight-medium);
    text-decoration: none;
  }
  .site-foot {
    --region-space: var(--space-5);
    margin-block-start: var(--space-8-9);
    border-block-start: 1px solid var(--color-outline-variant);
    font-size: var(--step-2);
    color: var(--color-on-surface-variant);
  }

  /* The dropdown has no shadow here (--shadow-4 is none), so its border is
     the only thing separating it from the page. The outline-variant hairline
     is too faint for that job. */
  .menu > div {
    border: 1px solid var(--color-outline);
  }
  /* The first row of the account menu is who is signed in — an identity, not
     an action, so it is not a link and does not take the hover wash. */
  .menu > div p.menu__who {
    padding-block-end: var(--space-2);
    color: var(--color-on-surface);
    font-weight: var(--font-weight-medium);
    text-transform: none;
    letter-spacing: 0;
    font-size: var(--step-3);
  }
  .menu > div p.menu__who small {
    display: block;
    color: var(--color-on-surface-variant);
    font-weight: var(--font-weight-regular);
    font-size: var(--step-2);
  }
  /* Logout is a state change, so it has to be a POST — a <form> around a
     <button>, where the other rows are links. The form must not add a row of
     spacing of its own inside the panel. */
  .menu > div form {
    margin: 0;
  }
  .menu > div form > * + * {
    margin-block-start: 0;
  }
  /* On a narrow screen the name (often an email address) crowds the brand out
     of the head, so the trigger collapses to the user's initials — plain
     text, no avatar disc; this design has no filled surfaces. The name is
     visually hidden rather than removed so the button keeps its accessible
     name; the initials are aria-hidden decoration. */
  .menu__initials {
    display: none;
  }
  @media (max-width: 40rem) {
    .menu__initials {
      display: inline;
    }
    .menu__name {
      position: absolute;
      clip-path: inset(50%);
      height: 1px;
      width: 1px;
      overflow: hidden;
      white-space: nowrap;
    }
  }

  /* Small tracked label above a number or a block. */
  .label {
    font-size: var(--step-1);
    color: var(--color-on-surface-variant);
    text-transform: uppercase;
    letter-spacing: 0.1ch;
  }

  /* --- messages ----------------------------------------------------------
     The alert is not a box. The colour runs along the leading edge, because a
     coloured rule across the column reads as a section divider and collides
     with every other horizontal line on the page.

     data-tone comes straight from the flash level in Go (success / error /
     info), so the template needs no branching; "warning" is here for the
     pages that need it. The neutral default is deliberate: an info message is
     not an event worth a colour.

     Dismissible alerts do not carry a close button in the markup —
     smooth-extras.js builds it, so a page without JS has no dead control.
     Mark the alert instead: data-dismissible="Lukk" (the value is the
     button's accessible name, since the button is icon-only). */
  .alert {
    --alert-background: transparent;
    --alert-border: 0;
    --alert-padding: 0;
    border-inline-start: 2px solid var(--color-outline);
    padding-inline-start: var(--space-3);
  }
  .alert:has(.alert__close) {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
  }
  .alert[data-tone='success'] {
    border-inline-start-color: var(--color-ok);
  }
  .alert[data-tone='success'] :is(h1, h2, h3, h4, h5, h6, strong) {
    color: var(--color-ok);
  }
  .alert[data-tone='warning'] {
    border-inline-start-color: var(--color-warn);
  }
  .alert[data-tone='warning'] :is(h1, h2, h3, h4, h5, h6, strong) {
    color: var(--color-warn);
  }
  .alert[data-tone='error'] {
    border-inline-start-color: var(--color-error);
  }
  .alert[data-tone='error'] :is(h1, h2, h3, h4, h5, h6, strong) {
    color: var(--color-error);
  }

  /* The close button follows the message rather than being pushed to the
     trailing edge with an auto margin: the column is wide and the message is
     usually short, so an edge-aligned × ends up an arm's length from the
     thing it closes and stops reading as its control. */
  .alert__close {
    flex: none;
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    block-size: calc(var(--line-height-standard) * 1em);
    padding-inline: 0.25rem;
    border: 0;
    background: transparent;
    color: var(--color-on-surface-variant);
    cursor: pointer;
  }
  .alert__close:hover {
    color: var(--color-on-surface);
  }
  .alert__close > svg {
    inline-size: 1.25rem;
    block-size: 1.25rem;
  }

  /* Field errors. The convention the templates follow is a <div> per field
     holding a <label>, a ul.errorlist when the field is invalid, and
     aria-invalid on the control itself. */
  .errorlist {
    margin: 0;
    padding: 0;
    list-style: none;
    /* Body size, not the label's small size: the error is the one line in
       the field the reader must not miss. */
    font-size: var(--step-3);
    color: var(--color-error);
  }
  /* The field's three lines — label, error, control — stack with the same
     small gap between each, so the error sits as far from the control under
     it as from the label over it. As plain blocks they touched, and the
     label's leading made the gap above the error look twice the one below. */
  form div:has(> label) {
    display: grid;
    gap: var(--space-1);
  }
  form div:has(> label) > * {
    margin-block: 0;
  }
  [aria-invalid='true'] {
    border-color: var(--color-error);
  }

  /* A one-time code is eight characters. A field the width of the column
     says "write an address here"; this one says how much to type. The server
     upper-cases and trims before checking, so the display matches the test. */
  .code-input {
    /* Sized for eight of the widest glyphs plus the letter-spacing and the
       padding. 12ch was the width of twelve zeros, and a code is uppercase
       letters in a proportional face: "GP4DBFVZ" needed 101 px of the 95 it
       had, and a W-heavy code far more, so the last character was clipped. */
    inline-size: 12em;
    text-transform: uppercase;
    letter-spacing: 0.15em;
    font-variant-numeric: tabular-nums;
  }

  /* --- auth pages --------------------------------------------------------
     A login screen is one field and one button. It gets a narrow column and
     nothing else — no card, no border, no fill. The emptiness around it is
     what centres the eye. */
  .auth {
    max-inline-size: 24rem;
  }
  .auth h1 + p {
    color: var(--color-on-surface-variant);
  }

  /* --- panel -------------------------------------------------------------
     The dashboard's own blocks. Same rules as everything above: the number
     carries the weight, nothing is boxed, and colour appears only where
     something is wrong.

     Every number on this page is compared against the one above or below it,
     so all of them are tabular: proportional digits make a column of
     percentages ragged and genuinely harder to scan. */
  .num,
  .stat {
    font-variant-numeric: tabular-nums;
  }

  /* The row of headline figures: no boxes, no hairlines — each figure is a
     tight stack of label, number and graph, and the figures are grouped by
     nothing but distance. The gap within a figure is small, the gap between
     figures is large; that contrast is the whole grouping mechanism, so the
     column gap must stay clearly bigger than anything inside a tile. As a
     cluster the row wraps per figure on a narrow screen instead of snapping
     to a grid column that is wider than the content. */
  .metrics {
    --cluster-gap: var(--space-5) var(--space-7);
    --cluster-vertical-alignment: start;
  }

  /* A page heading and its verdict: one cluster on the baseline, because
     the status word belongs beside the name it describes. Pushing it to the
     trailing edge put a full column of air inside the page's most important
     pairing. */
  .page-head {
    --cluster-gap: var(--space-3);
    --cluster-vertical-alignment: baseline;
  }

  /* Disk rows are a label, an amount and a meter — one group. Left to the
     column width, the amount sat fifty characters from the mount it
     describes and the meter stretched to match; capped, the three stay
     within one glance. */
  .disk-list {
    max-inline-size: 45ch;
  }

  /* One headline figure under a .label, inside a .metrics row. */
  .stat {
    font-size: var(--step-6);
    line-height: 1.1;
    letter-spacing: -0.03ch;
  }
  /* The denominator and the unit are context, not the reading — so they are
     small and grey, and the eye lands on the figure itself. */
  .stat__of,
  .stat__unit {
    font-size: 0.45em;
    color: var(--color-on-surface-variant);
    letter-spacing: 0;
  }
  .stat__unit {
    margin-inline-start: 0.15em;
  }
  /* The figure carries the worst active level, never just "there is
     something": a count of warnings in rust would say "wrong now" about
     things that are merely about to be. Same tones as .alert and .meter. */
  .stat[data-tone='warning'] {
    color: var(--color-warn);
  }
  .stat[data-tone='error'] {
    color: var(--color-error);
  }

  /* A meter is data, not a container, so it stays a hairline's worth of ink:
     a track the weight of a rule with a filled span over it. Neutral by
     default — a bar that is coloured at every level would spend the page's
     one alarm signal on a disk that is 6% full. */
  .meter {
    block-size: 3px;
    background: var(--color-outline-variant);
  }
  .meter > span {
    display: block;
    block-size: 100%;
    inline-size: 0;
    background: var(--color-on-surface-variant);
  }
  /* The fill width comes from the markup's data-fill, in steps of 5, rather
     than from a style attribute: the CSP has no 'unsafe-inline', so a style
     attribute is dropped and every bar would render empty. attr() cannot
     supply a length here yet, so the steps are enumerated. */
  .meter > span[data-fill='0'] {
    inline-size: 0%;
  }
  .meter > span[data-fill='5'] {
    inline-size: 5%;
  }
  .meter > span[data-fill='10'] {
    inline-size: 10%;
  }
  .meter > span[data-fill='15'] {
    inline-size: 15%;
  }
  .meter > span[data-fill='20'] {
    inline-size: 20%;
  }
  .meter > span[data-fill='25'] {
    inline-size: 25%;
  }
  .meter > span[data-fill='30'] {
    inline-size: 30%;
  }
  .meter > span[data-fill='35'] {
    inline-size: 35%;
  }
  .meter > span[data-fill='40'] {
    inline-size: 40%;
  }
  .meter > span[data-fill='45'] {
    inline-size: 45%;
  }
  .meter > span[data-fill='50'] {
    inline-size: 50%;
  }
  .meter > span[data-fill='55'] {
    inline-size: 55%;
  }
  .meter > span[data-fill='60'] {
    inline-size: 60%;
  }
  .meter > span[data-fill='65'] {
    inline-size: 65%;
  }
  .meter > span[data-fill='70'] {
    inline-size: 70%;
  }
  .meter > span[data-fill='75'] {
    inline-size: 75%;
  }
  .meter > span[data-fill='80'] {
    inline-size: 80%;
  }
  .meter > span[data-fill='85'] {
    inline-size: 85%;
  }
  .meter > span[data-fill='90'] {
    inline-size: 90%;
  }
  .meter > span[data-fill='95'] {
    inline-size: 95%;
  }
  .meter > span[data-fill='100'] {
    inline-size: 100%;
  }
  .meter[data-tone='warning'] > span {
    background: var(--color-warn);
  }
  .meter[data-tone='error'] > span {
    background: var(--color-error);
  }

  /* --- sparklines --------------------------------------------------------
     A line and a number, nothing else. No axes, no grid, no fill: the line
     answers "what shape?", and the figure beside it — the window's maximum —
     is what lets that shape be read. A sparkline scaled to its own maximum
     draws a blip and an outage as the same curve; this one does not.

     The stroke is the muted text colour rather than an accent, because the
     graph is a reading, not a verdict. Colour stays reserved for the things
     the page has decided are wrong. */
  .spark {
    display: inline-flex;
    align-items: flex-end;
    gap: var(--space-2);
    vertical-align: bottom;
    /* Never wider than the column: the line yields before the label is
       pushed into the neighbouring tile, and the label may drop below the
       line as a last resort. */
    max-inline-size: 100%;
    flex-wrap: wrap;
    min-inline-size: 0;
  }
  .spark__line {
    display: block;
    flex: 0 1 7.5rem;
    min-inline-size: 3.5rem;
    block-size: 1.5rem;
    overflow: visible;
  }
  .spark__line polyline {
    fill: none;
    stroke: var(--color-on-surface-variant);
    stroke-width: 1.25;
    stroke-linejoin: round;
    stroke-linecap: round;
  }
  .spark__max {
    font-size: var(--step-1);
    color: var(--color-on-surface-variant);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }
  /* A stat tile's graph sits under its figure, and the max label under
     the line — as grid rows, not a wrapped flex line: a flex row's
     intrinsic width is computed as if it never wrapped, so the tile
     would reserve line + label side by side and carry the label's
     width as empty space. Grid rows keep the tile as wide as its
     widest line. */
  .stat + .spark,
  .stat + .note + .spark {
    display: grid;
    justify-items: start;
    gap: var(--space-1);
    margin-block-start: var(--space-2);
  }
  /* In a table cell the graph is shorter, so the row keeps its rhythm —
     and the label stays beside the line: the table scrolls sideways, so
     there is no neighbour to protect. */
  td .spark {
    flex-wrap: nowrap;
  }
  td .spark__line {
    flex-basis: 5rem;
    block-size: 1rem;
  }

  /* Machine IDs, image refs and log lines: monospace, because the markup is
     quoting a machine rather than talking to a reader. */
  .mono,
  .log,
  .log-line {
    font-family: var(--font-mono-system);
  }
  .log,
  .log-line {
    font-size: var(--step-2);
  }
  .log time,
  .log-line time {
    color: var(--color-on-surface-variant);
  }

  /* Log output is the one place on the page where wrapping would destroy the
     shape of the data, so lines scroll sideways as a block instead. */
  .log-lines {
    overflow-x: auto;
    overscroll-behavior-x: contain;
  }
  .log-line {
    margin: 0;
    /* One event, one line: a long message must not push itself under its own
       timestamp — the block scrolls sideways instead. */
    white-space: nowrap;
  }
  /* Only the message keeps its literal shape. Putting pre on the whole line
     would also preserve the template's indentation around the elements. */
  .log-line__msg {
    white-space: pre;
  }
  .log-line + .log-line {
    margin-block-start: var(--space-1);
  }
  .log-line time {
    margin-inline-end: var(--space-2);
  }

  /* The raw log tail folds away under the interesting lines. The summary is
     a control in the aside voice — it says what is behind it and how much
     time it covers, so leaving it closed is an informed choice. */
  .log-details summary {
    cursor: pointer;
    font-size: var(--step-2);
    color: var(--color-on-surface-variant);
  }
  .log-details summary:hover {
    color: var(--color-on-surface);
  }
  .log-details[open] summary {
    margin-block-end: var(--space-3);
  }

  /* An aside under a table or a figure: explains what the reader is looking
     at without competing with it. */
  .note {
    font-size: var(--step-2);
    color: var(--color-on-surface-variant);
  }

  /* Free-form text quoted into a nowrap table cell — a health check's output
     can be a whole error message, and untruncated it would drag the table
     several screens wide. The full text sits in the title attribute. */
  .trunc {
    display: inline-block;
    max-inline-size: 18rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom;
  }

  /* Rows for apps that are not expected to serve (retired, never deployed).
     They stay in the table — leftover volumes still bill — but muted, so the
     eye stops before reading them. */
  tr.inactive :is(td, a) {
    color: var(--color-on-surface-variant);
  }
}
