/* Jaltech EPC Portal — the design system.
 *
 * Every colour in the portal is a custom property declared twice: once on
 * :root (dark, the default) and once on :root[data-theme="light"]. The shell
 * writes data-theme from the portal_theme cookie, so a theme change is one
 * attribute and no other rule needs to know about it.
 *
 * The palette is Jaltech's OFFICIAL one — docs/branding/colours-fonts and
 * the Corporate Identity Guidelines: Jaltech Navy #314F62 primary, Jaltech
 * Slate #809EAD secondary. The supporting ladder values were measured from
 * jaltech.co.za and agree with the guidelines' tints/shades to within a few
 * units. Both themes use those colours and no others, apart from the four
 * status tokens, which Jaltech do not publish.
 *
 * Their ladder, darkest to lightest:
 *
 *   #142B38  deep navy      #BFCED5  muted line
 *   #314F62  brand slate    #E6EAEC  pale blue-grey
 *   #506F7F  mid slate      #F4F4F4  off-white
 *   #809EAD  light slate    #FFFFFF  white
 *
 * Dark reads it bottom-up and light reads it top-down. What is NOT taken from
 * them is the PAIRING: their site puts #314F62 text on #142B38 (1.69:1) and
 * #979797 on #F4F4F4 (2.66:1), and both fail WCAG AA. So the assignments are
 * contrast-led:
 *
 *   the brand slate is a SURFACE, never an ink. It is --surface-card on
 *                   dark, where white on it is 8.68:1 — exactly how Jaltech
 *                   use it. As a text colour on dark it would be 1.69:1.
 *   --brand-action  is the interactive fill, and it flips by theme because the
 *                   page beneath it does. On light it is their pill exactly.
 *                   On dark a #314F62 pill on a #142B38 page is the 1.69:1
 *                   pair again, so the CTA becomes the light slate carrying
 *                   deep navy — hence --on-action, which is not always white.
 *   --ink-dim       is the muted line on dark and the mid slate on light,
 *                   never their body grey, which cannot pass on their own
 *                   off-white.
 *
 * The @contrast manifest below is machine-read by
 * tests/test_theme.py::test_every_manifest_pair_meets_wcag_aa_in_both_themes,
 * which resolves both tokens in BOTH theme blocks and fails under threshold.
 * Classifier `text` requires 4.5:1 (WCAG 1.4.3); `ui` requires 3:1 (1.4.11,
 * non-text contrast) and is for the visual boundary of a control.
 *
 * Every colour token must appear in a manifest line or in @contrast-exempt,
 * so a token cannot dodge the gate by being left out of the list.
 *
 * @contrast --ink          on --surface        : text
 * @contrast --ink          on --surface-card   : text
 * @contrast --ink          on --glass-solid    : text
 * @contrast --ink-dim      on --glass-solid    : text
 * @contrast --ink-dim      on --surface        : text
 * @contrast --ink-dim      on --surface-card   : text
 * @contrast --accent       on --surface        : text
 * @contrast --accent       on --surface-card   : text
 * @contrast --on-action    on --brand-action   : text
 * @contrast --ok           on --surface        : text
 * @contrast --warn         on --surface        : text
 * @contrast --danger       on --surface        : text
 * @contrast --info         on --surface        : text
 * @contrast --ok           on --surface-card   : text
 * @contrast --warn         on --surface-card   : text
 * @contrast --danger       on --surface-card   : text
 * @contrast --info         on --surface-card   : text
 * @contrast --topbar-ink       on --topbar        : text
 * @contrast --topbar-ink-dim   on --topbar        : text
 * @contrast --on-topbar-action on --topbar-action : text
 * @contrast --topbar-action    on --topbar        : ui
 * @contrast --ink          on --field-bg       : text
 * @contrast --ink-dim      on --field-bg       : text
 * @contrast --field-line   on --field-bg       : ui
 * @contrast --field-line   on --surface        : ui
 * @contrast --field-line   on --surface-card   : ui
 * @contrast --focus        on --surface        : ui
 * @contrast --focus        on --surface-card   : ui
 * @contrast --brand-action on --surface        : ui
 * @contrast --brand-action on --surface-card   : ui
 * @contrast --brand-action on --field-bg       : ui
 * @contrast --sx-ink        on --sx-panel      : text
 * @contrast --sx-data       on --sx-panel      : text
 * @contrast --sx-data       on --sx-surface    : text
 * @contrast --sx-context    on --sx-panel      : ui
 * @contrast --sx-context    on --sx-surface    : ui
 * @contrast --sx-on-accent  on --sx-accent     : text
 * @contrast --sx-accent     on --sx-surface    : ui
 * @contrast --sx-accent     on --sx-panel      : ui
 *
 * @contrast-exempt --line --sx-line
 *   A decorative rule between rows and sections. WCAG 1.4.11 covers the
 *   boundary of a CONTROL, which is --field-line; it does not require
 *   decorative separators to reach 3:1, and forcing them there would give a
 *   harsh grid that looks nothing like Jaltech.
 *
 * @contrast-exempt --glass-fill --glass-edge --topbar-hover --menu-hover --topbar-edge
 *   Translucent. A ratio is undefined without a named backdrop, and computing
 *   one against the token's own hex would flatter it. The readable case is
 *   covered instead by gating --ink and --ink-dim against --glass-solid, the
 *   opaque colour the panel falls back to and the darkest thing the glass can
 *   resolve to over --surface or --surface-card. --topbar-hover is a white
 *   wash over the fixed navy bar; the text on it is gated as --topbar-ink.
 */

/* ---- Poppins ------------------------------------------------------------
 * Self-hosted, latin subset, from Google's own files. Not linked from
 * fonts.googleapis.com: a sign-in-only portal should not tell a third party
 * who is loading it, and self-hosting is how Montserrat already ships for the
 * PDF renderer. font-display:swap so a slow font never blanks the page.
 */
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url("fonts/poppins-300.woff2") format("woff2");
}
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/poppins-400.woff2") format("woff2");
}
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("fonts/poppins-500.woff2") format("woff2");
}
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("fonts/poppins-600.woff2") format("woff2");
}
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("fonts/poppins-700.woff2") format("woff2");
}

/* Manrope carries button labels and short UI micro-copy — the identity
 * guidelines' second typeface (§4.1: "buttons only"). One variable file
 * covers both sanctioned weights (500 secondary, 600 primary). */
@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-weight: 500 600;
  font-display: swap;
  src: url("fonts/manrope-500-600.woff2") format("woff2");
}


/* ---- tokens: dark (default) --------------------------------------------- */

/* Dark is built from Jaltech's own ladder and nothing else: their deep navy
   is the page, their brand slate is the raised surface, their light slate is
   the call to action, and text is white or their muted line. An earlier
   version invented nine of these eleven values — the page background in
   particular — and the result read as a generic dark app rather than as
   Jaltech. Every value below is one they paint with. */
