/* AGRORTK caster admin panel — brand: AGRONAV red */

:root {
  /*
   * AGRONAV's red, #E70010, taken from the vector artwork rather than guessed
   * from a screenshot. It is the one value the whole palette hangs off; see
   * docs/BRANDI.md.
   *
   * Every colour below carries its measured contrast ratio, because "looks
   * fine on my screen" is how a panel ends up unreadable in a tractor cab in
   * low sun. Ratios are against --surface (white) in light mode.
   *
   * The important thing about this particular red: it is 4.77:1 on white, so
   * white text on it is fine and the logo is fine — but on the page
   * background it drops to 4.47:1, just under AA. So there are two brand
   * tokens and they are not interchangeable:
   *
   *   --brand      the logo colour. Fills, the emblem, large headings.
   *   --brand-ink  the *text* colour. Links, small labels, active tab.
   *
   * Using --brand for body text is the one mistake this split exists to
   * prevent, and it is the one that would actually cost someone a reading.
   */
  --brand:       #E70010;  /* 4.77:1 on white — fills and the logo */
  --brand-ink:   #C8000E;  /* 6.07:1 — anything set as text */
  --brand-600:   #C8000E;  /* hover on a brand fill */
  --brand-700:   #A50009;  /* pressed */
  --brand-050:   #FDE9EA;
  --brand-100:   #FBDCDE;

  /* Warm neutrals, pulled a touch towards the brand's hue so the greys do not
     read as cold against a pure red. */
  --bg:        #FAF7F7;
  --surface:   #FFFFFF;
  --surface-2: #F3EFEF;
  --ink:       #1F1B1B;  /* 17.1:1 */
  --ink-2:     #514A49;  /* 8.7:1 */
  /* Also checked on the tinted surfaces it actually sits on, not only on
     white: at #736B6A the reminder sub-lines were 4.46 on --brand-050. */
  --muted:     #6B6362;  /* 5.9:1 on white, 5.0:1 on the tints */
  --line:      #E5E0DF;

  /* Status colours. The error red is deliberately shifted to hue 353 and
     darkened so it never reads as the brand colour — with the brand this red,
     "something is wrong" and "this is AGRONAV" must not look alike. */
  /*
   * A rover is not a station status, so it does not take a status colour.
   * It used to be drawn in the brand red and sat on the same map as stations
   * drawn in the error red — two reds, one meaning "a tractor is here" and
   * the other "this station is down". Violet is the only hue on the map that
   * is neither a status nor a station type.
   */
  --rover:   #7C4DBE;  /* 5.7:1 */

  --ok:      #2F7A4C;  /* 5.2:1 */
  --ok-bg:   #E5F2EA;
  /* Measured on --warn-bg, not on white: this colour is only ever used on
     that tint, and against it #96681C was 4.28 — the ratio in the old comment
     was true and irrelevant. */
  --warn:    #8C6017;  /* 4.8:1 on --warn-bg, 5.5:1 on white */
  --warn-bg: #F8EFDC;
  --bad:     #A3182B;  /* 7.7:1, hue 350 */
  --bad-bg:  #F7E4E7;
  --info:    #3D6E8F;  /* 5.5:1 */
  --info-bg: #E7F0F6;

  /*
   * Radius, shadow and type as three short scales.
   *
   * Before this there were twelve different corner radii and twenty font
   * sizes between .62 and 1.15rem, all arrived at one rule at a time. Nothing
   * was individually wrong and the whole thing read as unconsidered, which is
   * what "not quite professional" usually turns out to mean when you go
   * looking for it.
   *
   * The tile radius of the app mark is --radius on purpose: the icon and the
   * panels it sits above share one corner.
   */
  --radius-xs: 6px;   /* skeletons, chips */
  --radius-sm: 10px;  /* buttons, inputs, badges, callouts */
  --radius: 14px;     /* panels, cards */
  --radius-lg: 18px;  /* modals, the login card */

  --shadow-sm: 0 1px 2px rgba(31, 27, 27, .07);
  --shadow: 0 1px 2px rgba(31, 27, 27, .05), 0 6px 18px rgba(31, 27, 27, .06);
  --shadow-lg: 0 16px 48px rgba(31, 27, 27, .20);

  /* Seven steps. Every font-size in this file is one of them. */
  --fs-2xs: .70rem;   /* uppercase micro labels */
  --fs-xs:  .78rem;   /* hints, sub-text */
  --fs-sm:  .86rem;   /* table cells, secondary text */
  --fs-md:  .95rem;   /* body */
  --fs-lg:  1.05rem;  /* panel headings */
  --fs-xl:  1.5rem;   /* page headings */
  --fs-kpi: 1.75rem;  /* the four numbers on the front page */

  --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

/*
 * The dark palette, defined once.
 *
 * The switch is an attribute on <html>, not a media query: theme.js resolves
 * "automatic" against the operating system and stamps data-theme before the
 * first paint, so the operator's own choice (Asetukset -> Yleiset) and the
 * system default go through the same single definition. A media query would
 * have needed this whole block twice, and two copies of a palette whose every
 * colour carries a measured contrast ratio is two copies that drift.
 */
:root[data-theme="dark"] {
    --bg:        #151111;
    --surface:   #201B1B;
    --surface-2: #2B2525;
    --ink:       #F4F0EF;  /* 15.0:1 */
    --ink-2:     #D4CDCC;  /* 10.9:1 */
    --muted:     #A69E9D;  /* 6.5:1 */
    --line:      #3B3535;

    /*
     * The brand keeps its true value here, and that took undoing an earlier
     * mistake: everything was lightened to #FF6250 so the brand could also
     * serve as *text*, and #FF6250 has unequal green and blue channels — an
     * orange cast, which is precisely why it read as the old terracotta
     * rather than as AGRONAV red.
     *
     * Separating the two jobs fixes it. As a **fill** the true #E70010 needs
     * only 3:1 against the surface for a UI component (it has 3.6) and white
     * text on it is 4.77:1, so a primary button is the real brand colour in
     * both themes and the dark-mode ink override on it is gone.
     *
     * As **text** no pure red can reach 4.5:1 on this surface — #FF0000 tops
     * out at 4.26. #FF3333 clears it at 4.68 with green and blue equal, so it
     * stays a true red hue instead of drifting to coral.
     */
    --brand:     #E70010;  /* fill; 3.6:1 vs surface, white on it 4.77:1 */
    --brand-ink: #FF3333;  /* text; 4.68:1, hue 0° — no orange cast */
    --brand-600: #FF1F2E;
    --brand-700: #FF3B48;
    --brand-050: #34141A;
    --brand-100: #45191F;

    --rover:     #B79BFF;  /* 7.4:1 */

    --ok:      #5FBF86;  /* 7.4:1 */
    --ok-bg:   #1B2F23;
    --warn:    #E0A94A;  /* 8.0:1 */
    --warn-bg: #33280F;
    --bad:     #F2777F;  /* 6.2:1 */
    --bad-bg:  #3A1C20;
    --info:    #7FB2D6;
    --info-bg: #17262F;

    --shadow-sm: 0 1px 2px rgba(0,0,0,.45);
    --shadow: 0 1px 2px rgba(0,0,0,.4), 0 6px 18px rgba(0,0,0,.32);
    --shadow-lg: 0 16px 48px rgba(0,0,0,.55);
}

* { box-sizing: border-box; }

/* Any class that sets `display` outranks the browser's [hidden] rule, and a
   still-displayed "hidden" element is either visible clutter or an invisible
   click trap. Settle it once, globally. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 { margin: 0; font-weight: 650; letter-spacing: -.01em; }
h2 { font-size: var(--fs-lg); }
a { color: var(--brand-ink); }

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

.login-screen {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 24px;
  background:
    radial-gradient(1200px 600px at 50% -10%, var(--brand-050), transparent 70%),
    var(--bg);
}

.login-card {
  width: 100%;
  max-width: 360px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  padding: 32px 28px;
  display: flex;
  flex-direction: column;
  text-align: center;
}
.login-logo { align-self: center; margin-bottom: 10px; }
.login-card h1 { font-size: var(--fs-xl); letter-spacing: .06em; }
.login-sub { margin: 2px 0 22px; color: var(--muted); font-size: var(--fs-sm); }
.login-card label {
  text-align: left;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--ink-2);
  margin-bottom: 5px;
}
.login-card input { margin-bottom: 14px; }
.login-error {
  margin: 14px 0 0;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  background: var(--bad-bg);
  color: var(--bad);
  font-size: var(--fs-sm);
}
.login-hint { margin: 12px 0 0; color: var(--muted); font-size: var(--fs-xs); }
.login-foot { color: var(--muted); font-size: var(--fs-xs); }

/* ------------------------------------------------------- the AGRONAV mark */

/*
 * "AGRORTK — powered by AGRONAV" as one component, used on the login card,
 * the public join form and the customer status page.
 *
 * The lockup SVG carries its own red for the emblem and `currentColor` for
 * the word, so the whole thing follows the surrounding text colour in dark
 * mode without a second file or a filter. Nothing here recolours the emblem:
 * a brand red that shifts with the theme stops being a brand red.
 */
.powered-by {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  color: var(--ink);
  font-size: var(--fs-2xs);
  letter-spacing: .1em;
  text-transform: uppercase;
}
.powered-by .label {
  color: var(--muted);
  letter-spacing: .1em;
  /* Sits optically on the wordmark's baseline rather than its box. */
  padding-top: 1px;
}
.powered-by img { height: 15px; width: auto; display: block; }

/* A divider that stops the credit reading as part of the form above it. */
.login-card .powered-by {
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
}

/* On the public pages the credit sits under the card, not inside it — those
   are the two surfaces a customer sees, and whose service this is belongs at
   the foot of the page rather than buried in a form. */
.page-credit { margin: 22px auto 40px; }

/*
 * The panel's own footer.
 *
 * The credit used to sit in the top bar next to the product name, which made
 * a bar that already carries five live numbers, a bell, a status dot and two
 * controls carry a sixth thing that never changes. A header should hold what
 * you look at; whose service this is belongs at the bottom, where you find it
 * when you want it and never trip over it when you do not.
 */
.app-foot {
  display: flex;
  justify-content: center;
  padding: 26px 20px 34px;
  margin-top: 8px;
  border-top: 1px solid var(--line);
}
.app-foot .powered-by { opacity: .75; }
.app-foot .powered-by:hover { opacity: 1; }

/* The confirmation code: six digits, spaced out so they are easy to read off
   a phone and easy to check against the email. */
.code-input {
  text-align: center;
  font-size: var(--fs-xl);
  letter-spacing: .45em;
  text-indent: .45em; /* balances the trailing letter-spacing */
  font-variant-numeric: tabular-nums;
  padding: 12px 10px;
}
.login-card .btn-quiet { margin-top: 8px; }

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

.topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 0 20px;
  height: 58px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
.brand-name { font-weight: 700; letter-spacing: .07em; }
.brand-sub {
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: .12em;
  color: var(--muted);
  padding: 2px 7px;
  border: 1px solid var(--line);
  border-radius: 999px;
}
.topbar-stats { display: flex; gap: 18px; flex: 1; overflow: hidden; }
.topbar-stat { display: flex; align-items: baseline; gap: 6px; font-size: var(--fs-xs); color: var(--muted); white-space: nowrap; }
.topbar-stat b { font-size: var(--fs-md); color: var(--ink); font-variant-numeric: tabular-nums; }
/*
 * `margin-left: auto` rather than relying on .topbar-stats to push it there.
 * The stats carry `flex: 1`, so on a wide screen they did the pushing — and
 * below 760 px they are hidden, which left the bell, the status dot and
 * "Kirjaudu ulos" stranded in the middle of the bar next to the logo.
 */
.topbar-right { display: flex; align-items: center; gap: 12px; margin-left: auto; }
.admin-name { font-size: var(--fs-sm); color: var(--ink-2); }
.live-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--ok);
  box-shadow: 0 0 0 3px var(--ok-bg);
}
.live-dot.is-stale { background: var(--bad); box-shadow: 0 0 0 3px var(--bad-bg); }

