/* 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, measured from three independent sources that agree
 * to within one unit per channel: the computed styles of jaltech.co.za, the
 * backgrounds of their own logo lock-ups, and BRAND_BLUE in the proposal
 * template. 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
 *   #304F62  brand slate    #E6EAEC  pale blue-grey
 *   #506F7F  mid slate      #F4F4F4  off-white
 *   #809FAD  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 #304F62 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 #304F62 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-exempt --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");
}


/* ---- 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: #304F62;   /* brand slate — their section bands */
  --brand-action: #809FAD;   /* light slate; carries deep navy, not white */
  --on-action: #142B38;
  --accent: #BFCED5;
  --ink: #FFFFFF;
  --ink-dim: #BFCED5;
  --line: #506F7F;           /* mid slate */
  --field-line: #809FAD;
  --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;

  /* 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: #304F62;
  --topbar-ink: #FFFFFF;
  --topbar-ink-dim: #BFCED5;
  --topbar-action: #809FAD;
  --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: #304F62;

  /* 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;

  --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: #304F62;   /* their pill, exactly as they ship it */
  --on-action: #FFFFFF;
  --accent: #304F62;
  --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: #304F62;
  --line: #BFCED5;
  --field-line: #506F7F;
  --focus: #304F62;
  --ok: #0F5C3C;
  --warn: #6E470C;           /* 4.5:1 on the card; #7A4E0C was 4.45 */
  --danger: #8F2E12;
  --info: #304F62;

  --topbar: #142B38;
  --topbar-ink: #FFFFFF;
  --topbar-ink-dim: #BFCED5;
  --topbar-action: #809FAD;
  --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; }

.page-title { letter-spacing: -0.015em; }
.page-sub { margin: 6px 0 26px; }

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; }
.shell--wide { --shell-w: 1040px; }

/* ---- 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);
  font: 500 15px/1 var(--font);
  text-decoration: none;
  cursor: pointer;
}

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

.btn--quiet {
  background: transparent;
  border: 1px solid var(--field-line);
  color: var(--ink);
}

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

.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 ---------------------------------------------------------
 * Three findings drive this, and the old form broke all three.
 *
 * 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).
 *
 * Fields are sized to the answer, not to the container. A 720px-wide box for
 * "200" tells the user their answer should be long, and is most of why the
 * old form looked wrong (GOV.UK Design System).
 *
 * 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.
 */

.formsec {
  border: 0;
  margin: 0 0 14px;
  padding: 22px 24px 8px;
  background: var(--surface-card);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
}

/* Floated rather than left in the border gap a <legend> cuts by default: the
   gap reads as an unfinished box, and floating makes it a heading that sits
   inside the panel while keeping the semantics of a legend. */
.formsec > legend {
  float: left;
  width: 100%;
  padding: 0;
  margin-bottom: 2px;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

/* 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. */
.formsec > *:not(legend) { clear: both; }

/* A row lets short related answers sit side by side without breaking the
   single-column reading order — each still reads label-then-field. */
.frow {
  display: flex;
  flex-wrap: wrap;
  gap: 18px 22px;
  margin-top: 16px;
}

.fld { margin: 0 0 18px; }
.frow .fld { margin-bottom: 0; }

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

.fld .hint {
  display: block;
  margin: 4px 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. `ch` so they track the font rather than a
   guess in pixels. */
.field--xs { width: 9ch; }
.field--sm { width: 14ch; }
.field--md { width: 22ch; }
.field--full { width: 100%; }

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

:root[data-theme="light"] .affix { background: var(--surface-card); }

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

.affix .field {
  border: 0;
  border-radius: 0;
  background: transparent;
}

.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); }

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

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

.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.
 */

.figure {
  display: grid;
  gap: 4px;
  margin: 4px 0 20px;
  padding: 24px 26px;
  background: var(--surface-card);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
}

.figure__cap {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.figure__value {
  font-size: 44px;
  line-height: 1.1;
  font-weight: 400;
}

.rtable {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
  margin-bottom: 14px;
}

.rtable td {
  padding: 11px 4px;
  border-bottom: 1px solid var(--line);
}

.rtable tr:last-child td { border-bottom: 0; }
.rtable td:first-child { color: var(--ink-dim); }

.rtable .num {
  text-align: right;
  font-weight: 500;
  white-space: nowrap;
  color: var(--ink);
}

/* ---- 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);
}

.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(--line);
  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; }

/* ---- 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 artwork is a 532×286 crop of Jaltech's own banner — ~5× this display
     size, so the browser downsamples cleanly instead of upscaling mush. The
     white-on-transparent art needs the bar beneath it, which is why the bar
     is a dark brand surface in BOTH themes. */
  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);
}

/* ---- 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: 12px; }
  .brand img { height: 28px; }
  .bar-end { width: 100%; justify-content: space-between; }
  .footer { flex-direction: column; gap: 6px; }
}

/* The short thick bar Jaltech set under section headings. */
.rule::after {
  content: "";
  display: block;
  width: 88px;
  height: 4px;
  margin-top: 12px;
  background: var(--accent);
  border-radius: 2px;
}