:root {
  --surface: #142B38;        /* deep navy — their footer */
  --surface-card: #314F62;   /* brand slate — their section bands */
  --brand-action: #809EAD;   /* light slate; carries deep navy, not white */
  --on-action: #142B38;
  --accent: #BFCED5;
  --ink: #FFFFFF;
  --ink-dim: #BFCED5;
  --line: #506F7F;           /* mid slate */
  --field-line: #809EAD;
  --focus: #E6EAEC;
  /* Jaltech publish no status palette, so these four are additions rather
     than brand colours. They are tuned to clear AA on both surfaces above
     rather than picked by eye. */
  --ok: #86E0BC;
  --warn: #F0C077;
  --danger: #FFB3A0;
  --info: #BFCED5;
  /* SunEx's pages (the reports console, the live desk, the guide) take the
     office boards' palette: the same in both themes, since they are always
     dark. body.sunex-page maps the console's tokens onto these. */
  --sx-surface: #111217;
  --sx-panel: #181b1f;
  --sx-line: #23262b;
  --sx-ink: #F2F2F2;
  --sx-data: #ccccdc;
  --sx-context: #8e8e9a;
  --sx-accent: #FAB600;
  --sx-on-accent: #111217;

  /* The top bar is always a dark brand surface, whatever the theme — which
     is what lets one white lock-up be the logo everywhere (the light theme
     used to float the slate mark on a white glass bar: white-on-white chrome
     with a barely-there logo). On dark it is the brand slate, one rung up
     from the navy page, or the bar would dissolve into it; on light it is
     the deep navy itself. The ink and action tokens are identical in both
     blocks because both fills carry them at AA. */
  --topbar: #314F62;
  --topbar-ink: #FFFFFF;
  --topbar-ink-dim: #BFCED5;
  --topbar-action: #809EAD;
  --on-topbar-action: #142B38;
  --topbar-hover: rgba(255, 255, 255, 0.08);
  --topbar-edge: rgba(255, 255, 255, 0.6);
  --menu-hover: rgba(255, 255, 255, 0.07);

  /* What an input paints beneath its text. On dark it is the page colour, as
     before; on light it is white — which is what lets the cards go to the
     brand's pale blue-grey without the fields disappearing into them. */
  --field-bg: #142B38;

  --glass-fill: rgba(255, 255, 255, 0.05);
  --glass-edge: rgba(255, 255, 255, 0.60);
  /* Painted first, so a browser without backdrop-filter still gets a solid
     panel rather than near-transparent text on a photograph. Matches the card
     surface so dim text over the fallback is gated by the same pair. */
  --glass-solid: #314F62;

  /* Jaltech use three radii at three scales: 35px on the floating nav pill,
     26px on cards, 14px on icon tiles. Kept as-is — the roundness is most of
     what makes their surfaces recognisable. */
  --r-pill: 35px;
  --r-card: 26px;
  --r-tile: 14px;
  --r-field: 10px;

  --font: "Poppins", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-button: "Manrope", system-ui, -apple-system, "Segoe UI", sans-serif;

  --shell-pad: 28px;
}

/* Light is the same ladder read the other way up: their off-white is the page,
   white is the card, and their deep navy is the ink. Their own body grey
   (#979797 on #F4F4F4) is 2.66:1 and is deliberately not used — the mid slate
   carries secondary text instead. The light slate is likewise too pale for an
   input border here at 2.55:1, so borders take the mid slate too. */
:root[data-theme="light"] {
  --surface: #F4F4F4;        /* their off-white */
  /* The muted-line rung, and deliberately not the pale blue-grey above it:
     #E6EAEC against the off-white page is a 1.09:1 difference, which a bright
     monitor washes straight back to white-on-white — it shipped and the
     operator immediately asked why nothing had changed. #BFCED5 is 1.47:1
     against the page: unmistakably a card, still nothing but their ladder. */
  --surface-card: #BFCED5;
  --brand-action: #314F62;   /* their pill, exactly as they ship it */
  --on-action: #FFFFFF;
  --accent: #314F62;
  --ink: #142B38;            /* deep navy as ink */
  /* The brand slate, not the mid slate: #506F7F on the blue-grey card is
     4.42:1 and fails AA by a hair. One rung darker passes everywhere. */
  --ink-dim: #314F62;
  --line: #BFCED5;
  --field-line: #506F7F;
  --focus: #314F62;
  --ok: #0F5C3C;
  --warn: #6E470C;           /* 4.5:1 on the card; #7A4E0C was 4.45 */
  --danger: #8F2E12;
  --info: #314F62;
  /* SunEx's pages (the reports console, the live desk, the guide) take the
     office boards' palette: the same in both themes, since they are always
     dark. body.sunex-page maps the console's tokens onto these. */
  --sx-surface: #111217;
  --sx-panel: #181b1f;
  --sx-line: #23262b;
  --sx-ink: #F2F2F2;
  --sx-data: #ccccdc;
  --sx-context: #8e8e9a;
  --sx-accent: #FAB600;
  --sx-on-accent: #111217;

  --topbar: #142B38;
  --topbar-ink: #FFFFFF;
  --topbar-ink-dim: #BFCED5;
  --topbar-action: #809EAD;
  --on-topbar-action: #142B38;
  --topbar-hover: rgba(255, 255, 255, 0.08);
  --topbar-edge: rgba(255, 255, 255, 0.6);
  --menu-hover: rgba(20, 43, 56, 0.06);

  --field-bg: #FFFFFF;

  --glass-fill: rgba(255, 255, 255, 0.55);
  --glass-edge: rgba(48, 79, 98, 0.18);
  --glass-solid: #FFFFFF;
}

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

*,
*::before,
*::after { box-sizing: border-box; }

html { color-scheme: dark; }
:root[data-theme="light"] { color-scheme: light; }

body {
  margin: 0;
  /* Top-anchored. The portal was previously `display:grid;place-items:center`,
     which floated short pages in the middle of the viewport and made content
     jump vertically between pages. */
  min-height: 100vh;
  background: var(--surface);
  color: var(--ink);
  font: 400 15px/1.6 var(--font);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); }
a:hover { text-decoration: none; }

/* One focus treatment everywhere. There was none at all before, so keyboard
   users got whatever the browser happened to do. */
:where(a, button, input, select, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: 4px;
}

h1, h2, h3 {
  /* Jaltech set large headings at weight 400, not bold. The lightness at size
     is the character of the brand; bolding reads as a different company. */
  font-weight: 400;
  line-height: 1.15;
  margin: 0;
}

h1 { font-size: 30px; }
h2 { font-size: 20px; }
h3 { font-size: 16px; }

/* The page's own name: one treatment, on every page. Larger than a section
   head and carrying no accent bar — the bar is Jaltech's device for a heading
   INSIDE a page, which is what `.rule` is now. Every page used to wear the
   bar, so nothing distinguished "where you are" from "a part of this page",
   and /admin wore it three times with no title at all. */
.page-title {
  font-size: 28px;
  font-weight: 700;  /* the identity guidelines' H1 weight */
  letter-spacing: -0.015em;
  margin: 0 0 22px;
}

/* A title that introduces a subtitle sits close to it. */
.page-title:has(+ .page-sub) { margin-bottom: 4px; }

.page-sub {
  margin: 0 0 26px;
  font-size: 14px;
  color: var(--ink-dim);
}

code, kbd, samp { font-family: ui-monospace, "Cascadia Code", Consolas, monospace; }

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

.shell {
  width: min(92vw, var(--shell-w, 720px));
  margin: 0 auto;
  padding: var(--shell-pad) 0 64px;
}

.shell--narrow { --shell-w: 420px; }
/* Wide enough that the 11-column proposal archive reads without a
   horizontal scroll on a desktop viewport; min(92vw, …) still shrinks it
   on smaller windows, where the table wrapper's scroll takes over. */