/* ------------------------------------------------------------------- tabs */

.tabs {
  display: flex;
  gap: 2px;
  padding: 0 14px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  overflow-x: auto;
  position: sticky;
  top: 58px;
  z-index: 29;
}
.tab {
  appearance: none;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 550;
  color: var(--muted);
  padding: 12px 14px;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  white-space: nowrap;
}
.tab:hover { color: var(--ink); }
.tab.is-active { color: var(--brand-ink); border-bottom-color: var(--brand); }

main { padding: 20px; max-width: 1500px; margin: 0 auto; }
.view { display: none; }
.view.is-active { display: block; }

/* ------------------------------------------------------------------ panel */

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  margin-bottom: 18px;
  overflow: hidden;
}
.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 13px 16px;
  border-bottom: 1px solid var(--line);
}
.panel-body { padding: 16px; }
.grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 18px; }

/* -------------------------------------------------------------------- KPI */

.kpi-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 14px;
  margin-bottom: 18px;
}
.kpi {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px 16px;
  box-shadow: var(--shadow);
}
.kpi-label { font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
.kpi-value { font-size: var(--fs-kpi); font-weight: 680; line-height: 1.2; font-variant-numeric: tabular-nums; }
.kpi-value.is-bad { color: var(--bad); }
.kpi-sub { font-size: var(--fs-xs); color: var(--muted); }

/* -------------------------------------------------------------------- map */

.map-panel .panel-head { border-bottom: 0; }
.map {
  position: relative;
  /*
   * Tall enough to hold the network without panning. 460px was chosen when
   * the map was one panel among many on a laptop; on a desktop it left the
   * northern and southern stations crowded against the edges, and the extra
   * height costs nothing there. The phone keeps the short one — the map is
   * one of several things on a small screen, not the whole screen.
   */
  height: 460px;
  background: var(--surface-2);
  overflow: hidden;
  cursor: grab;
  touch-action: none;
  user-select: none;
}
.map.is-dragging { cursor: grabbing; }
@media (min-width: 1000px) { .map { height: 620px; } }
@media (min-width: 1500px) { .map { height: 720px; } }
.map-tiles { position: absolute; inset: 0; }
.map-tiles img { position: absolute; width: 256px; height: 256px; }
:root[data-theme="dark"] .map-tiles img {
  filter: invert(1) hue-rotate(180deg) brightness(.85) contrast(.9);
}
.map-grid {
  position: absolute; inset: 0;
  background-image:
    linear-gradient(var(--line) 1px, transparent 1px),
    linear-gradient(90deg, var(--line) 1px, transparent 1px);
  background-size: 64px 64px;
  opacity: .5;
  pointer-events: none;
}
.map-circles { position: absolute; inset: 0; pointer-events: none; }
.map-circle {
  position: absolute;
  border-radius: 50%;
  transform: translate(-50%, -50%);
}
/* Faint on purpose. Twenty stations mean a lot of overlapping discs, and
   translucent fills compound: at the old 13 % four overlaps read as solid mud
   and the map stopped being a map. The fill is now light enough to stack, and
   each ring carries a thin edge so a station's extent is still legible where
   several overlap. */
/*
 * One hue, fading outwards — not a traffic light.
 *
 * The middle ring used to be brand red, which put a large pink disc over the
 * map for something that is good news. This project's own rule is that red
 * means broken (docs/BRANDI.md); coverage is the opposite, and since the
 * network view now draws only this middle ring, red made the whole map read
 * as an alarm. Green thinning with distance says the one thing the rings
 * actually mean: the further out, the weaker.
 */
.map-circle.is-good {
  background: color-mix(in srgb, var(--ok) 9%, transparent);
  border: 1px solid color-mix(in srgb, var(--ok) 26%, transparent);
}
.map-circle.is-usable {
  background: color-mix(in srgb, var(--ok) 4%, transparent);
  border: 1px solid color-mix(in srgb, var(--ok) 18%, transparent);
}
.map-circle.is-max { border: 1px dashed color-mix(in srgb, var(--muted) 38%, transparent); }

.map-markers { position: absolute; inset: 0; pointer-events: none; }
.marker {
  position: absolute;
  transform: translate(-50%, -50%);
  pointer-events: auto;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  /* Above the rovers below. A tractor working next to its base station lands
     on top of that station's name and blots out a letter or two of it, which
     reads as a rendering fault rather than as two things in the same place. */
  z-index: 2;
}
.marker.is-rover { z-index: 1; }
.marker-pin {
  position: relative; /* the online halo below is inset against this, not the label */
  width: 16px; height: 16px;
  border-radius: 50%;
  border: 2.5px solid var(--surface);
  box-shadow: 0 1px 5px rgba(0,0,0,.35);
  background: var(--muted);
}
.marker.is-online  .marker-pin { background: var(--ok); }
.marker.is-offline .marker-pin { background: var(--bad); }
.marker.is-disabled .marker-pin { background: var(--muted); }
.marker.is-rover .marker-pin { width: 10px; height: 10px; background: var(--rover); border-width: 2px; }
.marker.is-mobile .marker-pin { background: var(--info); border-radius: 3px; }
.marker-label {
  font-size: var(--fs-2xs);
  font-weight: 600;
  padding: 1px 6px;
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  border: 1px solid var(--line);
  white-space: nowrap;
}
.marker.is-online .marker-pin::after {
  content: "";
  position: absolute;
  inset: -6px;
  border-radius: 50%;
  border: 2px solid var(--ok);
  opacity: .35;
}
/* The card that opens over a pin: who owns the station and where it is, so it
   can be found without remembering its four-letter id. Kept narrow enough to
   fit a phone in portrait, and it never grabs the pointer at its edges —
   `pointer-events` stays on the card, not on the layer around it. */
.map-brief {
  position: absolute;
  z-index: 4;
  min-width: 168px;
  max-width: min(260px, calc(100% - 12px));
  padding: 9px 11px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  box-shadow: var(--shadow-lg, 0 6px 20px rgba(0,0,0,.35));
  font-size: var(--fs-xs);
  pointer-events: auto;
}
.map-brief-title { font-weight: 700; font-size: var(--fs-sm); line-height: 1.25; }
.map-brief-status { margin-top: 2px; color: var(--muted); }
.map-brief-status.is-online { color: var(--ok); }
.map-brief-status.is-offline { color: var(--bad); }
.map-brief-rows {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2px 8px;
  margin: 7px 0 0;
}
.map-brief-rows dt { color: var(--muted); }
.map-brief-rows dd { margin: 0; overflow-wrap: anywhere; }
.map-brief-action {
  margin-top: 9px;
  width: 100%;
  padding: 6px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  background: var(--surface-2);
  color: var(--ink);
  font-size: var(--fs-xs);
  font-weight: 600;
  cursor: pointer;
}
.map-brief-action:hover { border-color: var(--brand); }
.marker:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; border-radius: 4px; }

