/* ==========================================================================
   energylog — the design system

   Four looks, one set of components.

   The four are not four stylesheets. They are one component library reading one
   set of tokens, and each look supplies different values for those tokens plus
   a small number of structural switches. That is what makes it possible to have
   four of them at all: a bug fixed in the site list is fixed in every look, and
   a new screen gets all four for free.

     console   dark, dense, industrial — the plant-room reading
     bright    light, airy, generous — the calm reading
     bold      colour blocks and very large type — the across-the-room reading
     spatial   led by where things are rather than what they are called

   The look is set with data-look on <html>; light and dark within a look with
   data-theme. Console is dark by nature and bright is light by nature, but both
   honour the theme switch, because a phone in daylight and a wall screen at
   night are the same estate.

   Colour was chosen by measurement, not by eye. The status ramp is ordered —
   ok, warm, high, critical — so it is a sequential scale and adjacent steps are
   meant to resemble each other; what matters is that each step is separable
   from its neighbours, which was checked in OKLab under normal vision and under
   all three dichromacies, on both surfaces:

                     normal   prot   deut   trit
     light  ok/warm    31.2   21.7   29.3   45.7
            warm/high  20.4   22.5   14.6    8.9
            high/crit  21.1   22.2   21.0   20.9
     dark   ok/warm    21.7   12.6   19.5   38.2
            warm/high  18.8   20.2   14.6    9.7
            high/crit  22.1   26.1   17.6   12.9

   Every band still ships as icon + number + word. The colour is the fourth
   signal, never the first.
   ========================================================================== */

/* --------------------------------------------------------------- tokens */

:root {
  /* Brand. Violet replaces the old teal — it sits clear of the whole status
     ramp, which green did not, and holds up on both surfaces. */
  --brand:        #6d28d9;
  --brand-ink:    #ffffff;
  --brand-soft:   rgba(109, 40, 217, .12);

  /* Status marks — fills, dots, borders, bar segments. */
  --ok:       #166534;
  --warm:     #eab308;
  --high:     #ea580c;
  --critical: #9f1239;

  /* Status text. A mark only has to be seen; a word has to be read, and
     #eab308 on white is 1.92:1. These are the same hues carried down to
     something legible. */
  /*
   * Measured against the pill these words actually sit on, which is a 14% tint
   * of the MARK colour — not against white. That distinction is the whole bug:
   * sized against white these looked fine, but the tint is darker than white,
   * so "Normal" measured 4.15:1 and "High" 4.36:1 on the real background.
   * Warm (6.25) and critical (6.22) already cleared it and are unchanged.
   */
  --ok-text:       #166534;
  --warm-text:     #854d0e;
  --high-text:     #9a3412;
  --critical-text: #9f1239;

  /* Ink to use ON a filled status tile — the same rule as above, running the
     other way. The bold look fills a whole card with --ok or --critical, which
     turns a MARK colour into a SURFACE, and the text on it has to be chosen for
     that surface rather than assumed white. It cannot be one fixed colour: --ok
     is a dark forest green here and a light mint in the dark theme, so white is
     right on one and near-invisible on the other. */
  --on-ok:       #ffffff;
  --on-critical: #ffffff;
  /* --warm is a light yellow in BOTH themes — the one status token whose
     ink does not flip, so one fixed dark brown serves both. Measured on
     the fill itself: 7.60:1 on #eab308, 11.06:1 on #fde047. */
  --on-warm:     #422006;

  /* Sequential heat ramp, six steps, one hue family, light to dark. */
  --heat-1: #fef3c7;
  --heat-2: #fde68a;
  --heat-3: #fbbf24;
  --heat-4: #f97316;
  --heat-5: #dc2626;
  --heat-6: #7f1d1d;

  /* Geometry and motion. Looks override these; components never hard-code. */
  --radius:     14px;
  --radius-sm:  9px;
  --radius-lg:  20px;
  --gap:        14px;
  --pad:        16px;
  --tap:        44px;
  --rail:       232px;

  --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
  --display: var(--font);

  /* Type scale. Bold and wall displays push this up bodily rather than
     restyling every rule. */
  --step: 1;
  --t-micro: calc(.68rem * var(--step));
  --t-small: calc(.78rem * var(--step));
  --t-body:  calc(.88rem * var(--step));
  --t-head:  calc(1.05rem * var(--step));
  --t-title: calc(1.5rem * var(--step));
  --t-hero:  calc(2.8rem * var(--step));

  --speed: .18s;
}

/* ------------------------------------------------------------- surfaces */

/*
 * Light and dark are two sets of the same five surfaces. Every look picks from
 * these rather than inventing its own, so a component written once is legible
 * in all eight combinations.
 */
:root,
:root[data-theme="light"] {
  color-scheme: light;
  --page:      #f6f6f8;
  --surface:   #ffffff;
  --sunk:      #eeeef2;
  --raised:    #ffffff;
  --ink:       #0f1115;
  --ink-2:     #454a54;
  /* Was #6b7280, which measures 4.48:1 on the card — under AA by a rounding
     error, on the timestamps under every site name. */
  --ink-muted: #62697a;
  --line:      rgba(15, 17, 21, .11);
  --line-firm: rgba(15, 17, 21, .22);
  --shadow:    0 1px 2px rgba(15,17,21,.05), 0 8px 24px rgba(15,17,21,.06);
  --lift:      0 2px 6px rgba(15,17,21,.07), 0 18px 44px rgba(15,17,21,.12);
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --page:      #0d0f13;
  --surface:   #14161a;
  --sunk:      #1b1e24;
  --raised:    #1e222a;
  --ink:       #f3f4f6;
  --ink-2:     #c2c7d0;
  --ink-muted: #8b919d;
  --line:      rgba(243, 244, 246, .12);
  --line-firm: rgba(243, 244, 246, .26);
  --shadow:    0 1px 2px rgba(0,0,0,.5), 0 8px 24px rgba(0,0,0,.4);
  --lift:      0 2px 6px rgba(0,0,0,.55), 0 20px 50px rgba(0,0,0,.55);

  --brand:      #a78bfa;
  --brand-ink:  #1a1030;
  --brand-soft: rgba(167, 139, 250, .16);

  --ok:       #34d399;
  --warm:     #fde047;
  --high:     #fb923c;
  --critical: #e11d48;

  --ok-text:       #34d399;
  --warm-text:     #fde047;
  --high-text:     #fb923c;
  --critical-text: #fb7185;

  /* Mint on near-black is the right MARK here, and the wrong surface for white
     text: measured 1.92:1 on the "all clear" tile, 1.70:1 on its label. Crimson
     stays light-on-dark because it is dark enough to carry it (4.72:1). */
  --on-ok:       #0d0f13;
  --on-critical: #ffffff;
  --on-warm:     #422006;

  --heat-1: #23212a;
  --heat-2: #4a3324;
  --heat-3: #7a4526;
  --heat-4: #ad5527;
  --heat-5: #e0672f;
  --heat-6: #fca55b;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --page:      #0d0f13;
    --surface:   #14161a;
    --sunk:      #1b1e24;
    --raised:    #1e222a;
    --ink:       #f3f4f6;
    --ink-2:     #c2c7d0;
    --ink-muted: #8b919d;
    --line:      rgba(243, 244, 246, .12);
    --line-firm: rgba(243, 244, 246, .26);
    --shadow:    0 1px 2px rgba(0,0,0,.5), 0 8px 24px rgba(0,0,0,.4);
    --lift:      0 2px 6px rgba(0,0,0,.55), 0 20px 50px rgba(0,0,0,.55);
    --brand:      #a78bfa;
    --brand-ink:  #1a1030;
    --brand-soft: rgba(167, 139, 250, .16);
    --ok: #34d399; --warm: #fde047; --high: #fb923c; --critical: #e11d48;
    --ok-text: #34d399; --warm-text: #fde047;
    --high-text: #fb923c; --critical-text: #fb7185;
    --on-ok: #0d0f13; --on-critical: #ffffff; --on-warm: #422006;
    --heat-1: #23212a; --heat-2: #4a3324; --heat-3: #7a4526;
    --heat-4: #ad5527; --heat-5: #e0672f; --heat-6: #fca55b;
  }
}