.shell--wide { --shell-w: 1420px; }

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

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

/* Jaltech's signature: a translucent fill over a heavy blur with a hairline
   edge, and no shadow anywhere. The solid background is declared FIRST so a
   browser without backdrop-filter renders an opaque panel instead of unreadable
   text — @supports then swaps in the glass. */
.glass {
  background: var(--glass-solid);
  border: 1px solid var(--glass-edge);
  border-radius: var(--r-card);
}

@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .glass {
    background: var(--glass-fill);
    -webkit-backdrop-filter: blur(50px);
    backdrop-filter: blur(50px);
  }
}

/* ---- controls ------------------------------------------------------------ */

.btn {
  display: inline-block;
  padding: 12px 24px;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--brand-action);
  color: var(--on-action);
  /* Manrope 600 — the identity guidelines' primary-button label. */
  font: 600 15px/1 var(--font-button);
  text-decoration: none;
  cursor: pointer;
}

.btn:hover { filter: brightness(1.08); }

.btn--quiet {
  background: transparent;
  border: 1px solid var(--field-line);
  color: var(--ink);
  font-weight: 500;  /* the guidelines' secondary-button weight */
}

.btn--small { padding: 7px 14px; font-size: 13px; }

/* A field the form states rather than asks. It borrows the field's shape and
 * rhythm but not its affordance: a dashed edge and no fill, so it reads as a
 * line of information standing where a control would be. Deliberately not a
 * disabled input, which reads as broken rather than as settled. */
.locked {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 12px;
  border: 1px dashed var(--line);
  border-radius: var(--r-field);
  color: var(--ink);
  font: 400 15px/1.4 var(--font);
}

.locked svg { flex: 0 0 auto; color: var(--ink-dim); }

/* The confirmation, last in the form panel. A bordered strip rather than a
 * bare checkbox: it gates the submit, so it has to read as a step of its own
 * and not as a ninth field. --bad borrows the refusal colour the fields use,
 * so a missed tick looks like every other refusal on the page. */
.approve {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 12px;
  align-items: start;
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-radius: var(--r-field);
  background: var(--surface);
}

.approve--bad { border-color: var(--danger); }

.approve input {
  margin: 2px 0 0;
  width: 17px;
  height: 17px;
  accent-color: var(--brand-action);
}

.approve label { font-size: 14px; line-height: 1.5; color: var(--ink); }

/* The two record-keeping strips on the proposal page — the figures it was
 * priced from, and the supporting documents. Each is a <details>, closed
 * by default: both are reference rather than reading, and open they put
 * the document itself below the fold (operator, 15 Sep). The summary is
 * the heading with the state at a glance on the right; the figures inside
 * are one line per section with the values inline, the "Option B" the
 * operator chose over the form-shaped grouping. */
.recline {
  border: 1px solid var(--line);
  border-radius: var(--r-field);
  padding: 0 18px;
  margin: 0 0 22px;
  background: var(--surface);
}

.recline > summary {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 0;
  cursor: pointer;
  list-style: none;
  color: var(--ink);
}

.recline > summary::-webkit-details-marker { display: none; }

.recline__chev {
  width: 0;
  height: 0;
  border: 5px solid transparent;
  border-left: 7px solid var(--ink-dim);
  border-right: 0;
  transition: transform .15s;
}

.recline[open] .recline__chev { transform: rotate(90deg); }

.recline__ttl { font-size: 15px; font-weight: 500; }

.recline__ok { margin-left: auto; font-size: 12.5px; color: var(--ok); white-space: nowrap; }
.recline__dim { margin-left: auto; font-size: 12.5px; color: var(--ink-dim); white-space: nowrap; }

.recline__body { border-top: 1px solid var(--line); padding: 12px 0 14px; }

.recline__body > p:first-child { margin-top: 0; }

.recline dl { margin: 0; }

.recline dl div {
  display: grid;
  grid-template-columns: 120px 1fr;
  gap: 18px;
  padding: 10px 0;
}

.recline dl div + div { border-top: 1px solid var(--line); }

.recline dt { font-size: 12px; color: var(--ink-dim); padding-top: 2px; }

.recline dd { margin: 0; font-size: 15px; color: var(--ink); }

.recline dd span { color: var(--ink-dim); padding: 0 8px; }

/* The approval under the figures. Not a .notice: those are the page's
 * status lines and are counted as such. */
.rec__ok {
  display: flex;
  gap: 8px;
  align-items: baseline;
  margin: 12px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink-dim);
}

.rec__ok b { color: var(--ok); font-weight: 500; white-space: nowrap; }

/* The how-to video link above the form. Carries no colour of its own — the
 * quiet button already decides that. This rule exists for two things .btn
 * cannot do: lay a glyph beside the label (.btn is inline-block, so the
 * icons need a flex line), and hold the gap between the link and the panel
 * beneath it. The trailing glyph is the leaves-the-site arrow, muted so it
 * reads as a hint rather than a second icon. */
.howto {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 22px;
}

.howto svg { flex: 0 0 auto; }
.howto svg:last-child { opacity: .55; }

.field {
  width: 100%;
  padding: 11px 12px;
  border: 1px solid var(--field-line);
  border-radius: var(--r-field);
  background: var(--field-bg);
  color: var(--ink);
  font: 400 15px/1.4 var(--font);
}

.field::placeholder { color: var(--ink-dim); }

.field:focus { border-color: var(--focus); }

/* ---- form layout ---------------------------------------------------------
 * Four findings drive this.
 *
 * Labels sit above their field. Eye-tracking puts label and field in one
 * fixation, which is up to twice as fast as reading left-aligned labels
 * across (Nielsen Norman Group).
 *
 * Nine fields is past the six-field threshold where grouping starts to pay,
 * so they sit in four labelled sections — related questions chunked rather
 * than one flat run.
 *
 * The sections live inside ONE panel whose base is the submit. A form is a
 * single object with a beginning and an end; four separate cards with a
 * detached button below them is four objects and a stray control.
 *
 * Fields are sized by their section's COLUMN COUNT rather than each carrying
 * a width. Sizing to the answer is still the goal — a 720px box for "200"
 * tells the user their answer should be long — but a grid keeps a two-field
 * section and a three-field section on one rhythm, which hand-set widths
 * never did. The .field--* widths below survive for the admin forms, which
 * are not on a grid.
 */

.fpanel {
  background: var(--surface-card);
  border: 1px solid var(--glass-edge);
  border-radius: var(--r-card);
  overflow: hidden;              /* so the bar meets the rounded corner */
}

.fpanel__body {
  padding: 26px 30px 30px;
  display: grid;
  gap: 28px;
  counter-reset: fsec;
}

/* A section: a numbered head, then a grid. Kept as fieldset/legend for the
   semantics; the legend is floated so it sits inside the box rather than in
   the border gap, which reads as an unfinished edge. */
.fsec {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;                  /* fieldsets default to min-content */
  counter-increment: fsec;
}

.fsec > legend {
  float: left;
  width: 100%;
  padding: 0;
  margin-bottom: 16px;
  display: flex;
  align-items: center;
  gap: 12px;
  font: 500 15px/1 var(--font);
  color: var(--ink);
}