.map-controls { position: absolute; right: 12px; top: 12px; display: flex; flex-direction: column; gap: 6px; }
.map-btn {
  width: 32px; height: 32px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  font-size: var(--fs-lg);
  cursor: pointer;
  box-shadow: var(--shadow);
}
.map-btn:hover { background: var(--surface-2); }
.map-attribution {
  position: absolute; right: 6px; bottom: 4px;
  font-size: var(--fs-2xs);
  color: var(--muted);
  background: color-mix(in srgb, var(--surface) 80%, transparent);
  padding: 1px 6px;
  border-radius: 4px;
}
.map-legend { display: flex; gap: 14px; font-size: var(--fs-xs); color: var(--muted); flex-wrap: wrap; }
.dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 4px; }
.dot-ok { background: var(--ok); }
.dot-bad { background: var(--bad); }
.dot-off { background: var(--muted); }
.dot-rover { background: var(--rover); }
.dot-mobile { background: var(--info); border-radius: 2px; }
.ring-key {
  display: inline-block;
  width: 11px; height: 11px;
  margin-right: 4px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--ok) 22%, transparent);
  border: 1px dashed color-mix(in srgb, var(--muted) 45%, transparent);
}

/* The coverage switch sits in the legend, because it is the legend entry it
   controls — a separate button elsewhere would be one more thing to find. */
.map-toggle {
  gap: 5px;
  font-size: var(--fs-xs);
  font-weight: 400;
  color: var(--muted);
  margin: 0;
}
.map-toggle input { width: auto; margin: 0; }

/* ------------------------------------------------------------------ table */

.table-wrap { overflow-x: auto; }
.table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.table th {
  text-align: left;
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--muted);
  font-weight: 600;
  padding: 9px 16px;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
  background: var(--surface);
}
.table td { padding: 10px 16px; border-bottom: 1px solid var(--line); vertical-align: middle; }
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr.is-clickable { cursor: pointer; }
.table tbody tr.is-clickable:hover { background: var(--surface-2); }
.table .num { text-align: right; font-variant-numeric: tabular-nums; }
.empty { padding: 28px 16px; text-align: center; color: var(--muted); font-size: var(--fs-sm); }

.mono { font-family: var(--mono); font-size: .85em; }
.muted { color: var(--muted); }
.nowrap { white-space: nowrap; }