/* ============================================================== the looks */

/*
 * CONSOLE — the plant room.
 *
 * Square corners, hairline rules, tight spacing, monospaced figures. It reads
 * as instrumentation because instrumentation is square, dense and numeric; the
 * moment you round the corners and add air it starts reading as a website.
 */
:root[data-look="console"] {
  --radius: 3px; --radius-sm: 2px; --radius-lg: 4px;
  --gap: 8px; --pad: 11px;
  --shadow: none;
  --lift: 0 8px 28px rgba(0,0,0,.5);
  --display: var(--mono);
  --step: .94;
}
:root[data-look="console"] .card,
:root[data-look="console"] .panel { border-width: 1px; }
:root[data-look="console"] .num,
:root[data-look="console"] .stat-v { font-family: var(--mono); letter-spacing: -.02em; }
/* Console is dark unless somebody insists otherwise. */
:root[data-look="console"]:not([data-theme="light"]) { color-scheme: dark; }

/*
 * BRIGHT — the calm reading.
 *
 * Soft, generous, low contrast between elements and plenty of room around
 * them. The estate is mostly hospitals; this is the look for an administrator
 * who wants to know everything is fine, not an engineer hunting a fault.
 */
:root[data-look="bright"] {
  --radius: 18px; --radius-sm: 12px; --radius-lg: 26px;
  --gap: 18px; --pad: 22px;
  --step: 1;
}
:root[data-look="bright"] .card { border-color: transparent; box-shadow: var(--shadow); }

/*
 * BOLD — across the room.
 *
 * The figure is the design. Type goes up bodily through --step, colour arrives
 * in blocks rather than accents, and anything that is not a number gets out of
 * the way. This is the look that survives being read from six metres.
 */
:root[data-look="bold"] {
  --radius: 6px; --radius-sm: 4px; --radius-lg: 10px;
  --gap: 12px; --pad: 20px;
  --step: 1.18;
  --shadow: none;
}
:root[data-look="bold"] .card { border-width: 2px; }
:root[data-look="bold"] .stat-v { font-weight: 800; letter-spacing: -.045em; }
:root[data-look="bold"] .stat-k {
  text-transform: uppercase; letter-spacing: .1em; font-weight: 700;
}

/*
 * SPATIAL — where things are.
 *
 * Structurally the same components, but the estate is drawn as places rather
 * than listed as names. The plan itself is a separate view; what this look
 * changes globally is that grouping by location leads and the type stays quiet
 * so the plan is the loudest thing on screen.
 */
:root[data-look="spatial"] {
  --radius: 10px; --radius-sm: 7px; --radius-lg: 16px;
  --gap: 14px; --pad: 16px;
  --step: .96;
}

/* Wall mode: any look, one notch larger and non-interactive. */
:root[data-density="wall"] { --step: 1.45; --gap: 20px; --pad: 24px; }

/* ---------------------------------------------------------------- base */

* { box-sizing: border-box; }

html, body {
  margin: 0;
  background: var(--page);
  color: var(--ink);
  font-family: var(--font);
  font-size: 16px;
  -webkit-text-size-adjust: 100%;
}

a { color: inherit; }
button { font: inherit; color: inherit; }