/* Clearing the legend's float. Without this the next block shrinks to sit
   beside it — which threw a whole row of fields outside its card. */
.fsec > *:not(legend) { clear: both; }

/* The step number, from a counter — so the markup carries no digits and a
   section can be reordered or dropped server-side without renumbering. */
.fsec > legend::before {
  content: counter(fsec);
  flex: 0 0 auto;                /* or it deforms when the title is long */
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 14px;
  background: var(--brand-action);
  color: var(--on-action);
  font: 500 13px/1 var(--font);
}

/* The rule that runs out to the panel edge. */
.fsec > legend::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: var(--line);
}

.fsec--plain { counter-increment: none; }
.fsec--plain > legend::before { display: none; }

/* A section that is not a step. Delivery is where the proposal goes, not an
   input to it, so it carries no number and the front page's "three simple
   steps" counts what it says it counts.
 *
 * THIS BLOCK MUST STAY BELOW `.fsec > legend::before`. Both selectors are one
 * class, one type and one pseudo-element - identical specificity - so source
 * order is the only thing deciding which wins. Moved above it, the badge
 * renders again and nothing fails: no test here evaluates a cascade.
 */

.fgrid {
  display: grid;
  grid-template-columns: repeat(var(--cols, 3), minmax(0, 1fr));
  gap: 16px;
}

.fgrid--2 { --cols: 2; }
.fgrid--1 { --cols: 1; }

.fld { margin: 0; min-width: 0; }

.fld > label {
  display: block;
  margin-bottom: 6px;
  font-size: 13.5px;
  color: var(--ink-dim);
}

.fld .hint {
  display: block;
  margin: 6px 0 0;
  font-size: 12.5px;
  color: var(--ink-dim);
}

.fld .opt {
  margin-left: 6px;
  font-size: 12px;
  color: var(--ink-dim);
}

/* Widths matched to the answer, in `ch` so they track the font. The proposal
   form sizes by grid column now; these are the admin provision row and the
   logins filter, which are inline forms and would each claim 100% of their
   flex container without them. */
.field--xs { width: 9ch; }
.field--sm { width: 14ch; }
.field--md { width: 22ch; }
.field--lg { width: 44ch; }   /* the reports page's asset picker: name, kWp, battery, last data */
.field--full { width: 100%; }

/* Currency and unit affixes, so "R" and "kWp" stop living in the label. */
.affix {
  display: flex;
  width: 100%;
  align-items: stretch;
  border: 1px solid var(--field-line);
  border-radius: var(--r-field);
  background: var(--field-bg);
  overflow: hidden;
}

/* The focus ring belongs to the whole affix. The inner input's own outline is
   suppressed — it would be drawn inside a clipped box — so without this the
   only focus signal on six of nine fields was --field-line shifting to
   --focus, which is 1.61:1 on light and fails WCAG 2.4.11. */