/* ----------------------------------------------------------------- badges */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: var(--fs-2xs);
  font-weight: 600;
  padding: 2px 9px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--ink-2);
  white-space: nowrap;
}
.badge-ok   { background: var(--ok-bg);   color: var(--ok); }
.badge-bad  { background: var(--bad-bg);  color: var(--bad); }
.badge-warn { background: var(--warn-bg); color: var(--warn); }
/*
 * `badge-info` exists because the brand became a signal red.
 *
 * With terracotta, a brand-tinted badge read as "noteworthy". With #E70010 it
 * reads as "wrong" — and the stations table had "PPP saatavilla" (the good
 * state, on every healthy station) in a brighter red than "ei yhteyttä" right
 * beside it. A capability is not a status; this is the badge for one.
 */
.badge-info { background: var(--info-bg); color: var(--info); }
.badge-brand{ background: var(--brand-050); color: var(--brand-ink); }

/* ---------------------------------------------------------------- buttons */

.btn {
  appearance: none;
  display: inline-block;      /* so <a class="btn"> matches <button class="btn"> */
  text-decoration: none;
  text-align: center;
  font: inherit;
  font-weight: 600;
  font-size: var(--fs-sm);
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  padding: 8px 14px;
  cursor: pointer;
  transition: background .12s ease, border-color .12s ease, transform .05s ease;
}
.btn:hover { background: var(--surface-2); }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn-primary { background: var(--brand); border-color: var(--brand); color: #fff; }
.btn-primary:hover { background: var(--brand-600); border-color: var(--brand-600); }
.btn-danger { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 35%, var(--line)); }
.btn-danger:hover { background: var(--bad-bg); }
.btn-ghost { border-color: transparent; background: transparent; color: var(--muted); }
.btn-ghost:hover { background: var(--surface-2); color: var(--ink); }
.btn-quiet { background: transparent; color: var(--muted); }
.btn-quiet:hover { background: var(--surface-2); color: var(--ink); }
.btn-sm { padding: 5px 11px; font-size: var(--fs-xs); }
.btn-block { width: 100%; margin-top: 6px; }
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; }

/* ------------------------------------------------------------------ forms */

/* `search` belongs in this list: without it the browser's own dark border wins
   and the front page's search box looks like a different design system. */
input[type=text], input[type=password], input[type=number], input[type=date],
input[type=email], input[type=search], input[type=tel], select, textarea {
  width: 100%;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 8px 11px;
}
/* The file picker draws its own button, so it gets the frame and nothing else
   — restyling the button itself differs per browser and gains nothing. */
input[type=file] {
  width: 100%;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 8px 11px;
}
input[type=file]:disabled { opacity: .6; }

input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-050);
}
textarea { resize: vertical; min-height: 62px; }

.form { padding: 16px; display: flex; flex-direction: column; gap: 4px; }
/* Modal bodies are forms in everything but the class name — several are built
   as bare fields by app.js — so the label and hint styles apply to both, or
   half the dialogs render their help text at body size. */
.form label,
.modal-body > label { font-size: var(--fs-xs); font-weight: 600; color: var(--ink-2); margin-top: 10px; display: block; }
.form .hint,
.modal-body .hint { font-size: var(--fs-xs); color: var(--muted); margin: 3px 0 0; line-height: 1.5; }
/* A hint that reports a refusal. Same size, different colour, so the reason a
   file was not accepted is not read as small print about the field. */
.hint.is-bad { color: var(--bad); }
.modal-body > label.check { font-size: var(--fs-sm); font-weight: 500; color: var(--ink); margin-top: 14px; }
.req { color: var(--brand-ink); }
.form button { margin-top: 16px; align-self: flex-start; }
.field-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0 14px; }

.check { display: flex; align-items: center; gap: 7px; font-size: var(--fs-sm); font-weight: 500; color: var(--ink-2); cursor: pointer; }
.check input { width: auto; }
.check-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 6px;
  max-height: 210px;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 10px;
  margin-top: 6px;
}

/* A list of things to choose between where each option needs two or three
   lines of its own — a file with its date, size and interval. Deliberately
   not .check-list, which is a 180px grid built for one-word mountpoint names
   and turns multi-line options into an unreadable overlap. */
.pick-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 260px;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 8px;
  margin-top: 6px;
}
.pick {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  padding: 9px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  cursor: pointer;
  font-size: var(--fs-sm);
}
.pick:hover { background: var(--surface-2); }
/* A pick that also has an action of its own. The button cannot go inside the
   <label>: clicking it would select the radio it sits in, so removing a file
   would first choose it. */
.pick-row { display: flex; align-items: center; gap: 6px; }
.pick-row .pick { flex: 1; min-width: 0; }
.pick-actions { display: flex; flex-direction: column; gap: 4px; flex: none; }

/* A short list of reasons, in a modal. Not a table: each row is one sentence
   and a table's column headings would say nothing the sentence does not. */
.plain-list { margin: 6px 0 0; padding-left: 18px; font-size: var(--fs-sm); }
.plain-list li { margin: 3px 0; }
.pick-actions .btn { white-space: nowrap; }
.pick input { margin-top: 3px; flex: none; }
.pick-body { min-width: 0; }
.pick-sub {
  color: var(--muted);
  font-size: var(--fs-xs);
  margin-top: 2px;
  /* Prose here wraps at spaces; only the file name needs breaking mid-word,
     and applying `break-all` to the whole line split "sijaintia" in two on a
     phone. */
  overflow-wrap: break-word;
}
.pick-file { word-break: break-all; }

/* ------------------------------------------------- a choice between two routes

   Used where the panel offers two ways to reach the same result and the
   operator has to pick one. Side by side rather than stacked, because the
   whole point is comparing them; the recommended one carries a brand rail so
   the eye lands there first without the other looking broken. */

.choice-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 14px;
}
.choice {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.choice.is-recommended {
  border-color: var(--brand);
  box-shadow: inset 3px 0 0 var(--brand);
}
.choice-head {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--fs-md);
}
.choice .stat-list { margin: 0; }
.choice .hint { margin: 0; }
.choice .btn-row { margin-top: auto; }
.choice > .btn { margin-top: auto; align-self: flex-start; }

/* The exception, folded away: available, not offered. */
.ppp-exception { margin-top: 12px; }
.ppp-exception > summary {
  cursor: pointer;
  font-size: var(--fs-sm);
  color: var(--muted);
  padding: 6px 0;
}
.ppp-exception > summary:hover { color: var(--ink-2); }
.ppp-exception[open] > summary { color: var(--ink-2); margin-bottom: 8px; }

/* ----------------------------------------------------------------- modals */

.modal-root {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 40px 16px;
  overflow-y: auto;
  background: rgba(30, 22, 20, .45);
  backdrop-filter: blur(2px);
}
.modal {
  width: 100%;
  max-width: 620px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}
.modal-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 15px 18px;
  border-bottom: 1px solid var(--line);
}
.modal-head h2 { font-size: var(--fs-lg); }
.modal-body { padding: 4px 18px 18px; }
.modal-foot {
  display: flex; justify-content: flex-end; gap: 9px;
  padding: 14px 18px;
  border-top: 1px solid var(--line);
  background: var(--surface-2);
}
.modal .form { padding: 0; }
.modal .form button { margin-top: 0; }

/* -------------------------------------------------------------- station detail */

.detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 18px; }
.stat-list { display: grid; grid-template-columns: auto 1fr; gap: 7px 16px; font-size: var(--fs-sm); }
.stat-list dt { color: var(--muted); }
.stat-list dd { margin: 0; font-variant-numeric: tabular-nums; }