:is(a, button, input, select, [tabindex]):focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.num { font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------- layout */

/*
 * One frame, three shapes.
 *
 * Phone gets a single column with the bar at the bottom where a thumb is.
 * Desktop gets a rail down the side, because a persistent rail costs nothing
 * horizontally on a wide screen and saves a tap every time. A wall display gets
 * the rail collapsed to icons, since nobody is going to click it.
 */
.app { min-height: 100dvh; display: flex; flex-direction: column; }

.rail { display: none; }

.main { flex: 1; min-width: 0; padding: var(--pad); padding-bottom: 88px; }

.wrap { max-width: 1680px; margin: 0 auto; }

@media (min-width: 900px) {
  .app { flex-direction: row; }
  .rail {
    display: flex; flex-direction: column; gap: 4px;
    width: var(--rail); flex: none;
    padding: 18px 12px;
    background: var(--surface);
    border-right: 1px solid var(--line);
    position: sticky; top: 0; height: 100dvh;
  }
  .main { padding: 26px 30px; padding-bottom: 40px; }
  .tabbar { display: none; }
}

/*
 * Wall mode: icons only, but never a room you cannot leave.
 *
 * The first version hid the whole rail footer, which is where the button that
 * turns wall mode off lives — so switching it on removed the only way back and
 * left the user stuck with a stranger's screen. Only the labels hide now. The
 * buttons stay, as icons, and Escape exits as well.
 */
:root[data-density="wall"] .rail { width: 84px; align-items: center; }
:root[data-density="wall"] .rail .rail-label { display: none; }
:root[data-density="wall"] .rail-item { justify-content: center; padding: 12px 0; }
:root[data-density="wall"] .rail-item .ic { width: auto; font-size: 1.3em; }
:root[data-density="wall"] .rail-item .badge {
  margin: 0; position: absolute; transform: translate(14px, -12px);
}
:root[data-density="wall"] .rail-item { position: relative; }

/* The wordmark does not fit an 84px rail, and letting it overflow drew it
   across the page title. The mark alone carries the brand at this size. */
:root[data-density="wall"] .brand { justify-content: center; padding: 4px 0 20px; }
/* The wordmark is a bare text node beside a <b>, so :not() cannot reach it.
   Collapsing the font size hides both; the dot restores its own. */
:root[data-density="wall"] .brand { font-size: 0; }
:root[data-density="wall"] .brand-dot { width: 20px; height: 20px; font-size: 1rem; }

/* ----------------------------------------------------------------- rail */

.brand {
  display: flex; align-items: center; gap: 9px;
  padding: 4px 10px 18px;
  font-size: var(--t-title); font-weight: 700; letter-spacing: -.03em;
}
.brand b { color: var(--brand); font-weight: 700; }
/* One flex item, so the 9px gap sits between the mark and the word only —
   as bare siblings "energy" and "log" were split into "energy log". */
.brand-word { white-space: nowrap; }
.brand-dot {
  width: 10px; height: 10px; border-radius: 3px; flex: none;
  background: var(--brand);
}

.rail-item {
  display: flex; align-items: center; gap: 11px;
  padding: 10px 12px; min-height: var(--tap);
  border: 0; background: none; border-radius: var(--radius-sm);
  font-size: var(--t-body); font-weight: 560; color: var(--ink-2);
  text-decoration: none; cursor: pointer; width: 100%;
  transition: background var(--speed), color var(--speed);
}
.rail-item:hover { background: var(--sunk); color: var(--ink); }
.rail-item[aria-current="page"] {
  background: var(--brand-soft); color: var(--brand); font-weight: 660;
}
.rail-item .ic { width: 20px; text-align: center; flex: none; font-size: 1.05em; }
.rail-item .badge {
  margin-left: auto; padding: 1px 7px; border-radius: 999px;
  background: var(--critical); color: #fff;
  font-size: var(--t-micro); font-weight: 700;
}
/* Only sites that have stopped measuring, nothing over threshold: the held
   hue the site cards use, not the alarm red. */
.rail-item .badge.badge-held { background: var(--brand); color: var(--brand-ink); }

.rail-foot { margin-top: auto; padding-top: 14px; border-top: 1px solid var(--line); }

/* Phone bar. Kept, because at that size it is the right answer. */
.tabbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  display: flex; justify-content: space-around;
  padding: 6px 4px calc(6px + env(safe-area-inset-bottom));
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  backdrop-filter: saturate(1.6) blur(14px);
  border-top: 1px solid var(--line);
}
.tabbar a {
  flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 6px 2px; min-height: var(--tap);
  font-size: var(--t-micro); font-weight: 620;
  color: var(--ink-muted); text-decoration: none;
}
.tabbar a[aria-current="page"] { color: var(--brand); }
.tabbar .ic { font-size: 1.15rem; line-height: 1; }

/* ----------------------------------------------------------------- topbar */

.topbar {
  display: flex; align-items: center; gap: 12px;
  margin-bottom: var(--gap); flex-wrap: wrap;
}
.topbar h1 {
  margin: 0; font-size: var(--t-title); font-weight: 700; letter-spacing: -.03em;
  font-family: var(--display); flex: 1; min-width: 0;
}
.topbar .sub { font-size: var(--t-small); color: var(--ink-muted); }

/* The look switch. Four buttons, because the point of four looks is choosing. */
.looks {
  display: inline-flex; gap: 2px; padding: 3px;
  background: var(--sunk); border-radius: 999px;
}
.looks button {
  padding: 6px 13px; min-height: 34px;
  border: 0; background: none; border-radius: 999px;
  font-size: var(--t-small); font-weight: 600; color: var(--ink-muted);
  cursor: pointer; white-space: nowrap;
  transition: background var(--speed), color var(--speed);
}
.looks button[aria-pressed="true"] {
  background: var(--surface); color: var(--ink); box-shadow: var(--shadow);
}

/*
 * 34px is a comfortable segmented control under a mouse and a small one under a
 * thumb — it clears the 24px minimum but not the 44px a finger wants, which is
 * what --tap is set to and what every other control here already uses. Raised
 * on coarse pointers only: on a desktop the compact bar is the better design,
 * and making it 44px everywhere would push the header taller for no gain.
 */
@media (pointer: coarse) {
  .looks button { min-height: var(--tap); padding-inline: 15px; }
}

.icon-btn {
  width: 38px; height: 38px; flex: none;
  display: grid; place-items: center;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink-2); cursor: pointer;
}
.icon-btn:hover { background: var(--sunk); color: var(--ink); }

/* ------------------------------------------------------------------ cards */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--pad);
}

.grid { display: grid; gap: var(--gap); }
.grid.stats { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
/* min(): on a 320px phone the report cards' box is 276px, and a bare 320px
   floor made every card 320 wide and the page scroll sideways. */
.grid.two   { grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr)); }
.grid.sites { grid-template-columns: repeat(auto-fill, minmax(268px, 1fr)); }

.stat-k {
  display: block; font-size: var(--t-small); color: var(--ink-muted);
  font-weight: 600; margin-bottom: 6px;
}
.stat-v {
  font-family: var(--display);
  font-size: var(--t-hero); font-weight: 720; letter-spacing: -.04em;
  line-height: 1.02; display: flex; align-items: baseline; gap: 5px;
}
.stat-v .u { font-size: var(--t-head); font-weight: 600; color: var(--ink-2); }
.stat-s { margin-top: 5px; font-size: var(--t-small); color: var(--ink-muted); }

/* Bold turns a stat into a colour block rather than a card. */
/*
 * The tone tiles take their ink from the fill, not from an assumption.
 *
 * These were hard-coded to white. That is right on crimson in either theme and
 * on the light theme's forest green, and wrong on the dark theme's mint, where
 * the "all clear" tile measured 1.92:1 and its label 1.70:1 — on the Now screen,
 * in the look whose whole purpose is being readable across a plant room.
 */
:root[data-look="bold"] .stat.tone-crit { background: var(--critical); color: var(--on-critical); border-color: transparent; }
:root[data-look="bold"] .stat.tone-crit :is(.stat-k, .stat-s, .u) { color: var(--on-critical); }
:root[data-look="bold"] .stat.tone-ok   { background: var(--ok); color: var(--on-ok); border-color: transparent; }
:root[data-look="bold"] .stat.tone-ok :is(.stat-k, .stat-s, .u) { color: var(--on-ok); }
/* The bold look says a state by filling the tile, so a state with no fill
   reads as "no state" — and this one is the opposite of nothing to say: the
   all-clear above it does not cover the whole estate. Without this rule the
   warning tile is the only untoned card in the row. */