.affix:focus-within {
  border-color: var(--focus);
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.affix .field {
  min-width: 0;                  /* a long value shrinks the box, not the unit */
  border: 0;
  border-radius: 0;
  background: transparent;
  text-align: right;             /* numerals align on their right edge */
  font-variant-numeric: tabular-nums;
}

.affix .field:focus { outline: none; }

.affix__pre,
.affix__post {
  display: flex;
  align-items: center;
  padding: 0 11px;
  background: var(--glass-fill);
  color: var(--ink-dim);
  font-size: 14px;
  white-space: nowrap;
}

.affix__pre { border-right: 1px solid var(--line); }
.affix__post { border-left: 1px solid var(--line); }

/* ---- the panel's base ----------------------------------------------------
 * The submit belongs to the panel, not to the page, so the last thing inside
 * the object you have been filling in is the control that sends it.
 */
.fbar {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  padding: 18px 30px;
  background: var(--surface);
  border-top: 1px solid var(--line);
}

.fbar .btn { white-space: nowrap; flex: 0 0 auto; }
.fbar__note { font-size: 13px; color: var(--ink-dim); }

@media (max-width: 640px) {
  .fpanel__body { padding: 22px 18px 24px; gap: 24px; }
  .fgrid { grid-template-columns: minmax(0, 1fr); }
  .fbar { padding: 16px 18px; }
  .fbar .btn { width: 100%; text-align: center; }
}

/* ---- refusals ------------------------------------------------------------ */

.field--bad,
.affix--bad { border-color: var(--danger); }

/* The band, marked by portal.js before submission. `--over` shares --danger
   with a server refusal because it IS one: that submission will be refused.
   `--under` is advisory and must NOT read as a refusal — nothing on the
   server rejects a small project, so it takes --warn and stops at the
   border. A message would over-claim; the heading already says the band. */
.field--over,
.affix--over { border-color: var(--danger); }

.field--under,
.affix--under { border-color: var(--warn); }

.err {
  display: block;
  margin-top: 6px;
  font-size: 12.5px;
  color: var(--danger);
}

/* ---- the submit row ------------------------------------------------------ */

/* ---- the on-page PDF preview --------------------------------------------
 * The browser's own viewer in an iframe — no vendored renderer, because the
 * route already serves the bytes and every desktop browser ships a viewer
 * with zoom, print and page navigation that people already know.
 *
 * Touch devices are the exception, and it is a real one: iOS Safari has a
 * long history of rendering only page 1 of an iframed PDF, or a blank frame,
 * and Android Chrome frequently declines and offers a download instead.
 * Neither reports failure, so there is nothing to detect after the fact —
 * which is why the fallback is chosen UP FRONT by `pointer: coarse` rather
 * than by width (an iPad is wide and still unreliable), and why the fallback
 * link is ALWAYS in the markup: CSS decides which one is shown, so a device
 * that cannot render inline is never left with a grey box and no way out.
 */
/* The preview steps just outside the shell's reading column — 15%, centred,
   not the full viewport, which swamped the page it sits on.
   Chrome's viewer puts a fixed ~290px thumbnail rail beside the page and
   there is no way to narrow it: `#pagemode=none` and `#navpanes=0` are both
   ignored, verified against the live viewer. So the room the page gets comes
   from this modest widening plus `#zoom=page-width`, which replaces the 50%
   Chrome picks for itself in a column-width frame — that zoom was most of
   why the page read small, and it costs no layout at all. */
.pdfview {
  margin-top: 8px;
  width: min(96vw, 115%);
  margin-left: 50%;
  transform: translateX(-50%);
}

.pdfview__frame {
  display: block;
  width: 100%;
  /* Height by viewport, not aspect ratio: with the rail taking horizontal
     space the frame's proportions no longer track the page's. */
  height: min(80vh, 920px);
  border: 1px solid var(--line);
  border-radius: var(--r-tile);
  background: var(--surface-card);
}

.pdfview__fallback { display: none; }

@media (pointer: coarse) {
  .pdfview__frame { display: none; }
  .pdfview__fallback {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
    margin: 0;
    padding: 18px;
    border: 1px solid var(--line);
    border-radius: var(--r-tile);
    background: var(--surface-card);
    color: var(--ink-dim);
    font-size: 14px;
  }
}

.actions {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  margin-top: 22px;
}

/* Rendering the PDF and handing it to the mail transport happens before the
   redirect, so the wait is real. The button says so and stops accepting a
   second click, which used to produce two quotes and two emails. */
.btn[aria-busy="true"] {
  opacity: 0.75;
  cursor: progress;
}

/* ---- the result page -----------------------------------------------------
 * The nine figures used to carry identical weight, so the number an EPC
 * actually quotes was row three of nine in a hairline grid.
 */

/* ---- admin tables --------------------------------------------------------
 * The scroll container is the fix for a measured defect. The users table
 * demands 673px for a single short row against 358px available at 92vw on a
 * 390px phone, and its container had overflow-x: visible — so it dragged the
 * whole page sideways. tests/test_admin_layout asserts BOTH that every table
 * carries the wrapper and that this rule exists, because either half alone
 * passes with the defect intact.
 */

.tscroll {
  overflow-x: auto;
  margin-bottom: 18px;
  border: 1px solid var(--line);
  border-radius: var(--r-tile);
}

/* A table in a panel: the panel is the edge, so the scroll container gives
   up its own border and radius — two nested edges 1px apart read as a
   rendering fault. The panel's `overflow: hidden` clips the table's corners
   to the panel radius while .tscroll keeps scrolling inside it. */
.fpanel--table { margin-bottom: 18px; }

.fpanel--table > .tscroll {
  border: 0;
  border-radius: 0;
  margin-bottom: 0;
}

/* Row rhythm matched to the form's fields, so a table and a panel of inputs
   are recognisably the same system up close and not only at a glance. */
.fpanel--table .atable th,
.fpanel--table .atable td { padding: 13px 16px; }

.atable {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

.atable th {
  padding: 11px 12px;
  text-align: left;
  font-weight: 500;
  font-size: 12px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-dim);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

.atable td {
  padding: 11px 12px;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}

.atable tr:last-child td { border-bottom: 0; }

/* ---- destructive actions -------------------------------------------------
 * Deactivating kills live sessions and a reset burns the password
 * immediately. Both were one click.
 */

.confirm { display: inline-block; margin: 0 6px 4px 0; }

.confirm > summary {
  display: inline-block;
  padding: 4px 11px;
  border: 1px solid var(--field-line);
  border-radius: var(--r-pill);
  color: var(--ink-dim);
  font-size: 12px;
  cursor: pointer;
  list-style: none;
  white-space: nowrap;
}

.confirm > summary::-webkit-details-marker { display: none; }
.confirm > summary:hover { color: var(--ink); }
.confirm[open] > summary { color: var(--danger); border-color: var(--danger); }
.confirm > form { margin-top: 6px; }

.btn--danger {
  background: transparent;
  border: 1px solid var(--danger);
  color: var(--danger);
}

/* ---- provisioning + the one-time password -------------------------------- */

.provision {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  align-items: center;
  margin-bottom: 8px;
}

.pw {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 0 0 18px;
}

.pw code {
  padding: 9px 14px;
  border: 1px solid var(--line);
  border-radius: var(--r-field);
  background: var(--surface-card);
  font-size: 16px;
  color: var(--ink);
}

/* ---- the proposal history ------------------------------------------------ */

.hist { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }

.hist li {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  padding: 14px 18px;
  background: var(--surface-card);
  border: 1px solid var(--glass-edge);
  border-radius: var(--r-tile);
}

.hist a { font-weight: 500; text-decoration: none; }
.hist a:hover { text-decoration: underline; }

.tag {
  padding: 2px 9px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  font-size: 11px;
  color: var(--ink-dim);
  white-space: nowrap;
}

.tag--demo { color: var(--warn); border-color: var(--warn); }
.tag--sent { color: var(--ok); border-color: var(--ok); }
.tag--failed { color: var(--danger); border-color: var(--danger); }

.spacer { flex: 1 1 auto; }

/* ---- the documents behind a proposal ------------------------------------- */
/*
 * The card is the UPLOAD CONTROL and nothing else. Whether a proposal has
 * its documents is answered at the top of the page, beside the delivery
 * tick, where the same question about the email is already answered.
 *
 * Two columns, and the filename on its own line UNDER the control rather
 * than beside it. Beside it, the name wrapped into a second column and every
 * attached card grew taller than an empty one, so a row of them stepped.
 * Both states render that line, so the cards match whatever they hold.
 */

.updocs { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }

.updoc {
  display: grid;
  grid-template-columns: minmax(9rem, 12rem) 1fr;
  grid-template-areas: "kind form" "spacer file";
  align-items: center;
  gap: 6px 18px;
  padding: 14px 18px;
  background: var(--surface-card);
  border: 1px solid var(--glass-edge);
  border-radius: var(--r-tile);
}

.updoc__kind { grid-area: kind; font-weight: 500; }

.updoc__form {
  grid-area: form;
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* Its own grid cell under the control rather than a child of the form: it
   carries the remove form, and a form inside a form is dropped by the
   browser — the × would post the upload instead. */
.updoc__file {
  grid-area: file;
  display: flex;
  align-items: baseline;
  gap: 6px;
  color: var(--ink-dim);
  font-size: 12px;
  overflow-wrap: anywhere;
}

.updoc__file a { color: var(--ink); text-decoration: none; font-weight: 500; }
.updoc__file a:hover { text-decoration: underline; }

/* ---- removing a document ------------------------------------------------- */

.updoc__status {
  display: flex;
  align-items: center;
  gap: 10px;
}

.updoc__rm { display: flex; }

.updoc__x {
  appearance: none;
  border: 0;
  background: none;
  padding: 0 4px;
  color: var(--ink-dim);
  font: 400 16px/1 var(--font);
  cursor: pointer;
}

.updoc__x:hover { color: var(--danger); }

/* One column below the phone breakpoint: two columns at 390px leaves the
   file input about 150px, which is a control nobody can use. */
@media (max-width: 640px) {
  .updoc { grid-template-columns: 1fr; }
}

/* ---- type helpers -------------------------------------------------------- */

.t-dim { color: var(--ink-dim); }
.t-small { font-size: 13px; }
.t-tiny { font-size: 12px; }

/* ---- the top bar --------------------------------------------------------
 * Jaltech float a rounded bar over the page rather than sitting a header
 * against the top edge — the 35px pill radius is kept. The fill is the fixed
 * deep navy (--topbar) in BOTH themes rather than glass: on light the glass
 * resolved to white-on-white chrome, and a bar that never changes colour is
 * also what lets one white lock-up be the logo everywhere.
 */

.topbar {
  width: min(92vw, 1200px);
  margin: 18px auto 0;
  padding: 10px 14px 10px 18px;
  border-radius: var(--r-pill);
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
  background: var(--topbar);
  /* Jaltech's own hairline: their floating nav carries
     `border: 1px solid rgba(255,255,255,0.6)`, read off their stylesheet.
     The bar lost it when it stopped being a .glass panel; it is the edge
     that makes the pill read as a floating object rather than a painted
     band, so it comes back as a token of its own — white in BOTH themes,
     because the bar is dark in both. */
  border: 1px solid var(--topbar-edge);
  position: relative;
  z-index: 40;
}

.brand {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  line-height: 0;
}

.brand img {
  /* The official reverse (white) logo, vector — crisp at any pixel
     density. The guidelines sanction exactly this usage: white art on
     Jaltech Navy, which is why the bar is a dark brand surface in BOTH
     themes. Displayed well above the 120px minimum lock-up width. */
  height: 58px;
  width: auto;
  display: block;
}

.nav {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: 1 1 auto;
  flex-wrap: wrap;
}

.nav a {
  padding: 7px 13px;
  border-radius: var(--r-pill);
  color: var(--topbar-ink-dim);
  text-decoration: none;
  font-size: 14px;
}

.nav a:hover { color: var(--topbar-ink); background: var(--topbar-hover); }

/* The current page, marked for everyone rather than for the mouse: there was
   previously no way to tell which admin page you were on. The pill is the
   light slate carrying navy — the bar's own action pair, not the theme's,
   because the bar does not change with the theme. */
.nav a[aria-current="page"] {
  background: var(--topbar-action);
  color: var(--on-topbar-action);
}

.bar-end {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 0 0 auto;
}

/* ---- the menu ------------------------------------------------------------
 * One place for everything that is about the SESSION rather than the page:
 * who is signed in, the theme switch, sign out, and which environment this
 * is. They used to sit loose on the bar as four competing pills; a <details>
 * keeps the disclosure server-rendered — no route, no required JS, and the
 * boundary walk has nothing new to account for. portal.js only adds
 * close-on-outside-click on top.
 */

.menu { position: relative; }

.menu summary {
  list-style: none;
  cursor: pointer;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: var(--r-tile);
}

.menu summary::-webkit-details-marker { display: none; }

.menu summary:hover,
.menu[open] summary { background: var(--topbar-hover); }

/* The burger: three bars drawn in CSS, so there is no icon asset to case-
   mismatch on the box and nothing for the asset sweep to chase. */
.menu__bars,
.menu__bars::before,
.menu__bars::after {
  display: block;
  width: 18px;
  height: 2px;
  border-radius: 1px;
  background: var(--topbar-ink);
}

.menu__bars { position: relative; }
.menu__bars::before { content: ""; position: absolute; top: -6px; }
.menu__bars::after { content: ""; position: absolute; top: 6px; }

/* An ordinary dropdown, deliberately: a solid raised panel with row items and
   a shadow for elevation. The first version dressed it as frosted glass with
   pill buttons, and it read as loose floating chips rather than a menu. */
.menu__panel {
  position: absolute;
  right: 0;
  top: calc(100% + 10px);
  min-width: 252px;
  padding: 6px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  z-index: 60;
  text-align: left;
  background: var(--surface-card);
  /* The bar's own hairline, continued onto the panel it opens: the two are
     one object as far as the eye is concerned, and a --line border made the
     panel read as a separate card that happened to be nearby. */
  border: 1px solid var(--topbar-edge);
  border-radius: var(--r-tile);
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.18);
}

.menu__panel form { display: block; margin: 0; }

.menu__head,
.menu__foot {
  padding: 9px 12px;
  font-size: 12.5px;
  color: var(--ink-dim);
}

.menu__head { border-bottom: 1px solid var(--line); margin-bottom: 4px; }
.menu__foot { border-top: 1px solid var(--line); margin-top: 4px; }

/* One row, whatever element it is — a submit button in a form (theme, sign
   out) or a plain button (the demo fill). Button chrome reset so the rows
   match. */
.menu__item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 10px 12px;
  border: 0;
  border-radius: 9px;
  background: none;
  color: var(--ink);
  font: 400 14px/1.3 var(--font);
  text-align: left;
  cursor: pointer;
}

.menu__item:hover { background: var(--menu-hover); }

.menu__item svg { width: 16px; height: 16px; flex: 0 0 auto; }

/* The used state the demo fill sets — the classname is portal.js's. */
.menu__item.beaker__btn--used { color: var(--warn); }

.who {
  font-size: 13px;
  color: var(--ink-dim);
}

.who b { color: var(--ink); font-weight: 500; }

.inline-form { display: inline; margin: 0; }

/* ---- the admin sub-navigation ------------------------------------------- */

.subnav {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  margin: 0 0 22px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--line);
}
/* the way to the SunEx portal: last, set apart from Jaltech's own pages */
.subnav .subnav__sunex { margin-left: 24px; font-weight: 600; }