.mountpoint-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  padding: 3px 9px;
  border-radius: var(--radius-xs);
  background: var(--brand-050);
  color: var(--brand-ink);
  border: 1px solid var(--brand-100);
  margin: 0 5px 5px 0;
}
/* A name the rover can be given, but not one this station is behind. Muted
   rather than hidden: the operator still needs to know AUTO exists. */
.mountpoint-chip.is-muted {
  background: var(--surface-2);
  color: var(--muted);
  border-color: var(--line);
}


.callout {
  padding: 11px 14px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border-left: 3px solid var(--brand);
  font-size: var(--fs-sm);
  margin: 12px 0;
}
.callout.is-warn { background: var(--warn-bg); border-left-color: var(--warn); }
.callout.is-ok   { background: var(--ok-bg);   border-left-color: var(--ok); }
.callout.is-bad  { background: var(--bad-bg);  border-left-color: var(--bad); }
.callout.is-info { background: var(--surface-2); border-left-color: var(--info); }

/*
 * The security checklist.
 *
 * Deliberately not styled as three levels of alarm. Only the rows that are
 * genuinely a weakness get the warning tint; the rest are informational,
 * because "the station list is public" is a decision and not a fault, and
 * painting it amber would train the operator to ignore amber.
 *
 * `audit-` and not `check-`: `.check-list` above is the 180px checkbox grid
 * for mountpoint names, and naming this one the obvious thing quietly
 * inherited its `grid-template-columns` and its `max-height: 210px` — seven
 * columns of clipped text, in a file that already carries a comment on
 * `.pick-list` warning about exactly this. It looked deliberate enough in a
 * screenshot to argue about; the second time is why the prefix is different.
 */
.audit-list { display: grid; gap: 10px; margin-top: 14px; }

.audit-row {
  display: flex;
  gap: 11px;
  align-items: flex-start;
  padding: 11px 14px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border-left: 3px solid var(--line);
  font-size: var(--fs-sm);
}
.audit-row.is-ok   { background: var(--ok-bg);   border-left-color: var(--ok); }
.audit-row.is-warn { background: var(--warn-bg); border-left-color: var(--warn); }
.audit-row.is-info { border-left-color: var(--info); }

.audit-mark {
  flex: 0 0 20px;
  height: 20px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: var(--fs-2xs);
  font-weight: 700;
  color: #fff;
  background: var(--muted);
}
.audit-row.is-ok .audit-mark   { background: var(--ok); }
.audit-row.is-warn .audit-mark { background: var(--warn); }
.audit-row.is-info .audit-mark { background: var(--info); }

.audit-detail { color: var(--ink-2); margin-top: 3px; line-height: 1.5; }

/* The command to type, and it has to survive being copied: `pre` keeps the
   line breaks that separate two commands from each other, and the horizontal
   scroll keeps a long certificate path from widening the whole page on a
   phone. */
.audit-fix {
  margin: 8px 0 0;
  padding: 8px 11px;
  border-radius: var(--radius-xs);
  background: var(--surface);
  border: 1px solid var(--line);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink);
  overflow-x: auto;
  white-space: pre;
}

/* The rover diagnosis: a stack of findings, worst first. The body text is
   pre-wrapped because these are written as paragraphs with line breaks that
   carry meaning — a list of next steps, not one run-on sentence. */
.finding {
  border-left: 3px solid var(--line);
  background: var(--surface-2);
  border-radius: var(--radius-sm);
  padding: 11px 14px;
  margin: 0 0 10px;
}
.finding.is-bad  { border-left-color: var(--bad);  background: var(--bad-bg); }
.finding.is-warn { border-left-color: var(--warn); background: var(--warn-bg); }
.finding.is-ok   { border-left-color: var(--ok);   background: var(--ok-bg); }
.finding.is-info { border-left-color: var(--info); }
.finding h4 { margin: 0 0 5px; font-size: var(--fs-md); }
.finding p { margin: 0; font-size: var(--fs-sm); white-space: pre-wrap; }

/* The station's own diagnosis, above the reference fields. Separated by a rule
   rather than a box, because it is part of the station panel and not a second
   card floating on top of it. */
.station-diagnosis {
  border-bottom: 1px solid var(--line);
  padding-bottom: 4px;
}
.station-diagnosis h3 {
  margin: 0 0 10px;
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
}

/* Who this account belongs to, above the diagnosis: the operator is on the
   phone and has to confirm they have the right person before reading out an
   answer. */
.identity-card {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 11px 14px;
  margin: 0 0 14px;
}
.identity-head { font-size: var(--fs-md); margin-bottom: 8px; }
.identity-grid {
  display: grid;
  grid-template-columns: minmax(120px, auto) 1fr;
  gap: 3px 14px;
  margin: 0;
  font-size: var(--fs-sm);
}
.identity-grid dt { color: var(--muted); }
.identity-grid dd { margin: 0; word-break: break-word; }

.password-reveal {
  display: flex; align-items: center; gap: 10px;
  font-family: var(--mono);
  font-size: var(--fs-lg);
  background: var(--surface-2);
  border: 1px dashed var(--brand);
  border-radius: var(--radius-sm);
  padding: 11px 14px;
  margin: 10px 0;
  word-break: break-word;
  /* The setup sheet is multi-line; a bare password stays on one line. */
  white-space: pre-wrap;
  line-height: 1.45;
  max-height: 340px;
  overflow-y: auto;
}
.password-reveal span { flex: 1; min-width: 0; }

/* An archived message, shown exactly as it was sent — line breaks and all,
   because a customer message read as reflowed prose is not the thing that was
   sent. Scrolls inside itself so a long setup sheet cannot stretch the modal
   past the screen. */
.message-text {
  margin: 12px 0 0;
  padding: 12px 14px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: var(--fs-xs);
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 55vh;
  overflow: auto;
}

/* -------------------------------------------------------------- searchbar */

.searchbar { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; }
.searchbar input[type=search] {
  flex: 1;
  min-width: 0;
  font-size: var(--fs-md);
  padding: 11px 14px;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  appearance: none; /* drop Safari's inset bevel and the WebKit clear button */
}
.searchbar input[type=search]::-webkit-search-decoration,
.searchbar input[type=search]::-webkit-search-cancel-button { appearance: none; }

/* Search hits: one item per row, laid out the same way at every width.
   Deliberately not a table — see renderSearch() for why. */