:root[data-look="bold"] .stat.tone-warm { background: var(--warm); color: var(--on-warm); border-color: transparent; }
:root[data-look="bold"] .stat.tone-warm :is(.stat-k, .stat-s, .u) { color: var(--on-warm); }

/*
 * The labels on a filled tile take the full ink, not a faded one.
 *
 * They were held at 82% opacity to sit under the figure. On the light theme's
 * forest green that measured 3.92:1 — the label was the least legible thing on
 * the most legible screen. Getting 82% back over the line needs about 94%,
 * which is indistinguishable from opaque, so the fade buys nothing and costs
 * the contrast. The hierarchy is already carried by size and weight: the figure
 * is 65px, the label 18px. Opacity was never what separated them.
 */

/* ------------------------------------------------------------------ bands */

/*
 * A banded reading is always icon + number + word. Colour is the fourth
 * signal; the first three survive a monochrome print and a colour-blind
 * reader, which is the whole point of a reserved status palette.
 */
.band {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 9px; border-radius: 999px;
  font-size: var(--t-micro); font-weight: 680; white-space: nowrap;
}
.band .ic { font-size: .95em; }
.band-ok       { color: var(--ok-text);       background: color-mix(in srgb, var(--ok) 14%, transparent); }
.band-warm     { color: var(--warm-text);     background: color-mix(in srgb, var(--warm) 20%, transparent); }
.band-high     { color: var(--high-text);     background: color-mix(in srgb, var(--high) 17%, transparent); }
.band-critical { color: var(--critical-text); background: color-mix(in srgb, var(--critical) 15%, transparent); }
.band-stale    { color: var(--ink-muted);     background: color-mix(in srgb, var(--ink-muted) 14%, transparent); }

/*
 * A held reading, which is neither a verdict nor a silence.
 *
 * Deliberately outside the reserved status palette: --ok, --warm, --high and
 * --critical mean "this is how hot the panel is", and a frozen channel is not
 * a statement about the panel at all. It is also not the muted grey of stale,
 * because the difference matters operationally — stale means nobody has heard
 * from the site, held means the site is talking and the sensor inside it is
 * not. Given the brand hue so it reads as an instrument fault rather than an
 * alarm, and the `=` glyph carries the same meaning in monochrome.
 */
.band-held {
  color: var(--brand);
  background: color-mix(in srgb, var(--brand) 13%, transparent);
}

/* A reading nobody has refreshed is shown, but it stops competing for the eye. */
.site.is-stale .site-read b { color: var(--ink-muted); }

/*
 * The same treatment for a held one, plus the dashed outline this app already
 * uses on a device box to mean "not solid data". Fading alone was measured at
 * 2.2:1 on the light look when it was tried for stale devices — a label nobody
 * can read is not an improvement on a label that lies — so the number keeps
 * full contrast and the border carries the meaning.
 */
/* Three sides, not four: the left edge is the kind stripe, and this card's own
   rule is that the stripe is identity — what sort of site this is, which a
   frozen sensor does not change. Dashing it said the wrong thing about the
   wrong fact. The console look draws no card border at all, so there the pill,
   the muted figure and the count carry it; measured, not assumed. */
.site.is-held {
  border-top-style: dashed;
  border-right-style: dashed;
  border-bottom-style: dashed;
}
.site.is-held .site-read b { color: var(--ink-muted); }
/* Two classes on purpose: .site-meta is declared further down this file and
   would otherwise win the cascade and mute this line back to grey. */
.site-meta.held-note { color: var(--brand); font-weight: 600; }

/*
 * The same rule inside a diagram.
 *
 * A device box keeps its last reading — an engineer wants to know what the
 * panel said before it went quiet — but it stops claiming to be current:
 * viz.js has already taken its band away, so no alarm colour and no verdict
 * word, and the age is printed where the verdict used to be.
 *
 * Marked with a dashed outline rather than by fading the box. Fading was the
 * first attempt and it measured 2.2:1 on the light look — a label nobody can
 * read is not an improvement on a label that lies. A dashed border carries the
 * same "not solid data" meaning at full contrast, and survives greyscale.
 */
.dev.is-stale { border-style: dashed; background: var(--surface-sunk); }
.dev.is-stale .dev-read strong { color: var(--ink-muted); }
.dev.is-stale .dev-bar i { background: var(--ink-2); opacity: .45; }
.sld-dev.is-stale rect { stroke-dasharray: 5 4; }
.gauge-name .stale-tag { color: var(--ink-muted); font-weight: 600; }

/*
 * And the third state, on the same three drawings: a device that is reporting
 * while the channel inside it has stopped measuring.
 *
 * Told apart from stale on purpose, in the brand hue the card's own "Not
 * updating" pill already uses — the two faults need different people, a link
 * or a logger for one and the sensor itself for the other, and one grey for
 * both would send the wrong engineer. Not in --ok/--warm/--high/--critical,
 * because those four mean "this is how hot the panel is" and a frozen channel
 * is not a statement about the panel at all.
 *
 * The reading keeps FULL contrast — unlike the stale rules above, which mute
 * it. A held number is current and correct as the last thing the sensor
 * produced, and it is the number an engineer has to read before deciding
 * anything; what is false about it is only the claim that it is being
 * measured, and the dashed edge plus the word carry that.
 */
/*
 * 52%, not 48%. `--border` is TRANSLUCENT (rgba(15,17,21,.11) on light), so the
 * mix keeps its alpha and composites against the tile's white --surface: 48%
 * lands on rgb(171,138,223) = 2.82:1, under the 3:1 that WCAG 1.4.11 asks of a
 * boundary that carries meaning. Measured in the browser, not calculated —
 * getComputedStyle returns color(srgb .388 .147 .769 / .537) and the curve
 * crosses at 52% (3.03:1 light, 3.27:1 dark). 55% was left to the SLD stroke so
 * the two drawings stay visually distinct.
 */
.dev.is-held {
  border-style: dashed;
  border-color: color-mix(in srgb, var(--brand) 52%, var(--border));
}
/*
 * ...and it still answers the pointer. `.dev:hover` (viz.css) and `.dev.is-held`
 * are both two classes' worth of specificity, and ui.css is linked second, so
 * without this line the held rule silently won on hover and 402 tiles stopped
 * lighting up when you pointed at them. `.dev.is-stale` never had the problem
 * because it sets only border-STYLE.
 */