/* ---- footer -------------------------------------------------------------- */

.footer {
  margin-top: 56px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
  color: var(--ink-dim);
  font-size: 12px;
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  justify-content: space-between;
}

/* ---- notices ------------------------------------------------------------- */

.notice {
  margin: 0 0 16px;
  padding: 11px 14px;
  border-radius: var(--r-tile);
  border: 1px solid var(--line);
  background: var(--surface-card);
  font-size: 13px;
}

.notice--ok   { color: var(--ok); }
.notice--warn { color: var(--warn); }
.notice--info { color: var(--info); }
.notice--danger { color: var(--danger); }

/* ---- the sign-in card ---------------------------------------------------- */

.lede { margin: 18px 0 22px; }
.signin { display: grid; gap: 12px; }
.signin .btn { width: 100%; text-align: center; }

/* ---- the not-found page -------------------------------------------------- */

.errpage { padding: 40px 0 8px; }
.errpage h2 { font-size: 46px; margin: 0 0 6px; }

/* ---- narrow viewports ---------------------------------------------------- */

@media (max-width: 640px) {
  .topbar {
    border-radius: var(--r-card);
    gap: 10px 12px;
    padding: 10px 12px 10px 14px;
  }

  /* 28px turned the lockup into a stamp — "Fund Management" stopped being
     legible at all. The artwork is 532×286, so this is still a downsample. */
  .brand img { height: 36px; }

  /* Brand left, menu right, on ONE row; the nav drops beneath it.
     `.bar-end { width: 100% }` used to put the menu on its own full-width
     row, which was tolerable signed in — the nav filled row one — and wrong
     signed out, where the nav is EMPTY: it still claimed `flex: 1 1 auto`,
     took 261px of a 359px bar, and stranded the hamburger under the logo
     with the bar's whole right side blank and the bar 102px tall.

     `order` is what makes this work: source order is brand, nav, bar-end, so
     giving the nav `flex-basis: 100%` alone would wrap the MENU to a third
     row. Reordering puts the menu on row one and the nav on row two. */
  .brand { order: 1; }
  .bar-end { order: 2; margin-left: auto; }
  .nav { order: 3; flex-basis: 100%; justify-content: center; }

  /* Signed out there are no links at all, and an empty flex item is still a
     flex item. Nothing should be reserved for a nav that is not there. */
  .nav:empty { display: none; }

  .footer { flex-direction: column; gap: 6px; }
}

/* The short thick bar Jaltech set under section headings — and a section
   heading is now the only thing that wears it. It sits on an <h3>, below a
   `.page-title`, so a page with several parts reads as a hierarchy rather
   than as several pages stacked. Scaled down with the heading it marks. */
.rule::after {
  content: "";
  display: block;
  width: 56px;
  height: 3px;
  margin-top: 10px;
  background: var(--accent);
  border-radius: 2px;
}

h3.rule { margin-top: 34px; margin-bottom: 18px; font-size: 17px; }

/* ---- the Reports page: one console, four tabs ---------------------------
   plans/reports-page-ux-20260917, mockup A as signed off: a strip of small
   tiles, one build card (tick-chips, one row of fields, one footnote), the
   progress strip, tabs on the sub-nav row. Built from the tokens above and
   the components already here; nothing below introduces a colour. */