.search-hits { list-style: none; margin: 0; padding: 0; }
.search-hit {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px 16px;
  padding: 12px 16px;
  border-top: 1px solid var(--line);
  cursor: pointer;
}
.search-hit:first-child { border-top: 0; }
.search-hit:hover { background: var(--surface-2); }
.search-hit-main { min-width: 0; }
.search-hit-title { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.search-hit-title b { font-size: var(--fs-md); }
.search-hit-sub {
  margin-top: 3px;
  font-size: var(--fs-sm);
  color: var(--muted);
  overflow-wrap: anywhere; /* an email address must not widen the page */
}
.search-hit-status { flex: 0 0 auto; text-align: right; white-space: nowrap; }
.search-hit-when { margin-top: 3px; font-size: var(--fs-xs); color: var(--muted); }

@media (max-width: 640px) {
  /* The state drops under the name rather than being squeezed beside it, and
     goes left-aligned with it — a lone right-aligned badge under a left-
     aligned name reads as a different row. */
  .search-hit { display: block; }
  .search-hit-status { margin-top: 7px; text-align: left; white-space: normal; }
  .search-hit-when { display: inline; margin-left: 8px; }
}

.maintenance-banner {
  border-left: 3px solid var(--warn);
  background: var(--warn-bg);
  padding: 12px 16px;
  font-size: var(--fs-sm);
}
.maintenance-banner.is-running { border-left-color: var(--bad); background: var(--bad-bg); }

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

.template { border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 10px 12px; margin-bottom: 10px; }
.template > summary { cursor: pointer; font-weight: 600; font-size: var(--fs-sm); }
.template textarea { font-family: var(--mono); font-size: var(--fs-xs); margin-top: 8px; }
.template code {
  font-family: var(--mono);
  font-size: var(--fs-2xs);
  background: var(--surface-2);
  padding: 1px 5px;
  border-radius: var(--radius-xs);
}

/* ---------------------------------------------------------- notifications */

.bell {
  position: relative;
  width: 30px; height: 30px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--muted);
  font: inherit;
  font-weight: 800;
  font-size: var(--fs-sm);
  cursor: pointer;
  line-height: 1;
}
.bell:hover { background: var(--surface-2); color: var(--ink); }
.bell.has-unread { border-color: var(--brand); color: var(--brand-ink); background: var(--brand-050); }
.bell-count {
  position: absolute;
  top: -5px; right: -6px;
  min-width: 16px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--bad);
  color: #fff;
  font-size: var(--fs-2xs);
  font-weight: 700;
  line-height: 16px;
}

.notices { max-height: 460px; overflow-y: auto; }
.notice {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 12px 16px;
  border-bottom: 1px solid var(--line);
  border-left: 3px solid var(--brand);
}
.notice:last-child { border-bottom: 0; }
.notice.is-info { border-left-color: var(--ok); }
.notice.is-warn { border-left-color: var(--warn); }
.notice.is-error { border-left-color: var(--bad); }
.notice.is-read { opacity: .55; }
.notice-main { flex: 1; min-width: 0; }
.notice-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: var(--fs-sm); }
.notice-time { color: var(--muted); font-size: var(--fs-xs); margin-left: auto; white-space: nowrap; }
.notice-body { font-size: var(--fs-sm); color: var(--ink-2); margin-top: 5px; white-space: normal; }
.notice-meta { font-size: var(--fs-xs); color: var(--muted); margin-top: 6px; }
.notice-actions { display: flex; flex-direction: column; gap: 5px; flex-shrink: 0; }

/* --------------------------------------------------------------- progress */

.progress {
  height: 8px;
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
  margin: 10px 0 4px;
}
.progress-bar {
  height: 100%;
  background: var(--brand);
  border-radius: 999px;
  transition: width .4s ease;
}
.month-select { width: auto; min-width: 160px; padding: 5px 10px; font-size: var(--fs-xs); }

/* -------------------------------------------------------------------- log */

.log { max-height: 640px; overflow-y: auto; font-family: var(--mono); font-size: var(--fs-xs); }
.log-row { display: flex; gap: 12px; padding: 5px 16px; border-bottom: 1px solid var(--line); }
.log-row:hover { background: var(--surface-2); }
.log-time { color: var(--muted); white-space: nowrap; }
.log-level { width: 46px; font-weight: 700; }
.log-row.is-warn  .log-level { color: var(--warn); }
.log-row.is-error .log-level { color: var(--bad); }
.log-row.is-info  .log-level { color: var(--info); }
.log-msg { flex: 1; word-break: break-word; }
.log-meta { color: var(--muted); }

/* ----------------------------------------------------------------- toasts */

.toasts { position: fixed; right: 18px; bottom: 18px; z-index: 200; display: flex; flex-direction: column; gap: 9px; }
.toast {
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--brand);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
  padding: 11px 15px;
  font-size: var(--fs-sm);
  max-width: 380px;
  animation: toast-in .18s ease-out;
}
.toast.is-error { border-left-color: var(--bad); }
.toast.is-ok { border-left-color: var(--ok); }
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } }

.settings-group { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .07em; color: var(--brand-ink); margin: 18px 0 2px; }

.fault-stack {
  font-family: var(--mono);
  font-size: var(--fs-2xs);
  background: var(--surface-2);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  overflow-x: auto;
  white-space: pre;
  margin: 8px 0;
}

/* ------------------------------------------------ loading and empty states

   A panel that draws its boxes and then leaves them blank for a second reads
   as broken. Two things fix that: a skeleton while data is on its way, and a
   sentence — not an empty box — when there is genuinely nothing to show. */

.skeleton-block,
.skeleton-line {
  background: linear-gradient(
    90deg,
    var(--surface-2) 25%,
    color-mix(in srgb, var(--surface-2) 55%, var(--surface)) 37%,
    var(--surface-2) 63%
  );
  background-size: 400% 100%;
  animation: shimmer 1.4s ease-in-out infinite;
  border-radius: var(--radius-sm);
}
.skeleton-block { height: 76px; margin: 12px 0; }
.skeleton-line { height: 12px; margin: 9px 0; }
.skeleton-line.is-short { width: 45%; }
.skeleton-line.is-medium { width: 72%; }

@keyframes shimmer {
  from { background-position: 100% 0; }
  to   { background-position: 0 0; }
}

/* Someone who has set "reduce motion" gets a still placeholder, not a pulse. */
@media (prefers-reduced-motion: reduce) {
  .skeleton-block, .skeleton-line { animation: none; background: var(--surface-2); }
  .toast { animation: none; }
  * { scroll-behavior: auto !important; }
}

/* The KPI row before any data has arrived: the right number of cards, at the
   right size, so nothing jumps when the real values land. */
.kpi.is-loading .kpi-value,
.kpi.is-loading .kpi-sub,
.kpi.is-loading .kpi-label { color: transparent; }
.kpi.is-loading .kpi-value { background: var(--surface-2); border-radius: var(--radius-xs); width: 60%; }
.kpi.is-loading .kpi-label { background: var(--surface-2); border-radius: var(--radius-xs); width: 45%; }
.kpi.is-loading .kpi-sub   { background: var(--surface-2); border-radius: var(--radius-xs); width: 80%; }

.empty-state {
  margin: 0;
  padding: 30px 20px;
  text-align: center;
  color: var(--muted);
  font-size: var(--fs-sm);
  line-height: 1.6;
}

/* ------------------------------------------------ subscriptions and money */

.tab-badge {
  display: inline-block;
  min-width: 18px;
  margin-left: 7px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--bad);
  color: #fff;
  font-size: var(--fs-2xs);
  font-weight: 700;
  line-height: 18px;
  text-align: center;
  vertical-align: 1px;
}
/*
 * White text on a filled semantic colour, in dark mode.
 *
 * `--ok`, `--warn`, `--bad`, `--info` and `--muted` are all *light* in dark
 * mode, because their usual job is coloured text on a dark surface. The
 * moment one of them becomes a background with white on top, the ratio
 * inverts: 2.7:1 on the counters, 2.1:1 on the checklist markers.
 *
 * This is the third time it has been found, and the first two were fixed one
 * selector at a time — the tab badge got the override and the bell beside it
 * did not. So it is one rule now: any new chip, pill or marker that fills
 * itself with a semantic colour belongs in this list, not in a copy of it.
 */