.dev.is-held:hover { border-color: var(--brand); }
.dev.is-held .dev-state {
  color: var(--brand);
  background: color-mix(in srgb, var(--brand) 13%, transparent);
}
.dev.is-held .dev-bar i { background: var(--ink-2); opacity: .45; }
.sld-dev.is-held rect {
  stroke-dasharray: 5 4;
  stroke: color-mix(in srgb, var(--brand) 55%, var(--border));
}
.gauge-name .held-tag { color: var(--brand); font-weight: 600; }

/*
 * The state pill may drop to its own line, and the device keeps its name.
 *
 * `.dev-state` is flex:none and `.dev-name` is flex:1;min-width:0, so when the
 * pill does not fit it is the NAME that gives way: it shrinks to nothing and the
 * tile loses its label altogether while the pill still hangs past the row. The
 * name is the only thing on that tile that says WHICH panel this is, so losing
 * it is worse than losing any reading on it.
 *
 * Measured in the baked preview at 1280 px across all 75 sites and every SCADA
 * view, before and after:
 *   bright look, deployed code   :   0 tiles blanked
 *   bright look, + "NOT UPDATING":  401 blanked on 40 sites (93 px pill, 145 px row)
 *   console look, deployed code  :  951 blanked on 64 sites — ALREADY, with
 *                                   nothing more exotic than NORMAL and NO DATA,
 *                                   because its grid track is 128 px not 150 px
 *   either look, with this rule  :   0, and 0 px of pill left hanging
 * So this is not only the new word's problem; the console look has been eating
 * device names on two thirds of the estate and nobody had measured it.
 *
 * One rule rather than a `.is-held` special case, because the fault is the
 * geometry and not the word, and `flex-wrap` only ever engages on a line that
 * genuinely does not fit — every tile that fits today is laid out unchanged.
 * Verified in all four looks at 1280, 375 and 320 px.
 */
.dev-top { flex-wrap: wrap; }

/*
 * ...and the name may not be squeezed to nothing to make the pill fit.
 *
 * flex-wrap alone is not enough, and the reason is worth writing down: it only
 * engages when the line genuinely OVERFLOWS. When the pill and the symbol just
 * barely fit, nothing wraps and `.dev-name` (flex:1, flex-basis 0) is crushed
 * instead — it is the only item on the row that can give. Measured at 1280 px
 * with "NOT UPDATING" on the row, name width per look:
 *   bright  174 px tile ->  98 px   (wrapped)
 *   console 133 px tile ->  57 px   (wrapped)
 *   bold    227 px tile ->  48 px   (fits, name squeezed but readable)
 *   spatial 180 px tile ->   1 px   (fits by 3 px, name annihilated)
 * Spatial sits exactly on the knife edge, and 228 tiles across the estate lost
 * their name there — the one thing on a tile that says WHICH panel it is.
 *
 * 48px, measured rather than picked: it is the floor bold already settles at, so
 * it forces the wrap on spatial and leaves bright, bold and console unchanged in
 * both name width and tile height. 56px would also move bold, 64px would also
 * move console; neither is needed.
 */
.dev-name { min-width: 48px; }

.dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.dot-ok { background: var(--ok); }
.dot-warm { background: var(--warm); }
.dot-high { background: var(--high); }
.dot-critical { background: var(--critical); }
.dot-off { background: var(--ink-muted); }

/* ------------------------------------------------------------- site cards */

.site {
  display: flex; flex-direction: column; gap: 10px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: var(--pad);
  text-decoration: none; color: inherit; cursor: pointer;
  transition: border-color var(--speed), transform var(--speed);
}
.site:hover { border-color: var(--brand); }
.site:active { transform: scale(.995); }
.site-top { display: flex; align-items: flex-start; gap: 10px; }
/*
 * Site names are machine-made identifiers with nothing to break at —
 * CityServiceBlockPanelArea, Site_LBElectrical_Room1 — so the
 * min-content width of one is the whole string. In the console look .site is a
 * flex ROW (below), and that unbreakable width stopped .site-top shrinking and
 * pushed the reading and its status pill past the card edge, where the card's
 * own overflow:hidden deleted them: 17 of 124 cards at 375px were showing a
 * name and no temperature. Breaking anywhere costs a taller card on the long
 * names and keeps every reading on screen.
 */
.site-name { flex: 1; min-width: 0; font-weight: 640; font-size: var(--t-body);
  overflow-wrap: anywhere; }
.site-meta { font-size: var(--t-micro); color: var(--ink-muted); }
.site-read { display: flex; align-items: baseline; gap: 6px; }
.site-read b { font-family: var(--display); font-size: var(--t-title); font-weight: 720; }

/* Console shows sites as rows of instrument, not as cards. */
:root[data-look="console"] .grid.sites { grid-template-columns: 1fr; gap: 1px; background: var(--line); }
:root[data-look="console"] .site { border: 0; border-radius: 0; flex-direction: row; align-items: center; }

/* ------------------------------------------------------------- utilities */

.row { display: flex; align-items: center; gap: 10px; }
.spread { justify-content: space-between; }
.muted { color: var(--ink-muted); }
.small { font-size: var(--t-small); }
.section {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; margin: 26px 0 12px;
}
.section h2 { margin: 0; font-size: var(--t-head); font-weight: 680; }
.section .meta { font-size: var(--t-small); color: var(--ink-muted); }

.empty {
  padding: 34px 20px; text-align: center;
  color: var(--ink-muted); font-size: var(--t-small);
}

/* An all-clear that does not cover the whole estate says so — and says it in
   the colour the site cards already use for a held reading, not in the muted
   grey of an ordinary empty state. Muted grey is the voice of "nothing here",
   and this sentence is the opposite: it is the part a reader must not skip. */
.empty .empty-held {
  display: block; margin: 10px auto 0; max-width: 46ch;
  color: var(--brand); font-weight: 600;
}

.skeleton {
  background: linear-gradient(90deg, var(--sunk), var(--surface), var(--sunk));
  background-size: 200% 100%;
  border-radius: var(--radius);
  animation: sheen 1.3s linear infinite;
  min-height: 90px;
}
@keyframes sheen { to { background-position: -200% 0; } }

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* ----------------------------------------------------------------- login */