.page-head { display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; margin-bottom: 14px; }
.page-head .page-title { margin: 0; }
.page-sub { color: var(--ink-dim); font-size: 13px; }
.t-small { font-size: 12px; }
.t-warn { color: var(--warn); }
.eyebrow { display: block; font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-dim); margin-bottom: 4px; }

/* the tiles: one line of figure, one of detail */
.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 10px; margin: 0 0 14px; }
.tile { display: flex; flex-direction: column; gap: 2px; padding: 10px 14px; border-radius: 10px; background: var(--surface-card); border: 1px solid var(--line); color: var(--ink); text-decoration: none; min-width: 0; }
a.tile:hover { border-color: var(--focus); }
.tile__label { font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-dim); }
.tile__value { font-size: 18px; font-weight: 600; line-height: 1.15; }
.tile__sub { font-size: 11px; color: var(--ink-dim); display: flex; flex-wrap: wrap; gap: 2px 5px; align-items: center; }
.tile--fleet { border-color: var(--danger); }
.tile--fleet .tile__value { color: var(--danger); }
.tile--warn .tile__value, .tile--admin .tile__value { color: var(--warn); font-size: 16px; }
.tile .chip { padding: 0 7px; font-size: 10.5px; }

/* status chips */
.chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 6px; }
.chip { display: inline-flex; align-items: center; gap: 6px; padding: 2px 10px; border-radius: var(--r-pill); border: 1px solid var(--line); font-size: 12px; color: var(--ink-dim); white-space: nowrap; }
.chip b { font-weight: 600; color: var(--ink); }
.chip--big { font-size: 14px; padding: 6px 14px; }
.chip--big b { font-size: 18px; }
.chip--danger, .chip--danger b { border-color: var(--danger); color: var(--danger); }
.chip--warn, .chip--warn b { border-color: var(--warn); color: var(--warn); }
.chip--ok, .chip--ok b { border-color: var(--ok); color: var(--ok); }
.chip--info { border-color: var(--info); }

/* the build card */
.build .fpanel__body { gap: 12px; padding: 16px 22px 16px; }
.build__head { display: flex; justify-content: space-between; align-items: center; gap: 12px 20px; flex-wrap: wrap; }
.build__head h3 { margin: 0; font-size: 17px; }
.seg { display: inline-flex; flex-wrap: wrap; gap: 4px; padding: 3px; border-radius: var(--r-pill); background: var(--field-bg); border: 1px solid var(--line); }
.seg__opt input { position: absolute; opacity: 0; width: 0; height: 0; }
.seg__opt span { display: inline-block; padding: 6px 13px; border-radius: var(--r-pill); font-family: var(--font-button); font-size: 12.5px; font-weight: 600; color: var(--ink-dim); cursor: pointer; }
.seg__opt span::before { content: ""; display: inline-block; width: 12px; height: 12px; margin-right: 8px; border-radius: 3px; border: 1.5px solid var(--ink-dim); vertical-align: -1px; }
.seg__opt input:checked + span { background: var(--brand-action); color: var(--on-action); }
.seg__opt input:checked + span::before { background: var(--on-action); border-color: var(--on-action); box-shadow: inset 0 0 0 2px var(--brand-action); }
.seg__opt input:focus-visible + span { outline: 2px solid var(--focus); outline-offset: 2px; }
.seg__opt input:disabled + span { opacity: .55; cursor: default; }
.build__grid { display: grid; grid-template-columns: minmax(0, 2.2fr) minmax(0, 1fr) minmax(0, 1fr) auto; gap: 12px 14px; align-items: end; }
.build__grid .fld > label { display: block; font-size: 12px; color: var(--ink-dim); margin-bottom: 4px; }
.build__grid .fld--go .btn { white-space: nowrap; }
.build__grid .err { display: block; margin-top: 4px; font-size: 12px; color: var(--danger); }
.field--static { display: flex; align-items: center; color: var(--ink-dim); }
.build__foot { margin: 0; font-size: 12px; }
.btn:disabled { opacity: .55; cursor: default; }
@media (max-width: 900px) {
  .tiles { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .build__grid { grid-template-columns: 1fr 1fr; }
  .build__grid .fld--plant, .build__grid .fld--fleet, .build__grid .fld--go { grid-column: 1 / -1; }
}
@media (max-width: 520px) { .build__grid { grid-template-columns: 1fr; } .tiles { grid-template-columns: 1fr 1fr; } }

/* the progress strip: one line a job, a bar the server sets and the page moves */
.progress { display: grid; gap: 8px; }
.job { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 3fr) auto; gap: 14px; align-items: center; padding: 10px 14px; border: 1px solid var(--line); border-radius: var(--r-tile); font-size: 13px; }
.job__bar { display: block; width: 100%; height: 6px; border: 0; border-radius: 3px; background: var(--field-bg); overflow: hidden; -webkit-appearance: none; appearance: none; }
.job__bar::-webkit-progress-bar { background: var(--field-bg); border-radius: 3px; }
.job__bar::-webkit-progress-value { background: var(--brand-action); border-radius: 3px; transition: width .6s linear; }
.job__bar::-moz-progress-bar { background: var(--brand-action); border-radius: 3px; }
.job__stage { color: var(--ink-dim); font-size: 12px; margin-top: 4px; }
.job--done { border-color: var(--ok); }
.job--failed { border-color: var(--danger); }
.job--failed .job__stage { color: var(--danger); }
@media (max-width: 700px) { .job { grid-template-columns: 1fr; gap: 6px; } }

/* the tabs ride the sub-nav row; a count beside a tab, Admin at the far end */
.tabs { margin-top: 22px; }
.tabs__n { font-size: 11px; color: var(--ink-dim); margin-left: 4px; }
.tabs .tabs__admin { margin-left: auto; }
.tabs .tabs__live::before { content: ""; display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); margin-right: 7px; vertical-align: 1px; }
.tabpane { min-width: 0; }

/* the archive and the coverage table */
.filters { display: flex; gap: 14px; flex-wrap: wrap; align-items: end; margin-bottom: 14px; }
.filters .fld > label { display: block; font-size: 12px; color: var(--ink-dim); margin-bottom: 4px; }
.tag--none { border-color: transparent; }
a.tag { text-decoration: none; }
.atable .num { text-align: right; font-variant-numeric: tabular-nums; }
.atable--compact th, .atable--compact td { padding: 8px 9px; font-size: 12.5px; }
.acts { white-space: nowrap; }
.cov--ok { color: var(--ok); }
.cov--warn { color: var(--warn); }
.cov--danger { color: var(--danger); }