:root[data-theme="dark"] .tab-badge,
:root[data-theme="dark"] .bell-count,
:root[data-theme="dark"] .audit-mark { color: #1A1210; }

.panel-note { font-size: var(--fs-xs); color: var(--muted); }
.cell-sub { font-size: var(--fs-xs); color: var(--muted); margin-top: 2px; }
.row-actions { white-space: nowrap; text-align: right; }

.reminder {
  display: flex;
  gap: 16px;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  padding: 14px 16px;
  border-bottom: 1px solid var(--line);
  border-left: 3px solid transparent;
}
.reminder:last-child { border-bottom: 0; }
.reminder.is-urgent { border-left-color: var(--brand); background: var(--brand-050); }
.reminder-main { flex: 1 1 280px; min-width: 0; }
.reminder-title { font-weight: 650; }
.reminder-sub { font-size: var(--fs-xs); color: var(--muted); margin-top: 3px; word-break: break-word; }
.reminder-actions { display: flex; gap: 8px; flex-wrap: wrap; }

.form-section {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}

/*
 * A form long enough to scroll gets its sections side by side once there is
 * room. The price list is the worst of them: four groups, thirteen fields, laid
 * out in one 250px column inside a 1400px panel — a screen and a half of
 * scrolling past three quarters of empty page, and no way to see what you are
 * changing next to what it affects.
 *
 * Only where a form asks for it (`is-wide`), and only above 900px, because
 * the fields themselves must not stretch: a number input as wide as the panel
 * invites reading it as a text field.
 */
@media (min-width: 900px) {
  .form.is-wide {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 0 32px;
    align-items: start;
  }
  .form.is-wide .form-section { margin-top: 0; }
  /* The example and the save button belong to the whole form, not a column. */
  .form.is-wide > .callout,
  .form.is-wide > button { grid-column: 1 / -1; }
}
.form-section h3 {
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--brand-ink);
  margin-bottom: 4px;
}

.report-head { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.report-head label { font-size: var(--fs-xs); font-weight: 600; color: var(--ink-2); }
.report-head select { width: auto; min-width: 120px; }
.report h3 { margin: 20px 0 8px; font-size: var(--fs-md); }
.report .kpi-row { margin: 0; }

/* ------------------------------------------------------------------ mobile

   The panel has to be usable from a phone in a yard, not only from a desk.
   Three sizes: full desktop, a tablet/narrow-window layout that drops the
   two-column grids, and a phone layout where the tab bar scrolls sideways and
   the wide tables become cards.

   Tables are the hard part. Below 640px each row is re-laid-out as a stacked
   card with the column heading printed in front of each cell, which is why
   every table cell that needs one carries a data-label. */

@media (max-width: 1024px) {
  .grid-2, .detail-grid { grid-template-columns: 1fr; }
  .kpi-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 760px) {
  html, body { font-size: 15px; }
  main { padding: 12px; }

  .topbar { padding: 8px 12px; gap: 8px; flex-wrap: wrap; }
  .topbar-stats { display: none; }
  .brand-sub { display: none; }
  .admin-name { display: none; }

  /* The tab bar scrolls rather than wrapping into three rows. */
  .tabs {
    overflow-x: auto;
    flex-wrap: nowrap;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    padding: 0 8px;
  }
  .tabs::-webkit-scrollbar { display: none; }
  .tab { white-space: nowrap; flex: 0 0 auto; }

  /* Two across rather than four stacked: four full-width cards push the map
     below the fold on every phone. */
  .kpi-row { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .kpi { padding: 12px 13px; }
  .kpi-value { font-size: var(--fs-xl); }
  .kpi-label { font-size: var(--fs-2xs); }
  .kpi-sub { font-size: var(--fs-2xs); }

  .map { height: 320px; }
  .panel-head { flex-wrap: wrap; gap: 8px; }

  .modal { max-width: 100%; margin: 0; border-radius: 0; max-height: 100vh; }
  .modal-root { padding: 0; align-items: stretch; }
  .modal-foot { flex-wrap: wrap; }
  .modal-foot .btn { flex: 1 1 auto; }

  .field-grid { grid-template-columns: 1fr; }
  .reminder-actions { width: 100%; }
  .reminder-actions .btn { flex: 1 1 auto; }

  /* Touch targets: 44px is the smallest a finger reliably hits. */
  .btn { min-height: 40px; }
  .btn-sm { min-height: 36px; padding: 7px 12px; }
  input, select, textarea { font-size: 16px; } /* stops iOS zooming on focus */
}

@media (max-width: 640px) {
  .table thead { display: none; }
  .table, .table tbody, .table tr, .table td { display: block; width: 100%; }
  .table tr {
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    margin-bottom: 10px;
    padding: 4px 0;
    background: var(--surface);
  }
  .table td {
    display: flex;
    /* The label is pushed apart from the value by the label's own margin, not
       by space-between: a cell holding two chips (KIUR and KIUR_PPP) had them
       flung to opposite edges of the row, reading as two unrelated columns. */
    justify-content: flex-end;
    align-items: baseline;
    gap: 8px;
    border: 0;
    padding: 7px 13px;
    text-align: right;
  }
  .table td.num { text-align: right; }
  .table td::before {
    content: attr(data-label);
    flex: 0 0 auto;
    margin-right: auto;
    font-size: var(--fs-2xs);
    font-weight: 650;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--muted);
    text-align: left;
  }
  /* A cell with no heading (an action column, say) spans the row instead. */
  .table td:not([data-label]) { justify-content: flex-end; }
  .table td.empty { display: block; text-align: center; }
  .table-wrap { overflow-x: visible; }

  /* A notice on a phone: the buttons take a column of their own out of a
     screen that has none to spare, squeezing the text into a ribbon. They go
     under it instead, side by side, and the timestamp joins the title row. */
  .notice { display: block; }
  .notice-time { margin-left: 0; }
  .notice-actions {
    flex-direction: row;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 10px;
  }
  /* A borderless button in its own column reads as a button; the same button
     on a line under a paragraph reads as text. There is no hover on a phone
     to correct the impression, so give it its outline back. */
  .notice-actions .btn-ghost { border-color: var(--line); color: var(--ink); }

/* Who acknowledged an inbox entry, and when. Matters only once there are two
   admins — which is exactly when "read" alone answers the wrong question. */
.notice-ack { font-size: var(--fs-xs); color: var(--muted); white-space: nowrap; }

  .login-card { padding: 26px 20px; border-radius: var(--radius); }
  .toasts { left: 12px; right: 12px; bottom: 12px; }
  .toast { max-width: none; }
}

/* Landscape on a phone: the map would otherwise take the whole viewport. */
@media (max-height: 520px) and (orientation: landscape) {
  .map { height: 260px; }
}

/* ------------------------------------------------------------- refinements

   A form inside a full-width panel would otherwise stretch its inputs across
   1500px, which looks broken and is harder to read than a short field. Cap
   the measure; a form inside a modal is already narrow enough. */

.panel > .form { max-width: 720px; }
.panel > .form input,
.panel > .form select,
.panel > .form textarea { max-width: 320px; }
.panel > .form .field-grid input,
.panel > .form .field-grid select { max-width: none; }

/* The report reuses the KPI card, but in a modal there is room for two across,
   not four, and a value like "ei mittausta" needs a smaller size than "99 %". */
.report .kpi-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.report .kpi-value { font-size: var(--fs-xl); }
@media (max-width: 560px) {
  .report .kpi-row { grid-template-columns: 1fr; }
}

/* A long word in a table cell must wrap rather than force a sideways scroll —
   except a mountpoint name, which is what the customer types into a receiver
   and must never be shown broken across two lines. */
.table td { overflow-wrap: anywhere; }
.table td.num { overflow-wrap: normal; }
.mountpoint-chip { white-space: nowrap; overflow-wrap: normal; }

/* ------------------------------------------------------- liittymislomake */

/* The public onboarding form. Shares the panel's tokens so the two look like
   one product, but it is a plain single-column document — a customer filling
   this in on a phone in a tractor cab is the case to design for. */

.join-page {
  min-height: 100vh;
  padding: 24px 16px 64px;
  background:
    radial-gradient(1200px 600px at 50% -10%, var(--brand-050), transparent 70%),
    var(--bg);
}

.join-card {
  width: 100%;
  max-width: 620px;
  margin: 0 auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  padding: 32px 28px;
}

.join-logo { display: block; margin: 0 auto 14px; }
.join-card h1 { font-size: var(--fs-xl); text-align: center; margin-bottom: 6px; }
.join-lead {
  text-align: center;
  color: var(--muted);
  font-size: var(--fs-sm);
  line-height: 1.55;
  margin-bottom: 8px;
}

.join-section { border-top: 1px solid var(--line); margin-top: 26px; padding-top: 20px; }
.join-section:first-of-type { border-top: 0; }
.join-section h2 {
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--brand-ink);
  margin-bottom: 14px;
}
.join-section-lead { margin: -6px 0 14px; }