.login {
  min-height: 100dvh; display: grid; place-items: center; padding: 24px;
  background:
    radial-gradient(820px 460px at 8% -12%, var(--brand-soft), transparent 62%),
    var(--page);
}
.login-card {
  width: 100%; max-width: 372px; padding: 30px 26px 24px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-lg); box-shadow: var(--lift);
}
.field { display: block; margin-bottom: 14px; }
.field span {
  display: block; font-size: var(--t-small); font-weight: 620;
  color: var(--ink-2); margin-bottom: 5px;
}
.field input {
  width: 100%; min-height: var(--tap); padding: 10px 12px;
  font: inherit; color: var(--ink); background: var(--sunk);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.btn {
  width: 100%; min-height: var(--tap); border: 0;
  border-radius: var(--radius-sm);
  background: var(--brand); color: var(--brand-ink);
  font-weight: 650; cursor: pointer;
}
.btn[disabled] { opacity: .6; cursor: default; }
.btn-link { color: var(--brand); font-weight: 620; }
.form-error {
  margin: 0 0 14px; padding: 9px 11px; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--critical) 13%, transparent);
  color: var(--critical-text); font-size: var(--t-small);
}

/* ==========================================================================
   Compatibility with the drawing library

   viz.js and its stylesheet came from the previous app and are good — the
   single-line diagrams, the device grids, the machine symbols, the heatmap.
   They are reused rather than rewritten, so app.css loads first and this file
   loads after it and wins on every shared name.

   What is left is a handful of tokens that were renamed in the new system.
   Aliasing them here means the drawing code keeps working, unedited, while
   taking its colours from the new palette — so the diagrams change appearance
   with the look like everything else does.
   ========================================================================== */

:root {
  --border:       var(--line);
  --surface-sunk: var(--sunk);
  --shadow-lift:  var(--lift);
  --grid:         var(--line);
  --axis:         var(--line-firm);
  --series:       var(--brand);
  --series-soft:  var(--brand-soft);
  --good:         var(--ok);
  --warning:      var(--warm);
  --serious:      var(--high);

  /* A panel-mounted meter reads as a recessed display in both themes; it does
     not invert, because a real one does not. */
  --meter-bg:  #10141c;
  --meter-ink: #7ee6b0;
}

/* The drawing library sizes itself to its container in every look. */
.viz { width: 100%; }
/* superseded below — this matched every nested symbol and dial */
.viz-scroll { overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; }

/* ==========================================================================
   Colour

   The first cut was almost entirely grey: a violet brand mark, four status
   colours that only appeared when something was wrong, and everything else in
   neutrals. On a dark screen that reads as unfinished rather than restrained —
   and it wastes the one thing a monitoring product can use colour for, which is
   telling you at a glance what kind of thing you are looking at.

   So colour now carries two jobs, and only two. Status keeps the reserved ramp
   and never shares it. Everything else takes a tint from what a site IS —
   solar, industrial, sensors — which is a categorical fact that does not change
   while you watch, so it can be colour without being alarming.
   ========================================================================== */

:root {
  /* Site kinds. Categorical, deliberately clear of the status ramp: nothing
     here is green, amber, orange or crimson.

     These are also chip TEXT, printed on a 14% tint of themselves, so they have
     to clear 4.5:1 against that tint and not merely against white.

     Corrected 2026-08-14 after measuring the rendered pill rather than assuming
     the card underneath: the 700 step was sized against white and still came in
     at 4.14 (amber) and 4.41 (cyan) once the tint was accounted for. The 800
     step clears it with room — 5.08 and 5.85. Violet (4.62) and slate (6.13)
     already passed and are unchanged. */
  --kind-solar:  #9a4a0a;
  --kind-indus:  #155e75;
  --kind-sensor: #7c3aed;
  /* Slate 500 measured 4.41 on the "Not set up" chip in the bold look, whose
     chips run larger but no lighter. One step down clears it everywhere. */
  --kind-empty:  #475569;
}

/*
 * The dark tints, applied the same way the surfaces above are.
 *
 * These were once a bare `:root:not([data-theme="light"])`, which matches when
 * the attribute is merely ABSENT — so a light desktop got the dark tints, which
 * are tuned to glow on near-black and turn to pale wash on white. Only `bright`
 * and `console` set data-theme; `bold` and `spatial` leave it off, so those two
 * looks drew every site chip at 1.6:1 in daylight. The media query is what makes
 * "no attribute" mean "ask the OS" instead of "assume dark".
 */
:root[data-theme="dark"] {
  --kind-solar:  #fbbf24;
  --kind-indus:  #22d3ee;
  --kind-sensor: #a78bfa;
  --kind-empty:  #94a3b8;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --kind-solar:  #fbbf24;
    --kind-indus:  #22d3ee;
    --kind-sensor: #a78bfa;
    --kind-empty:  #94a3b8;
  }
}

/* The page itself gets some depth rather than being flat black. */
body {
  background:
    radial-gradient(1200px 700px at 88% -8%, var(--brand-soft), transparent 60%),
    radial-gradient(900px 600px at -6% 106%, color-mix(in srgb, var(--kind-indus) 9%, transparent), transparent 62%),
    var(--page);
  background-attachment: fixed;
}

/* ------------------------------------------------------------ site cards */

/*
 * A stripe down the edge in the site's own colour, and a status wash only when
 * there is something to say. The stripe is identity; the wash is state. Keeping
 * them on different channels is what stops a colourful screen from looking like
 * an alarming one.
 */
.site {
  position: relative; overflow: hidden;
  border-left: 3px solid var(--kind, var(--kind-empty));
}
.site::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(120deg,
    color-mix(in srgb, var(--kind, var(--kind-empty)) 7%, transparent), transparent 55%);
}
.site > * { position: relative; z-index: 1; }
.site:hover { border-color: var(--kind, var(--brand)); }

.site.kind-solar      { --kind: var(--kind-solar); }
.site.kind-industrial { --kind: var(--kind-indus); }
.site.kind-sensor     { --kind: var(--kind-sensor); }
.site.kind-empty      { --kind: var(--kind-empty); }

/* A site with something wrong takes the status colour instead — state wins
   over identity, because state is the reason anybody opened the page. */
.site.state-warm     { --kind: var(--warm); }
.site.state-high     { --kind: var(--high); }
.site.state-critical { --kind: var(--critical); }

.kind-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 9px; border-radius: 999px;
  font-size: var(--t-micro); font-weight: 640;
  color: var(--kind, var(--ink-muted));
  background: color-mix(in srgb, var(--kind, var(--ink-muted)) 14%, transparent);
}

/* ---------------------------------------------------------------- tiles */

/* Stat cards carry a hairline of their own tone across the top, so a row of
   them is scannable before a single figure has been read. */