/* the fleet card */
.fleetcard { margin-bottom: 18px; }
.fleetcard__head { display: flex; justify-content: space-between; gap: 14px; align-items: start; flex-wrap: wrap; }
.fleetcard__head h3 { margin: 0; }
.fleetcard h4 { margin: 18px 0 8px; font-size: 13px; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-dim); }
.look { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.look li { display: grid; grid-template-columns: auto auto 1fr; gap: 10px; align-items: baseline; font-size: 13px; }
@media (max-width: 700px) { .look li { grid-template-columns: 1fr; gap: 2px; } }

/* the admin tab */
.admingrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.admingrid h4 { margin: 0 0 6px; }
.admingrid .fbar { padding: 0; background: none; border-top: 0; margin-top: 10px; }
.card--warn { border-color: var(--warn); }
.recips { list-style: none; margin: 0 0 12px; padding: 0; display: grid; gap: 6px; font-size: 13px; }
.recips li { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.fbar__end { align-self: end; }
@media (max-width: 800px) { .admingrid { grid-template-columns: 1fr; } }

/* ---- SunEx's pages (plans/bruwer-round5-20260925) ----------------------
   The reports console, the pages it leads to, the guide and the live desk
   wear the office boards' look: the console's own tokens pointed at the
   --sx-* palette, square corners, and one SunEx header. Jaltech's pages
   are untouched: everything here hangs off body.sunex-page. */
body.sunex-page {
  --surface: var(--sx-surface);
  --surface-card: var(--sx-panel);
  --line: var(--sx-line);
  --ink: var(--sx-ink);
  --ink-dim: var(--sx-data);
  --accent: var(--sx-data);
  --brand-action: var(--sx-accent);
  --on-action: var(--sx-on-accent);
  --field-bg: var(--sx-surface);
  --field-line: var(--sx-context);
  --focus: var(--sx-accent);
  --glass-fill: var(--sx-panel);
  --glass-edge: var(--sx-line);
  --glass-solid: var(--sx-panel);
  --r-card: 4px;
  --r-tile: 4px;
  --r-field: 4px;
  --r-pill: 4px;
  color-scheme: dark;
}
body.sunex-page .tile { border-radius: var(--r-tile); }
body.sunex-page .nav a[aria-current="page"] { background: var(--brand-action); color: var(--on-action); border-radius: var(--r-pill); }
body.sunex-page .page-title { font-weight: 600; }
.sunex-head { display: flex; align-items: center; gap: 24px; flex-wrap: wrap; padding: 12px var(--shell-pad); background: var(--sx-panel); border-bottom: 2px solid var(--sx-accent); }
.sunex-head__brand img { height: 40px; width: auto; display: block; }
.sunex-head__nav { display: flex; gap: 6px; flex-wrap: wrap; }
.sunex-head__nav a { color: var(--sx-data); text-decoration: none; padding: 6px 14px; border: 1px solid var(--sx-line); border-radius: 4px; }
.sunex-head__nav a:hover { border-color: var(--sx-context); }
.sunex-head__nav a[aria-current="page"] { background: var(--sx-accent); border-color: var(--sx-accent); color: var(--sx-on-accent); font-weight: 600; }
.sunex-head__end { margin-left: auto; display: flex; align-items: center; gap: 14px; color: var(--sx-context); font-size: 13px; }
.sunex-head__end form { margin: 0; }
.sunex-head__out { background: none; border: 1px solid var(--sx-line); border-radius: 4px; color: var(--sx-data); padding: 5px 12px; font: inherit; cursor: pointer; }
.sunex-head__out:hover { border-color: var(--sx-context); }
.jaltech-links { display: flex; align-items: center; gap: 4px 16px; flex-wrap: wrap; padding: 6px var(--shell-pad); background: var(--sx-surface); border-bottom: 1px solid var(--sx-line); font-size: 13px; }
.jaltech-links__home { color: var(--sx-ink); font-weight: 600; }
.jaltech-links a { color: var(--sx-data); }
body.sunex-page .shell { padding-top: 20px; }
@media (max-width: 700px) {
  .sunex-head { gap: 10px 14px; padding: 10px 16px; }
  .sunex-head__brand img { height: 30px; }
  .jaltech-links { padding: 6px 16px; }
}

/* ---- The live pages (plans/live-dashboard-ui-20260918, bruwer-round5) -----
   /live on the office screens and on the desk: the bundle (wall/src, built
   into portal/static/wall) draws both, the boards and the desk, in its own
   namespaced stylesheet; the page is always dark, on SunEx's surface. */
body.live-page { background: var(--sx-surface); color: var(--sx-ink); color-scheme: dark; font-size: 15px; line-height: 1.35; }
/* the office board and the desk: the bundle takes the whole width; a board never scrolls */
body.live-page .shell--board { width: 100%; max-width: none; margin: 0; padding: 0; }
body.board-page { overflow: hidden; margin: 0; }
.live { display: block; }
/* the guide page: one screen at a time, its shot with a spot on each numbered
   badge (portal.js places them and opens their explanation), the key beside it */
.guide__screens { margin-bottom: 10px; }
.guide__turn { display: flex; gap: 8px; margin: 0 0 12px; font-size: 13px; }
.guide__turn a { color: var(--ink-dim); text-decoration: none; padding: 3px 10px; border: 1px solid var(--line); border-radius: var(--r-pill); }
.guide__turn a[aria-current="page"] { color: var(--on-action); background: var(--brand-action); border-color: var(--brand-action); }
.guide__view { display: grid; grid-template-columns: minmax(0, 3fr) minmax(260px, 1fr); gap: 20px; align-items: start; }
.guide__figure { position: relative; margin: 0; }
.guide__frame { position: relative; }   /* the shot and its spots: the spots are shares of this box alone */
.guide__shot { display: block; width: 100%; height: auto; border: 1px solid var(--line); border-radius: var(--r-card); }
.guide__figure--portrait { max-width: 540px; }   /* the figure is the image's box: the spots are shares of it */
.guide__spot { position: absolute; width: 2.2%; min-width: 22px; aspect-ratio: 1; transform: translate(-50%, -50%); margin: 0; padding: 0; border: 0; border-radius: 50%; background: transparent; color: transparent; cursor: pointer; font: inherit; }
.guide__figure--portrait .guide__spot { width: 3.2%; }
.guide__spot:hover, .guide__spot--lit, .guide__spot:focus-visible { box-shadow: 0 0 0 3px var(--brand-action); }
.guide__pop { position: absolute; z-index: 5; box-sizing: border-box; padding: 12px 14px; background: var(--surface-card); border: 1px solid var(--brand-action); border-radius: var(--r-card); color: var(--ink); font-size: 14px; line-height: 1.45; }
.guide__pop p { margin: 6px 0 0; color: var(--ink-dim); }
.guide__key h3 { margin: 0 0 8px; font-weight: 600; }
.guide__keylist { list-style: none; margin: 0 0 14px; padding: 0; }
.guide__keyline { display: flex; align-items: center; gap: 10px; width: 100%; padding: 6px 8px; background: none; border: 1px solid transparent; border-radius: var(--r-field); color: var(--ink); font: inherit; text-align: left; cursor: pointer; }
.guide__keyline:hover, .guide__keyline:focus-visible { border-color: var(--line); }
.guide__num { flex: 0 0 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; background: var(--brand-action); color: var(--on-action); font-weight: 700; font-size: 13px; }
.guide__also-head { margin: 0 0 4px; color: var(--ink-dim); font-size: 13px; }
.guide__colours { list-style: none; margin: 0 0 14px; padding: 0; font-size: 14px; line-height: 1.45; }
.guide__colours li { display: flex; gap: 10px; align-items: flex-start; margin: 6px 0; }
.guide__swatch { flex: 0 0 14px; width: 14px; height: 14px; margin-top: 3px; }
.guide__list { line-height: 1.5; margin: 0 0 14px; padding-left: 18px; font-size: 14px; }
.guide__list li { margin: 6px 0; }
@media (max-width: 1099px) {
  .guide__view { grid-template-columns: minmax(0, 1fr); }
  .guide__pop { position: static; margin-top: 8px; }   /* too small a shot to float over: the open explanation sits under it */
}