.join-card label {
  display: block;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--ink-2);
  margin: 14px 0 5px;
}
.join-card label.check {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  font-weight: 500;
  font-size: var(--fs-sm);
  color: var(--ink);
  cursor: pointer;
}
.join-card label.check input { width: auto; margin-top: 2px; flex: none; }
.join-card .hint { font-size: var(--fs-xs); color: var(--muted); margin: 5px 0 0; line-height: 1.5; }
.join-card .req { color: var(--brand-ink); }

.join-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 14px; }
@media (max-width: 520px) { .join-grid { grid-template-columns: 1fr; } }

/* The live price. Itemised on purpose: "three connections" reads like three
   times the price until the taper is spelled out. */
.join-price {
  margin: 16px 0 4px;
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}
.join-price-rows > div,
.join-price-total {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  font-size: var(--fs-sm);
  padding: 3px 0;
}
.join-price-total {
  border-top: 1px solid var(--line);
  margin-top: 8px;
  padding-top: 9px;
  font-size: var(--fs-md);
}
.join-price-total strong { font-variant-numeric: tabular-nums; }

.join-terms {
  max-height: 260px;
  overflow-y: auto;
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  line-height: 1.6;
  color: var(--ink-2);
  white-space: pre-wrap;
  margin-bottom: 16px;
}

.join-support { text-align: center; color: var(--muted); font-size: var(--fs-xs); margin-top: 18px; }
.btn-lg { padding: 12px 20px; font-size: var(--fs-md); margin-top: 24px; }

/* ------------------------------------------------------ asetusten osiot */

/* Settings used to be nine unrelated topics in one long scroll. Sub-tabs put
   one on screen at a time. */
.subtabs {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  margin-bottom: 16px;
  padding: 4px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.subtab {
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
  padding: 8px 14px;
  cursor: pointer;
}
.subtab:hover { color: var(--ink); background: var(--surface); }
.subtab.is-active { color: var(--brand-ink); background: var(--surface); box-shadow: var(--shadow-sm); }

/*
 * Sub-tabbed screens. Settings was nine unrelated panels in one scroll and
 * nobody reached the bottom; the subscriptions view had the same problem with
 * three. One topic per screen, and the same two rules serve both.
 */
.settings-section, .subs-section { display: none; }
.settings-section.is-active, .subs-section.is-active { display: block; }

/* ---------------------------------------------------------- hakemukset */

.app-card {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 18px 20px;
  margin-bottom: 14px;
}
/* One that needs the operator now is the only one worth colouring. */
.app-card.is-waiting {
  border-color: color-mix(in srgb, var(--brand) 40%, var(--line));
  background: var(--brand-050);
}
.app-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 16px;
  margin-bottom: 12px;
}
.app-title { font-weight: 700; font-size: var(--fs-lg); }
.app-sub { color: var(--muted); font-size: var(--fs-xs); margin-top: 2px; }
.app-note { font-size: var(--fs-sm); color: var(--muted); margin: 4px 0 12px; line-height: 1.55; }
.app-note.is-ok { color: var(--ok); }

.app-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 10px 24px;
  margin: 0 0 14px;
}
.app-facts dt {
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}
.app-facts dd { margin: 2px 0 0; font-size: var(--fs-sm); white-space: pre-wrap; overflow-wrap: anywhere; }

@media (max-width: 640px) {
  .app-head { flex-direction: column; gap: 8px; }
  .app-facts { grid-template-columns: 1fr; }
}

/* Pushes what follows to the far end of a button row, so a destructive
   action is never adjacent to a routine one. */
.btn-row-gap { flex: 1 1 auto; min-width: 8px; }

/* ---------------------------------------------------------- tilannesivu */

/* One answer, big enough to read at arm's length in a cab: the customer
   opened this page to learn exactly one thing. */
.st-status {
  border-radius: var(--radius);
  padding: 20px 22px;
  margin: 20px 0 22px;
  border: 1px solid var(--line);
}
.st-status.is-ok  { background: var(--ok-bg);  border-color: color-mix(in srgb, var(--ok) 30%, var(--line)); }
.st-status.is-bad { background: var(--bad-bg); border-color: color-mix(in srgb, var(--bad) 30%, var(--line)); }
.st-status-title { font-size: var(--fs-lg); font-weight: 700; }
.st-status.is-ok  .st-status-title { color: var(--ok); }
.st-status.is-bad .st-status-title { color: var(--bad); }
.st-status-note { font-size: var(--fs-sm); color: var(--ink-2); margin-top: 6px; line-height: 1.5; }

.st-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px 20px; margin: 0 0 20px; }
.st-facts dt { font-size: var(--fs-2xs); font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
.st-facts dd { margin: 3px 0 0; font-size: var(--fs-md); font-variant-numeric: tabular-nums; }

.st-heading { font-size: var(--fs-xs); font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--brand-ink); margin: 22px 0 10px; }
.st-stations { border: 1px solid var(--line); border-radius: var(--radius-sm); overflow: hidden; }
.st-station {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 12px 14px;
  border-top: 1px solid var(--line);
}
.st-station:first-child { border-top: 0; }
.st-station-name { font-weight: 600; font-size: var(--fs-md); }
.st-station-sub { font-size: var(--fs-xs); color: var(--muted); margin-top: 1px; }