.stat { border-top: 3px solid var(--brand); }
.stat.tone-ok   { border-top-color: var(--ok); }
.stat.tone-crit { border-top-color: var(--critical); }
.stat.tone-warm { border-top-color: var(--warm); }

/* ----------------------------------------------------------- per look */

/* Console earns its colour on the instruments, not the furniture: rails and
   cards stay near-black so the readings are the only bright thing. */
:root[data-look="console"] body { background: var(--page); }
:root[data-look="console"] .site::after { display: none; }
:root[data-look="console"] .site { border-left-width: 4px; }

/* Bold puts the site colour behind the whole card. */
:root[data-look="bold"] .site {
  border-left-width: 0;
  background: color-mix(in srgb, var(--kind, var(--brand)) 14%, var(--surface));
  border-color: color-mix(in srgb, var(--kind, var(--brand)) 40%, transparent);
}
:root[data-look="bold"] .site::after { display: none; }

/* Spatial groups by place, so the tint is the grouping cue and wants to be
   quiet inside a group and clear between them. */
:root[data-look="spatial"] .site { border-left-width: 5px; border-radius: var(--radius); }

/* -------------------------------------------------------------- the rail */

/* The brand mark, and the current section, are the only coloured things in the
   navigation — a rail that is itself colourful competes with the data. */
.rail { background: color-mix(in srgb, var(--surface) 88%, var(--brand-soft)); }
.brand-dot {
  background: linear-gradient(135deg, var(--brand), var(--kind-indus));
  box-shadow: 0 0 0 3px var(--brand-soft);
}

/* ==========================================================================
   Diagram sizing

   The rule below used to be `.viz svg`, which was wrong in a way that only
   showed up once the diagrams were on screen: it matched EVERY svg inside the
   container, including the 40px machine symbols on the equipment cards and the
   dial faces on the gauges. CSS beats a presentation attribute, so `width=40`
   lost to `width:100%` and a single fan symbol filled the viewport.

   Only the top-level diagram — the single-line, which really is one drawing
   that should fill its card — is stretched. Everything nested keeps the size it
   was drawn at.
   ========================================================================== */

.viz > svg { display: block; width: 100%; height: auto; }

/* The device and gauge grids come from the drawing library, but how many
   columns to give them is a layout decision and belongs with the looks. */
.viz .dev-grid {
  display: grid;
  gap: var(--gap);
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.viz .dev-grid.wide { grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); }

/* A dial does not need to be large to be read; it needs to be one of many you
   can compare at a glance. Twenty-seven of them on one screen is the point. */
.viz .gauge-card { padding: 10px 6px; }
.viz .gauge { max-width: 132px; }

:root[data-look="console"] .viz .dev-grid {
  grid-template-columns: repeat(auto-fill, minmax(128px, 1fr));
  gap: 6px;
}
:root[data-look="bold"] .viz .dev-grid,
:root[data-density="wall"] .viz .dev-grid {
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
:root[data-density="wall"] .viz .gauge { max-width: 180px; }

/*
 * A grid item can always be narrower than its contents.
 *
 * Grid items default to min-width:auto, so one oversized child does not
 * overflow its track — it widens it, and with auto-fill that collapses the
 * whole grid to a single column. That is what turned twenty-seven gauges into
 * twenty-seven full-width rows: the real fault was one stretched symbol, but
 * this is why the damage spread to the layout instead of staying local.
 */
.viz .dev-grid > * { min-width: 0; }

/*
 * The rail must not spill into the page.
 *
 * At the narrowest rail width the wordmark is wider than the rail, and with
 * nothing clipping it the brand was drawn straight across the page heading —
 * "energylog" and "Reports" on top of each other. A rail is a column, not a
 * suggestion.
 */
.rail { overflow: hidden; }
.brand { white-space: nowrap; min-width: 0; }

/* The heading has to survive a long site name beside the look switch. */
.topbar h1 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topbar .looks { flex: none; }

/* Below the rail breakpoint the look switch wraps under the title rather than
   squeezing it to nothing. */
@media (max-width: 620px) {
  .topbar { row-gap: 8px; }
  .topbar h1 { flex: 1 0 100%; }
}

/*
 * The diagram picker and the device count share one row on a site's SCADA page.
 * On a 375px touch phone the four 44px buttons alone measured 337.5px in a
 * 331px row, so the count was pushed 35px off the screen and the whole page
 * scrolled sideways — 59 of 79 SCADA pages on 2026-09-27, every site with a
 * Rise view. The row may wrap (the count drops under the picker), and on a
 * phone the buttons give up side padding, never height: the height is what a
 * thumb needs. At 320px 7px still fits all four (274px in 276px); the pill
 * scrolls inside itself, never the page, if a font makes it wider still.
 */
.scada-bar { flex-wrap: wrap; row-gap: 8px; }
@media (max-width: 420px) {
  .scada-bar .looks button { padding-inline: 10px; }
}
@media (max-width: 359px) {
  .scada-bar .looks { max-width: 100%; overflow-x: auto; }
  .scada-bar .looks button { padding-inline: 7px; }
}

/* ==========================================================================
   Hiding the rail, and Reports
   ========================================================================== */

/*
 * The handle that hides the menu, and brings it back.
 *
 * It sits outside the rail on purpose. Putting it inside would repeat the wall
 * mode mistake — hiding the menu would hide the control that restores it, and
 * the only way back would be clearing storage.
 */
.rail-toggle {
  position: fixed; top: 14px; left: 12px; z-index: 60;
  width: 26px; height: 34px;
  display: none; place-items: center;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink-muted);
  cursor: pointer; font-size: 1rem; line-height: 1;
  transition: left var(--speed), color var(--speed);
}
.rail-toggle:hover { color: var(--ink); }

@media (min-width: 900px) {
  .rail-toggle { display: grid; left: calc(var(--rail) - 13px); }
  :root[data-rail="off"] .rail { display: none; }
  :root[data-rail="off"] .rail-toggle { left: 8px; }
}

/* Wall mode narrows the rail, so the handle follows it. */
@media (min-width: 900px) {
  :root[data-density="wall"] .rail-toggle { left: 71px; }
  :root[data-density="wall"][data-rail="off"] .rail-toggle { left: 8px; }
}

/* --------------------------------------------------------------- reports */

/*
 * Nothing on this page has the class "rep".
 *
 * The reports screen is built from .rep-bar, .rep-card, .rep-h and friends, and
 * three rules here were written for a wrapper that is never emitted: `.rep`,
 * `.rep .field` and `.rep select`. A class selector matches whole names, so
 * `.rep` does not reach `.rep-bar` — and the miss is invisible, because dead
 * CSS looks exactly like live CSS.
 *
 * What it cost was the site picker. Its rule, `.rep select`, was a
 * character-for-character copy of the date-input rule below and never fired
 * once, so the control rendered as a raw browser default — 19px tall, no
 * padding, white — sitting beside two date inputs correctly styled to the 44px
 * tap target. It was both the odd one out and too small to press on a phone.
 *
 * The select is now styled by the same rule as the inputs it sits next to,
 * rather than by a copy of it, so the two cannot drift apart again.
 */
.rep-h { margin: 0; font-size: var(--t-head); font-weight: 680; }
.rep-p { margin: 0; font-size: var(--t-small); color: var(--ink-muted); line-height: 1.55; }
.rep-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: auto; padding-top: 6px; }
.rep-actions .btn { width: auto; padding: 0 16px; }

.btn.ghost {
  background: transparent; color: var(--ink-2);
  border: 1px solid var(--line);
}
.btn.ghost:hover { background: var(--sunk); color: var(--ink); }

/* ------------------------------------------------------- report templates */

/* Site and dates chosen once, at the top. A date picker on each of fifteen
   cards would be fifteen chances to export the wrong month. */
.rep-bar {
  display: grid; gap: 12px; align-items: end;
  grid-template-columns: 2fr 1fr 1fr;
  margin-bottom: 6px;
}
.rep-bar .field { margin: 0; }
.rep-bar :is(select, input[type="date"]) {
  width: 100%; min-height: var(--tap); padding: 9px 12px;
  font: inherit; color: var(--ink); background: var(--sunk);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.rep-note { grid-column: 1 / -1; margin: 0; font-size: var(--t-micro); color: var(--ink-muted); }
@media (max-width: 620px) { .rep-bar { grid-template-columns: 1fr; } }

.rep-card { display: flex; flex-direction: column; gap: 8px; }
.rep-top { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.rep-span {
  flex: none; padding: 2px 9px; border-radius: 999px;
  background: var(--brand-soft); color: var(--brand);
  font-size: var(--t-micro); font-weight: 640; white-space: nowrap;
}

/*
 * The report table has eight columns and every cell is nowrap, so it is wider
 * than a phone whatever the sensors are called — 761px inside a 375px screen
 * before this rule, and the whole page scrolled sideways with it. ui.js has
 * always wrapped the table in .table-scroll for exactly this, but the class
 * was never given a rule, so it did nothing. Now the table scrolls inside its
 * own box and the page does not move.
 */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

.rep-table { width: 100%; border-collapse: collapse; font-size: var(--t-small); }
.rep-table th, .rep-table td {
  padding: 8px 11px; text-align: left; white-space: nowrap;
  border-bottom: 1px solid var(--line);
}
.rep-table thead th {
  color: var(--ink-muted); font-weight: 640; font-size: var(--t-micro);
  text-transform: uppercase; letter-spacing: .05em;
}
.rep-table .n { text-align: right; font-variant-numeric: tabular-nums; }
.rep-table tbody tr:last-child td { border-bottom: 0; }

/* ==========================================================================
   Corrections found by looking at the rendered page

   Both of these are ordering mistakes of the same kind: a rule written earlier
   in the file, overridden by one written later at the same specificity. CSS
   resolves that by source order, and a media query does not raise specificity —
   so `@media (min-width: 900px) { .tabbar { display: none } }` written ABOVE
   `.tabbar { display: flex }` loses to it at every width. The phone bar was
   showing on a 1318px desktop underneath the rail, giving two navigations.
   ========================================================================== */

@media (min-width: 900px) {
  .tabbar { display: none; }
  .main { padding-bottom: 40px; }
}

/* ------------------------------------------------ parameter report (root) */
.pr-link { display: flex; flex-direction: column; gap: 2px; text-decoration: none; color: var(--ink);
  border-left: 3px solid var(--brand, var(--series)); margin-bottom: 12px; }
.pr-link span { color: var(--ink-muted); font-size: .86rem; }
.pr-picker { display: grid; gap: 12px; margin-top: 12px; }
.pr-top { display: flex; gap: 8px; align-items: stretch; }
.pr-top input { flex: 1 1 auto; width: auto; min-width: 0; padding: 9px 12px; border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--surface); color: var(--ink); font: inherit; }
.pr-top .btn { flex: 0 0 auto; width: auto; }
.pr-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.pr-presets { grid-column: 1 / -1; }
.pr-chip { border: 1px solid var(--border); background: var(--surface-sunk, var(--sunk)); color: var(--ink); border-radius: 999px;
  padding: 5px 11px; font: inherit; font-size: .8rem; cursor: pointer; }
.pr-chip span { color: var(--ink-muted); }
.pr-chip:hover { border-color: var(--series); }
.pr-meters { display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
.pr-meter { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 8px 10px; background: var(--surface); min-width: 0; }
.pr-mh { display: flex; justify-content: space-between; align-items: center; gap: 6px; margin-bottom: 4px; }
.pr-mh b { font-size: .88rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pr-all { border: 0; background: none; color: var(--series); font: inherit; font-size: .78rem; cursor: pointer; padding: 2px 4px; }
.pr-p { display: flex; align-items: center; gap: 7px; padding: 3px 0; font-size: .85rem; cursor: pointer; min-width: 0; }
.pr-p span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.pr-p em { font-style: normal; font-size: .74rem; color: var(--ink-muted); }
.pr-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 12px; }
.pr-count { flex: 1; min-width: 140px; color: var(--ink-muted); font-size: .88rem; }
.pr-actions .btn:disabled { opacity: .45; cursor: not-allowed; }
.pr-table { overflow-x: auto; }
.pr-table table { width: 100%; border-collapse: collapse; font-size: .86rem; }
.pr-table th { text-align: left; font-weight: 600; color: var(--ink-muted); font-size: .76rem; padding: 6px 8px; border-bottom: 1px solid var(--border); }
.pr-table td { padding: 7px 8px; border-bottom: 1px solid var(--border); white-space: nowrap; font-variant-numeric: tabular-nums; }
.pr-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 7px; vertical-align: middle; }
.pr-chart { padding-bottom: 10px; margin-top: 12px; }
.pr-ct { font-weight: 600; font-size: .9rem; margin-bottom: 6px; }
.pr-svg { width: 100%; height: auto; display: block; }
.pr-grid { stroke: var(--grid, var(--border)); stroke-width: 1; vector-effect: non-scaling-stroke; }
.pr-ax { fill: var(--ink-muted); font-size: 11px; }
.pr-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: .8rem; color: var(--ink-muted); margin-top: 6px; }
