/* ============================================================
   Front surface — sidebar + content.
   Separate from app.css (the engine reads light/teal/Inter).
   ============================================================ */

:root {
  /* One size for every action button. Measured before this existed, the front
     carried six button heights: 20.8 (.cal-run-* icons), 25 (.row-btn/.mtab),
     27 (.modal-x/.dd-btn), 29 (.side-foot button), 37 (.btn-quiet/
     .auth-submit) and 40 (.fab). The block at the end of this file applies
     these three to all of them but the chip icons and .mtab. */
  --btn-h:     32px;
  --btn-px:    14px;
  --btn-font:  13px;

  /* ---- palette, from the brand guide (kit:12-15) ----
     void #0a0507 . coal #120a0d . ash #1c1013 . line #271619
     blood #e10e24 . ember #ff2e42 . scar #7a0a15
     bone #ede6e4 . smoke #8a7a7d . dim #4a3d41 . amber #ffb02e . mint #3ad46a
     The kit's own names are added as aliases at the end of this block; the
     app's names keep their meaning so every existing rule still resolves. */
  --bg:        #060305;
  --side-bg:   #120a0d;

  /* Page gradient. --bg stays the flat mid-tone the linear layer passes
     through, so anything still reading var(--bg) matches the middle of the
     ramp instead of clashing with it. */
  --bg-red:    #100407;
  --bg-black:  #020101;

  /* Glass surfaces. backdrop-filter samples whatever is painted behind the
     element, so every surface using these must stay translucent - give one an
     opaque background and the blur becomes a no-op with no error anywhere. */
  /* Glass surfaces. backdrop-filter samples whatever is painted behind the
     element, so every surface using these must stay translucent - give one an
     opaque background and the blur becomes a no-op with no error anywhere.
     Retinted off the guide's coal/ash rather than neutral grey. */
  --glass:         rgba(28, 16, 19, .58);
  --glass-deep:    rgba(18, 10, 13, .66);
  --glass-strong:  rgba(18, 10, 13, .84);
  --glass-line:    #271619;
  --glass-blur:    blur(14px) saturate(140%);
  --glass-blur-sm: blur(8px) saturate(130%);
  /* For the one surface that overlays moving content rather than the page
     gradient. Measured at --glass-strong (.82) with the 14px blur: the terms
     list scrolling underneath stayed legible through the bar, so a row of
     search terms read across "Back" and the step label. .94 still left a
     readable ghost, hence .975 - the blur and the gradient tint still show,
     but nothing behind it competes with the two controls. */
  --glass-veil:    rgba(10, 5, 7, .975);
  --glass-blur-lg: blur(22px) saturate(140%);

  /* ---- palette, from the brand guide (kit:12-15) ----
     void #0a0507 . coal #120a0d . ash #1c1013 . line #271619
     blood #e10e24 . ember #ff2e42 . scar #7a0a15
     bone #ede6e4 . smoke #8a7a7d . dim #4a3d41 . amber #ffb02e . mint #3ad46a
     The app's token NAMES are kept and only their values change, so all 335
     rules in this file keep resolving and no selector has to move. */
  --modal-bg:  #0f090b;
  --panel:     #120a0d;
  --panel-2:   #1c1013;
  --line:      #271619;
  --line-soft: #1d1013;

  --text:      #ffffff;
  /* Not a straight lift of the guide's smoke/dim onto these two names. Both
     carry information here - --mute paints .who, the second line of every list
     row - and measured against --panel #120a0d the guide's dim #4a3d41 gives
     1.89:1, under WCAG AA's 4.5:1 for body text and under 3:1 for large.
     These two clear it: 6.44:1 and 4.80:1. The guide's own #4a3d41 is kept
     below as --faint, for the decorative micro-labels it uses it for. */
  --dim:       #a2908f;
  --mute:      #8a7a7d;
  --faint:     #9a8d90;

  --accent:    #e10e24;
  --accent-lit:#ff2e42;
  --accent-bg: rgba(225, 14, 36, .11);

  --ok:        #3ad46a;
  --ok-bg:     rgba(58, 212, 106, .12);
  --warn:      #ffb02e;
  --warn-bg:   rgba(255, 176, 46, .12);
  --bad:       #ff2e42;
  --bad-bg:    rgba(255, 46, 66, .12);

  /* The guide's own names, for rules written against it directly. */
  --void: #0a0507;  --coal: #120a0d;  --ash: #1c1013;
  --blood: #e10e24; --ember: #ff2e42; --scar: #7a0a15;
  --bone: #ffffff;  --smoke: #8a7a7d; --amber: #ffb02e; --mint: #3ad46a;

  /* kit:23 puts Chakra Petch on headings and control labels; kit:19 puts
     JetBrains Mono on body copy. The names stay so the ~30 rules that
     reference them do not move. */
  --ui: "Chakra Petch", system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: "JetBrains Mono", ui-monospace, Consolas, monospace;

  --side-w: 264px;
  /* The guide is square: 2px on controls, 0 on surfaces (kit:83, :91, :141). */
  --radius: 2px;
  /* Corners for the two things a reader treats as objects: the section panel
     and the button. Separate from --radius (2px), which is a hairline used on
     small chrome like the terminal's bottom bar and would read as a square
     corner at panel size. */
  --radius-panel: 10px;
  --radius-btn: 8px;

  --scroll-thumb:       #1c1013;
  --scroll-thumb-hover: #7a0a15;
}

/* ============================================================
   Scrollbars - front.css owns these. front_base.html loads only this sheet,
   so the ::-webkit-scrollbar rules in app.css never reach a front page and
   every scroll area here was drawing the platform default.
   ============================================================ */

/* The standard properties, for engines with no ::-webkit-scrollbar. Guarded by
   @supports rather than declared outright: Chromium implements BOTH, and when
   scrollbar-width is set it stops honouring the ::-webkit-* sizing and draws its
   own thin bar. Measured before the guard - the bar came back 10px wide against
   the 8px declared below, and every other webkit rule here was inert with it. */
@supports not selector(::-webkit-scrollbar) {
  * {
    scrollbar-width: thin;
    scrollbar-color: var(--scroll-thumb) transparent;
  }
}

::-webkit-scrollbar { width: 8px; height: 8px; }

/* Transparent, not a colour: these sit on --panel, --panel-2 and the terminal's
   #0A0A0A, and one track colour would be wrong against two of the three. */
::-webkit-scrollbar-track { background: transparent; }

::-webkit-scrollbar-thumb {
  background: var(--scroll-thumb);
  border-radius: 999px;
  /* Inset by a transparent border so the thumb does not touch the content
     edge. background-clip keeps the border from painting the thumb colour. */
  border: 2px solid transparent;
  background-clip: content-box;
}

::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hover); background-clip: content-box; }

/* Both axes meeting; without this the corner keeps the platform grey. */
::-webkit-scrollbar-corner { background: transparent; }

* { box-sizing: border-box; }

body {
  margin: 0;
  /* Two layers: a red glow off the top-left corner, over a linear ramp that
     runs dark-red to black. fixed attachment sizes both to the viewport, so
     the ramp does not stretch with page height or restart when the content
     grows - a 1148px page and a 3878px one look the same.
     min-height keeps the gradient covering the viewport on a short page,
     where body would otherwise be only as tall as its content. */
  background:
    radial-gradient(1100px 620px at 10% -8%, rgba(225, 14, 36, .12), transparent 62%),
    linear-gradient(158deg, var(--bg-red) 0%, var(--bg) 46%, var(--bg-black) 100%);
  background-attachment: fixed, fixed;
  min-height: 100vh;
  color: var(--text);
  /* Body copy is the mono face (kit:19); headings and controls take --ui,
     now Chakra Petch, through the rules that already name it. */
  font-family: var(--mono);
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* ============ circuit surface ========================================== */

/* One copy of the artwork, referenced by both layers below. The traces are
   drawn in the accent red inside the svg rather than drawn white and tinted
   from css: an svg used as a background-image cannot be recoloured by the
   page, so the colour has to be baked in. No `#` appears in the data uri -
   rgb() is used instead - because `#` would have to be percent-encoded there. */
:root {
  --circuit: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240' viewBox='0 0 240 240'><g fill='none' stroke='rgb(225,14,36)' stroke-width='1.4' stroke-linejoin='round'><path d='M0 36 H58 L74 52 H132 L148 36 H240'/><path d='M0 148 H36 L52 132 H104 L120 148 H176 L192 132 H208 L224 148 H240'/><path d='M36 0 V52 L52 68 V116 L36 132 V240'/><path d='M148 36 V88 L164 104 V240'/><path d='M74 52 V196 L90 212 H240'/><path d='M192 0 V132'/><path d='M0 212 H50'/><rect x='84' y='158' width='36' height='14' rx='2'/><rect x='198' y='58' width='14' height='32' rx='2'/></g><g fill='rgb(225,14,36)'><circle cx='58' cy='36' r='3'/><circle cx='148' cy='36' r='3'/><circle cx='74' cy='52' r='3'/><circle cx='52' cy='132' r='3'/><circle cx='120' cy='148' r='3'/><circle cx='192' cy='132' r='3'/><circle cx='164' cy='104' r='3'/><circle cx='90' cy='212' r='3'/><circle cx='50' cy='212' r='3'/></g></svg>");
}

/* These ride HTML's pseudo-elements, not body's: body::before carried the
   engraved wordmark (removed 2026-09-04, see the note below)
   and body::after is the scanline veil (:170), both already taken. front.css
   sets no rule on `html` itself, so html's own `background` stays unset and
   body's gradient still propagates to the canvas - which is what the negative
   z-indexes below are measured against. A pseudo-element does not change that
   propagation; only a background on html itself would.

   Painting order, all negative so all above the canvas and below every in-flow
   box: -3 static traces, -2 pulse, -1 the word. */
html::before,
html::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  background-image: var(--circuit);
  background-repeat: repeat;
  background-size: 240px 240px;
}

html::before {
  z-index: -3;
  opacity: .05;
}

/* The pulse is the same traces at a higher opacity, revealed only through a
   narrow band that travels. The MASK moves, not the background - so the board
   stays put and the light runs along fixed copper, rather than the whole
   circuit sliding across the page. */
html::after {
  z-index: -2;
  /* .2, not the .4 this started at: at .4 the lit band read as a bright swath
     across the page rather than something glimpsed behind the word. */
  opacity: .2;
  -webkit-mask-image: linear-gradient(115deg, transparent 0%, transparent 26%, #000 50%, transparent 74%, transparent 100%);
          mask-image: linear-gradient(115deg, transparent 0%, transparent 26%, #000 50%, transparent 74%, transparent 100%);
  -webkit-mask-size: 55% 140%;
          mask-size: 55% 140%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: -60% 0;
          mask-position: -60% 0;
  animation: circuit-flow 22s linear infinite;
}

/* Undersized-image arithmetic - the same the removed sgen-shine used, and the
   only remaining user of it: at mask-size 55% the offset
   is P * 0.45 * width, so P increasing moves the band RIGHT. Outside the mask
   image there is no mask at all, which masks the element out completely - that
   is what confines the glow to the band instead of lighting the whole board.
   Sweeps over the first 65% of 22s, then holds off-frame, so it reads as an
   occasional pass rather than a strobe. */
@keyframes circuit-flow {
  0%   { -webkit-mask-position: -60% 0; mask-position: -60% 0; }
  65%  { -webkit-mask-position: 160% 0; mask-position: 160% 0; }
  100% { -webkit-mask-position: 160% 0; mask-position: 160% 0; }
}

/* Parked mid-board rather than off-frame, so the traces keep a lit section
   with nothing travelling. */
@media (prefers-reduced-motion: reduce) {
  html::after {
    animation: none;
    -webkit-mask-position: 50% 0;
            mask-position: 50% 0;
  }
}

/* The engraved SGEN wordmark that used to sit here is gone, removed on
   request 2026-09-04: this is not an SGEN-branded surface. It was a
   body::before carrying content: "SGEN", a body.auth-body::before that
   recentred it on /login, a max-width:720px recentring, the sgen-shine and
   sgen-light keyframes that drove it, and a prefers-reduced-motion block
   that parked it. All of it went together - nothing else animated with
   those two keyframes, and no template names any of it.

   The circuit surface above (html::before / html::after) is NOT part of
   this and stays: it carries no mark, only texture. */

/* The guide's scanline veil (kit:20). pointer-events:none, so it intercepts
   nothing - every click still reaches the control underneath, which is why it
   can be added without touching a single handler. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 9999;
  background: repeating-linear-gradient(0deg, transparent 0 3px, rgba(0, 0, 0, .16) 3px 4px);
  mix-blend-mode: multiply;
  opacity: .35;
}

/* Headings take the display face (kit:23). */
h1, h2, h3, h4 { font-family: var(--ui); }

a { color: inherit; text-decoration: none; }

:focus-visible { outline: 2px solid var(--accent-lit); outline-offset: 2px; border-radius: 4px; }

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

.shell { display: flex; min-height: 100vh; }

/* ---------------- sidebar ---------------- */

.side {
  width: var(--side-w);
  flex: none;
  /* Translucent, not var(--side-bg): an opaque panel here would cut a flat
     216px column out of the gradient down the whole left edge. The black wash
     keeps it readable as its own surface while the ramp continues under it. */
  background: rgba(7, 7, 7, .55);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-right: 1px solid var(--glass-line);
  padding: 22px 14px;
  display: flex;
  flex-direction: column;
  gap: 26px;
}

/* Brand lockup, ported from the kit's sidebar head
   (reputation-ui-kit.html:33-38 for the rules, :351-353 for the markup).
   The glyph is two pseudo-element borders on one 26px box - one square, one
   rotated 45deg at .55 - so it needs no image and no svg, and it recolours
   with --blood like everything else.

   Deviation from the kit: padding stays `0 10px` rather than the .sb-head
   20px. .nav-item sets padding: 8px 10px at :267 and the later .nav-item at
   :2250 does not override it, so 10px is what the nav labels below actually
   sit at and the glyph's left edge lines up with them.

   .brand-name sets no font-family, so it inherits the body face (--mono, :95
   = JetBrains Mono) exactly as the kit's .sb-name inherits from the kit body.
   Note this is NOT the nav's face: .nav-item:2251 switches those to --ui
   (Chakra Petch). The wordmark follows the kit, not its neighbours. */
.brand {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0 10px;
}

/* The mark itself. Replaced the kit's static rotated-square glyph
   (reputation-ui-kit.html:33-36) with a wireframe globe; the markup and all of
   the geometry are in templates/_brand.html.

   The colour is set ONCE here and the svg paints with currentColor, so the
   whole mark retints from this one line and carries no hex of its own. */
.brand-glyph {
  width: 26px;
  height: 26px;
  flex: none;
  color: var(--blood);
}

.globe { display: block; width: 100%; height: 100%; overflow: visible; }

/* The two SOLID shapes are scoped `.globe .g-*` on purpose. The group rule
   below is `.globe circle`, which is (0,1,1) - a bare `.g-sat` at (0,1,0)
   loses to it and computes fill:none/stroke:blood, i.e. the satellite draws as
   a hollow ring and the disc as a second circle straight on top of the rim.
   The descendant form is (0,2,0) and wins. */
.globe .g-disc,
.globe .g-sat {
  fill: currentColor;
  stroke: none;
}

/* non-scaling-stroke pins every line to device px. Without it a 30-unit
   viewBox drawn into 26px would multiply each width by .867 and the whole
   wireframe would render sub-pixel and muddy. It also holds the meridians at
   constant weight while scaleX squeezes them - a plain scaled stroke thins out
   as the ellipse collapses. */
.globe circle,
.globe ellipse {
  fill: none;
  stroke: currentColor;
  vector-effect: non-scaling-stroke;
}

.g-disc {
  opacity: .1;
  animation: brand-pulse 4.2s ease-in-out infinite;
}

.g-rim { stroke-width: 1; }
.g-par { stroke-width: .75; opacity: .45; }

.g-mer {
  stroke-width: .9;
  opacity: .7;
  /* fill-box, so `center` is the ellipse's own centre (15,15) and not the
     origin of the user space. */
  transform-box: fill-box;
  transform-origin: center;
  animation: brand-mer 5.4s linear infinite;
}

/* Negative delays start mid-cycle on the FIRST frame; a positive delay would
   stack all three on top of each other until the offsets elapsed. One third of
   5.4s apart puts them at even longitudes. */
.g-mer-b { animation-delay: -1.8s; }
.g-mer-c { animation-delay: -3.6s; }


.g-orbit {
  transform-box: view-box;
  transform-origin: 50% 50%;
  animation: brand-orbit 7.2s linear infinite;
}

/* One cycle is a HALF turn: it ends on scaleX(-1), which renders identically
   to scaleX(1) for an ellipse centred on its own origin, so the loop repeats
   with no jump frame. The intermediate stops are cos() sampled every 22.5deg -
   interpolating straight from 1 to -1 would give a triangle wave, which reads
   as a line bouncing rather than a meridian turning. */
@keyframes brand-mer {
  0%     { transform: scaleX(1); }
  12.5%  { transform: scaleX(.924); }
  25%    { transform: scaleX(.707); }
  37.5%  { transform: scaleX(.383); }
  50%    { transform: scaleX(0); }
  62.5%  { transform: scaleX(-.383); }
  75%    { transform: scaleX(-.707); }
  87.5%  { transform: scaleX(-.924); }
  100%   { transform: scaleX(-1); }
}

@keyframes brand-orbit {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

@keyframes brand-pulse {
  0%, 100% { opacity: .1; }
  50%      { opacity: .04; }
}

/* Frozen, not hidden: the three meridians park at three different widths, so
   the mark still reads as a globe with nothing moving. */
@media (prefers-reduced-motion: reduce) {
  .g-disc, .g-mer, .g-orbit { animation: none; }
  .g-disc { opacity: .1; }
  .g-mer-b { transform: scaleX(.66); }
  .g-mer-c { transform: scaleX(.33); }
}

.brand-name {
  font-weight: 700;
  letter-spacing: .28em;
  font-size: 15px;
  white-space: nowrap;
  overflow: hidden;
}

.brand-name span { color: var(--ember); }

.side nav { display: flex; flex-direction: column; gap: 2px; }

.nav-item {
  display: block;
  padding: 8px 10px;
  border-radius: 6px;
  font-size: 13.5px;
  color: var(--dim);
  transition: background .12s, color .12s;
}

.nav-item:hover { background: var(--ash); color: var(--text); }

.nav-item.is-active {
  background: var(--accent-bg);
  color: var(--accent-lit);
  font-weight: 500;
}

/* ---------------- content ---------------- */

.content {
  flex: 1;
  min-width: 0;
  padding: 32px clamp(24px, 3vw, 40px) 44px;
}

/* The dashboard is the only page that opts into the rail (body.has-rail), so
   this centres ITS column vertically without touching the other five pages.
   `safe center` centres the stack when it is shorter than the viewport and
   falls back to top-alignment the moment it is taller, so a full dashboard is
   never pushed off the top of the screen. */
body.has-rail .content {
  display: flex;
  flex-direction: column;
  justify-content: safe center;
}

.page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 18px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}

.page-head h1 { font-size: 19px; font-weight: 600; margin: 0 0 3px; letter-spacing: -.01em; }
.page-head p  { font-size: 13.5px; color: var(--dim); margin: 0; }

.action {
  background: var(--accent);
  color: #fff;
  font-size: 13px;
  font-weight: 500;
  padding: 8px 14px;
  border-radius: 6px;
  white-space: nowrap;
  transition: background .12s;
}
.action:hover { background: var(--accent-lit); }

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

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(178px, 1fr));
  gap: 16px;
  margin-bottom: 22px;
}

.stat {
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-line);
  border-radius: var(--radius);
  padding: 14px 16px 16px;
  min-width: 0;
}

.stat .k {
  font-size: 10.5px;
  letter-spacing: .085em;
  text-transform: uppercase;
  color: var(--mute);
  margin-bottom: 8px;
}

/* Which SOURCE a tile's number came from, on the tile itself.
   "Logged in" and "Session held" are two different measurements of the same
   question and they disagree by three accounts, so each says where it got its
   figure rather than leaving an operator to guess which to believe. Lower
   contrast and no letter-spacing: it qualifies the label, it is not a second
   label. */
.stat .k-sub {
  letter-spacing: 0;
  text-transform: none;
  opacity: .62;
  font-style: italic;
}

.stat .v {
  font-size: 27px;
  font-weight: 600;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}

.stat .v.is-pending { color: var(--mute); }

/* ---------------- panels ---------------- */

.panel {
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-line);
  border-radius: var(--radius);
  margin-bottom: 22px;
  overflow: hidden;
}

.panel > header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 15px 20px;
}

.panel > header h2 { font-size: 13.5px; font-weight: 600; margin: 0; }

.tag {
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--mute);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 2px 9px;
  white-space: nowrap;
}

.rows { border-top: 1px solid var(--line-soft); }

.row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 13px 20px;
  border-bottom: 1px solid var(--line-soft);
  font-size: 13.5px;
}

.row:last-child { border-bottom: 0; }

.row .label { color: var(--text); min-width: 0; overflow-wrap: anywhere; }
/* .who now LEADS the log line rather than trailing it (_raillog.html:269), so
   it is an identifier being scanned down a column, not an afterthought. --mute
   was chosen when it sat at the end; at the head of the line it needs to be
   separable from the message without out-shouting it, and it must not wrap -
   a two-line email would break the one-line-per-entry rule. --dim clears WCAG
   AA at 6.44:1 against --panel (see the palette note at the top of this file). */
.row .label .who {
  color: var(--dim);
  font-weight: 600;
  white-space: nowrap;
}

.row.is-empty { color: var(--mute); justify-content: flex-start; }

/* count + time badges */

.badge {
  font-family: var(--mono);
  font-size: 11.5px;
  min-width: 30px;
  text-align: center;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid transparent;
  flex: none;
  font-variant-numeric: tabular-nums;
}

.badge.ok   { color: var(--ok);   background: var(--ok-bg);   border-color: rgba(63,185,80,.28); }
.badge.warn { color: var(--warn); background: var(--warn-bg); border-color: rgba(210,153,34,.28); }
.badge.bad  { color: var(--bad);  background: var(--bad-bg);  border-color: rgba(248,81,73,.28); }
.badge.flat { color: var(--mute); border-color: var(--line); }
/* A warm-up context that carries no category: recorded before the column
   existed, or the labelling call failed. Dashed so the Topics column does
   not present raw free text as though it were a category. */
.badge.flat.is-raw { border-style: dashed; opacity: .75; font-style: italic; }

/* ---------------- footnote ---------------- */

.note {
  font-size: 12px;
  font-style: italic;
  color: var(--mute);
  margin: 0;
}

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

@media (max-width: 720px) {
  .shell { flex-direction: column; }
  .side {
    width: auto;
    border-right: 0;
    border-bottom: 1px solid var(--line-soft);
    flex-direction: row;
    align-items: center;
    gap: 14px;
    padding: 12px 14px;
    overflow-x: auto;
  }
  .side nav { flex-direction: row; gap: 4px; }
  .nav-item { white-space: nowrap; }
}

/* placeholder nav entries: present, not linked, not focusable */
.nav-item.is-soon {
  color: var(--faint);
  cursor: default;
}
.nav-item.is-soon:hover { background: transparent; color: var(--faint); }

.tag[data-state="up"]   { color: var(--ok);  border-color: rgba(63,185,80,.28); }
.tag[data-state="down"] { color: var(--bad); border-color: rgba(248,81,73,.28); }

/* ============================================================
   Sign-in
   ============================================================ */

.auth-body {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: 24px;
}

.auth-card {
  width: 100%;
  max-width: 360px;
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-line);
  border-radius: var(--radius);
  padding: 28px 26px 26px;
}

.auth-brand { padding: 0; margin-bottom: 22px; }

.auth-card h1 {
  font-size: 18px;
  font-weight: 600;
  margin: 0 0 4px;
  letter-spacing: -.01em;
}

.auth-sub {
  font-size: 13px;
  color: var(--dim);
  margin: 0 0 20px;
}

.auth-error {
  font-size: 13px;
  color: var(--bad);
  background: var(--bad-bg);
  border: 1px solid rgba(248, 81, 73, .28);
  border-radius: 6px;
  padding: 9px 12px;
  margin-bottom: 16px;
}

.auth-field { display: block; margin-bottom: 14px; }

.auth-field span {
  display: block;
  font-size: 12px;
  color: var(--dim);
  margin-bottom: 6px;
}

.auth-field input {
  width: 100%;
  background: var(--coal);
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--text);
  font-family: var(--ui);
  font-size: 14px;
  padding: 9px 11px;
}

.auth-field input:focus {
  outline: none;
  border-color: var(--accent);
}

.auth-submit {
  width: 100%;
  margin-top: 6px;
  background: var(--accent);
  color: #fff;
  border: 0;
  border-radius: 6px;
  font-family: var(--ui);
  font-size: 14px;
  font-weight: 500;
  padding: 10px;
  cursor: pointer;
  transition: background .12s;
}

.auth-submit:hover { background: var(--accent-lit); }

/* signed-in identity + sign out, shown in the front sidebar */

.side-foot {
  margin-top: auto;
  padding: 0 10px;
  font-size: 12px;
  color: var(--mute);
}

.side-foot .who {
  display: block;
  overflow-wrap: anywhere;
  margin-bottom: 8px;
}

.side-foot button {
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--dim);
  font-family: var(--ui);
  font-size: 12px;
  padding: 6px 10px;
  cursor: pointer;
  width: 100%;
  transition: border-color .12s, color .12s;
}

.side-foot button:hover { border-color: var(--accent); color: var(--accent-lit); }

/* ============================================================
   Front settings: forms and user rows
   ============================================================ */

.hidden { display: none !important; }

.form-body { padding: 16px; border-top: 1px solid var(--line-soft); }

.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 0 14px;
}

.auth-field em {
  font-style: normal;
  color: var(--mute);
}

.field-note {
  font-size: 12px;
  color: var(--mute);
  margin: -6px 0 14px;
}

.check-line {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 13px;
  color: var(--dim);
  margin-bottom: 16px;
  cursor: pointer;
}

.check-line input { accent-color: var(--accent); width: 14px; height: 14px; }

/* Seven day toggles on one row. Deliberately NOT wrapped in .auth-field: that
   block's descendant rules reach everything inside it and both hit these -
   .auth-field input (:413) put width:100%, a border, a background and
   padding:9px 11px on each 14px checkbox, and .auth-field span (:406) forced
   display:block plus a 6px bottom margin on every day label. */
.day-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 14px;
}

/* Full width, so it sits on its own line above the row of chips. */
.day-picker .day-lbl {
  width: 100%;
  margin: 0 0 2px;
  font-size: 12px;
  color: var(--dim);
}

.day-chip { cursor: pointer; }

/* Taken off-screen rather than display:none, which would drop the box out of
   the tab order and out of the label's implicit association with it. */
.day-chip input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  margin: 0;
  pointer-events: none;
}

.day-chip span {
  display: block;
  min-width: 44px;
  padding: 7px 10px;
  border: 1px solid var(--line);
  border-radius: 6px;
  font-family: var(--ui);
  font-size: 12px;
  font-weight: 500;
  color: var(--dim);
  text-align: center;
  transition: border-color .12s, color .12s, background .12s;
}

.day-chip:hover span { border-color: var(--mute); color: var(--text); }

.day-chip input:checked + span {
  border-color: var(--accent);
  background: var(--accent-bg);
  color: var(--accent-lit);
}

/* The input is off-screen, so keyboard focus has to be shown on the chip or it
   lands nowhere visible. */
.day-chip input:focus-visible + span {
  border-color: var(--accent-lit);
  box-shadow: 0 0 0 2px var(--accent-bg);
}

/* min-width:150px removed: it made "Close" 72px wide next to "New schedule
   this day" at 171px in the same modal footer, which is the one width mismatch
   left once the heights match. Buttons hug their text instead. */
.form-submit { width: auto; margin-top: 0; }
.form-submit:disabled { opacity: .55; cursor: default; }

/* user rows */

.row-tail {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
}

.row-note { font-size: 12px; color: var(--mute); }

.row-btn {
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--dim);
  font-family: var(--ui);
  font-size: 12px;
  padding: 4px 10px;
  cursor: pointer;
  transition: border-color .12s, color .12s;
}

.row-btn:hover { border-color: var(--bad); color: var(--bad); }

/* .row-btn had no disabled rule, so a button carrying the disabled attribute
   rendered identically to a live one - same colour, still cursor:pointer from
   the base rule, and the hover above still fired. Every .row-btn that uses the
   attribute (Generate, Clear, Back, Next, Stop) looked clickable while inert. */
/* Destructive row action. .row-btn's own hover already turns --bad, so this
   only carries the resting state that tells the two apart before hover. */
.row-btn.is-danger { border-color: rgba(248, 81, 73, .38); color: var(--bad); }
/* .is-danger existed only for .row-btn, so the edit card's Delete button
   (front_accounts.html:816) carried the class and rendered as an ordinary
   quiet button - a destructive control that looked identical to Cancel. */
.btn-quiet.is-danger { border-color: rgba(248, 81, 73, .38); color: var(--bad); }
.btn-quiet.is-danger:hover:not(:disabled) { border-color: var(--bad); color: var(--bad); }
.row-btn.is-danger:hover:not(:disabled) { border-color: var(--bad); color: var(--bad); }

.row-btn:disabled { opacity: .45; cursor: default; }
.row-btn:disabled:hover { border-color: var(--line); color: var(--dim); }

/* ============================================================
   Section-19 settings mock components.
   Same structure as the dossier mock (mtabs/mock-panel/mset/mtog/
   mchk/mchip/pill), re-tokenised onto the front palette so the page
   stays one theme instead of importing the dossier's.
   ============================================================ */

.mtabs {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  margin: 0 0 16px;
  border-bottom: 1px solid var(--line);
  padding-bottom: 10px;
}

.mtab {
  font-family: var(--mono);
  font-size: 11.5px;
  padding: 4px 10px;
  border-radius: 6px;
  color: var(--dim);
  border: 1px solid transparent;
  background: transparent;
  white-space: nowrap;
  cursor: pointer;
  transition: color .12s, border-color .12s, background .12s;
}

.mtab:hover { color: var(--text); }

.mtab.on {
  color: var(--accent-lit);
  border-color: var(--accent);
  background: var(--accent-bg);
}

.mock-panel {
  border: 1px solid var(--glass-line);
  border-radius: var(--radius);
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  padding: 18px 20px;
  margin-bottom: 18px;
}

.mock-panel .ph {
  font-weight: 600;
  font-size: 13.5px;
  margin-bottom: 10px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
}

.mset {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 9px 0;
  border-bottom: 1px dashed var(--line);
}

.mset:last-of-type { border-bottom: 0; }

.mtxt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.mset .lbl { font-size: 13px; color: var(--text); }
.mset .sub { font-size: 11.5px; color: var(--mute); }

/* toggle */

.mtog {
  width: 32px; height: 18px;
  border-radius: 999px;
  position: relative;
  flex: none;
  border: 1px solid var(--line);
  background: var(--coal);
  cursor: pointer;
  padding: 0;
  transition: background .14s, border-color .14s;
}

.mtog::after {
  content: "";
  position: absolute;
  top: 1px; left: 1px;
  width: 14px; height: 14px;
  border-radius: 50%;
  background: var(--mute);
  transition: left .14s, background .14s;
}

.mtog.on { background: var(--ok-bg); border-color: var(--ok); }
.mtog.on::after { left: 15px; background: var(--ok); }
.mtog[aria-disabled="true"] { opacity: .55; cursor: default; }

@media (prefers-reduced-motion: reduce) {
  .mtog, .mtog::after { transition: none; }
}

/* checkbox */

.mchk {
  width: 17px; height: 17px;
  border-radius: 4px;
  border: 1px solid var(--line);
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  color: transparent;
  background: var(--coal);
  cursor: pointer;
  padding: 0;
}

.mchk.on { background: var(--ok-bg); border-color: var(--ok); color: var(--ok); }

/* chips */

.mchip {
  font-family: var(--ui);
  font-size: 12px;
  font-weight: 500;
  padding: 4px 11px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--dim);
  cursor: pointer;
  transition: background .12s, border-color .12s, color .12s;
}

.mchip:hover { color: var(--text); }
.mchip.on { background: var(--accent); border-color: var(--accent); color: #fff; }

.mock-flex { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }

/* rows, pills, notes */

.mock-row {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  padding: 9px 0;
  border-bottom: 1px solid var(--line-soft);
}

.mock-row:last-of-type { border-bottom: 0; }
.mock-row .grow { flex: 1; min-width: 0; }

.pill {
  font-family: var(--mono);
  font-size: 10.5px;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--mute);
  white-space: nowrap;
}

.pill.g { color: var(--ok); border-color: rgba(63,185,80,.32); background: var(--ok-bg); }
.pill.a { color: var(--warn); border-color: rgba(210,153,34,.32); background: var(--warn-bg); }
.pill.r { color: var(--bad); border-color: rgba(248,81,73,.32); background: var(--bad-bg); }

.mock-note {
  font-size: 11.5px;
  color: var(--mute);
  margin-top: 10px;
  font-style: italic;
}

.tag {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .05em;
  padding: 2px 7px;
  border-radius: 5px;
  border: 1px solid;
  white-space: nowrap;
  text-transform: uppercase;
}

.t-no { color: var(--bad); border-color: rgba(248,81,73,.45); background: var(--bad-bg); }

.mgone .lbl { text-decoration: line-through; color: var(--mute); }

.mock-select {
  border: 1px solid var(--glass-line);
  border-radius: 6px;
  background: rgba(255, 255, 255, .04);
  backdrop-filter: var(--glass-blur-sm);
  -webkit-backdrop-filter: var(--glass-blur-sm);
  color: var(--text);
  font-family: var(--mono);
  font-size: 12px;
  padding: 5px 8px;
  flex: none;
}

.save-state {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute);
}

/* ============================================================
   All-settings table
   ============================================================ */

.tbl-wrap { overflow-x: auto; }

/* The accounts roster is the one table whose row count is unbounded - 45 rows
   today, and it grows every time a batch is imported. Left alone it stretches
   the page and the toolbar/pagination scroll off the bottom, so the controls
   that change the page are only reachable after scrolling past the rows they
   control. Cap the viewport and scroll INSIDE it; the header stays put so the
   twelve columns remain identifiable at any scroll depth.

   Scoped to .acct-tbl-wrap, not .tbl-wrap: the import preview and the delete
   list on the same page set their own inline max-height and must not inherit
   this one. */
.acct-tbl-wrap { max-height: 60vh; overflow-y: auto; }
.acct-tbl-wrap thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  /* thead is transparent by default, so rows would show through it while
     scrolling. --panel is the surface this table already sits on; --card does
     NOT exist in this stylesheet and would have silently fallen back to a
     hardcoded white, breaking the dark theme. */
  background: var(--panel);
}

table.stbl {
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
}

table.stbl th {
  text-align: left;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--mute);
  font-weight: 400;
  padding: 0 12px 9px 0;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

table.stbl td {
  padding: 9px 12px 9px 0;
  border-bottom: 1px dashed var(--line);
  vertical-align: top;
}

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

table.stbl td.k {
  font-family: var(--mono);
  color: var(--text);
  white-space: nowrap;
}

table.stbl td.v {
  font-family: var(--mono);
  color: var(--dim);
  overflow-wrap: anywhere;
  max-width: 340px;
}

table.stbl td.v.empty { color: var(--mute); font-style: italic; }
table.stbl td.v.masked { color: var(--warn); letter-spacing: .12em; }
table.stbl td.n { width: 1%; white-space: nowrap; }


/* warm-up progress bar */
.prog-track {
  height: 6px;
  border-radius: 999px;
  background: var(--coal);
  border: 1px solid var(--line);
  overflow: hidden;
  margin-top: 10px;
}
.prog-fill {
  height: 100%;
  background: var(--warn);
  transition: width .3s ease;
}
@media (prefers-reduced-motion: reduce) { .prog-fill { transition: none; } }

.auth-submit.danger { background: var(--bad); }
.auth-submit.danger:hover { background: #ff6a62; }


/* account picker inside the warm-up form (mirrors warmup.html's scroll box) */
.picker {
  max-height: 220px;
  overflow-y: auto;
  border: 1px solid var(--glass-line);
  border-radius: 6px;
  background: var(--glass-deep);
  backdrop-filter: var(--glass-blur-sm);
  -webkit-backdrop-filter: var(--glass-blur-sm);
}
.picker-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 11px;
  border-bottom: 1px solid var(--line-soft);
  font-size: 13px;
  cursor: pointer;
}
.picker-row:last-child { border-bottom: 0; }
.picker-row:hover { background: var(--panel-2); }
.picker-row input { accent-color: var(--accent); flex: none; }
.picker-email { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.picker-empty { padding: 12px; font-size: 13px; color: var(--mute); }

/* ============================================================
   Floating action button + modal
   ============================================================ */

.fab {
  position: fixed;
  right: clamp(18px, 3vw, 34px);
  bottom: clamp(18px, 3vw, 30px);
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--accent);
  color: #fff;
  border: 0;
  border-radius: 999px;
  font-family: var(--ui);
  font-size: 13.5px;
  font-weight: 500;
  padding: 12px 18px;
  cursor: pointer;
  /* Sits over the panel background, so it carries its own elevation. */
  box-shadow: 0 6px 20px rgba(0, 0, 0, .45), 0 0 0 1px rgba(255, 255, 255, .05);
  transition: background .12s, transform .12s;
}

.fab:hover { background: var(--accent-lit); transform: translateY(-1px); }
.fab:active { transform: translateY(0); }
.fab .fab-plus { font-size: 17px; line-height: 1; margin-top: -1px; }

/* The modal is display:none until .is-open, so the markup can live in the
   page without the backdrop swallowing clicks while it is shut. */
.modal {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: none;
  align-items: flex-start;
  justify-content: center;
  padding: clamp(16px, 6vh, 64px) 16px;
  overflow-y: auto;
  background: rgba(0, 0, 0, .78);
}

.modal.is-open { display: flex; }

.modal-card {
  width: 100%;
  max-width: 620px;
  /* OPAQUE ON PURPOSE. --glass-strong is rgba(18,10,13,.84) over a blur, so the
     card's effective background was whatever sat behind it: the same white text
     measured 19.73:1 over the page's own #060305 and washed out over a bright
     table row, which is why "hard to read" was reported for popups and not for
     the pages under them. #0f090b IS that dark composite, so nothing changes
     over a dark page - the ratio just stops depending on the page. */
  background: var(--modal-bg);
  border: 1px solid var(--glass-line);
  border-radius: var(--radius);
  box-shadow: 0 18px 48px rgba(0, 0, 0, .6);
  /* margin:auto keeps a short card centred and a tall one scrollable from
     the top, which align-items:center alone cannot do. */
  margin: auto;
}

.modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--line-soft);
}

.modal-head h2 {
  font-size: 14px;
  font-weight: 600;
  margin: 0;
  letter-spacing: -.01em;
}

.modal-body { padding: 20px; }

.modal-foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  padding: 12px 16px;
  border-top: 1px solid var(--line-soft);
}

.modal-x {
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--dim);
  font-family: var(--ui);
  font-size: 15px;
  line-height: 1;
  padding: 5px 9px;
  cursor: pointer;
}

.modal-x:hover { border-color: var(--accent); color: var(--accent-lit); }

/* ---- wide, two-column modal layout: is-wide / modal-2col / modal-side ----
   The base .modal-card above (:1396) caps at 620px, sized for a short
   confirm dialog. A long form - the account edit card, #acctModal - has no
   room to spread out under that cap and ends up one tall scrolling column.
   is-wide raises the cap for cards that opt in; modal-2col and modal-side
   arrange .modal-body's direct children into columns once there is room,
   both collapsing to one column under 760px. Same fixed/proportional
   two-track pattern as .lst-split (:2714 as of this write) - grid + gap
   instead of margins, min-width:0 so a long value can't force a track
   wider than the grid gives it. */
.modal-card.is-wide { max-width: 980px; }
/* No separate breakpoint needed here: .modal-card's own width:100%
   (:1397, untouched) already resolves the used width against .modal's
   content box before max-width caps it, so below 980px the card already
   fills the available width - the same reason the base 620px card never
   needed one either. */

.modal-2col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}

/* 220px is a fixed rail, not a fraction, so the sidebar reads as a nav or
   label column rather than competing for width with the main area.
   minmax(0, 1fr) on the second track stops a long value (an email, a URL)
   from overflowing the grid the way a bare 1fr can. */
.modal-side {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr);
  gap: 16px;
}

@media (max-width: 760px) {
  .modal-2col,
  .modal-side {
    grid-template-columns: 1fr;
  }
}

/* One column of fields inside either layout above. min-width:0 lets it
   shrink inside its grid track instead of forcing the track wider - same
   reason .lst-main/.lst-side get it at :2721. gap, not margin-bottom per
   field, keeps spacing consistent regardless of what the column holds. */
.modal-col {
  display: flex;
  flex-direction: column;
  gap: 14px;
  min-width: 0;
}

/* .auth-field carries its own 14px margin-bottom (:827) for the
   single-column forms it was built for; inside .modal-col that would
   stack with the gap above and double the space between fields. Zeroed
   for this context only - .auth-field itself is unchanged everywhere
   else it's used. */
.modal-col > .auth-field { margin-bottom: 0; }

/* Small uppercase heading marking a group of fields within a column. Same
   microcaps treatment as .field-label (:2602), sized down and coloured
   --mute - measured 4.85:1 on --modal-bg - since it names a section of
   the form rather than one field. */
.modal-col-head {
  font-family: var(--ui);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--mute);
  margin: 0;
}

.btn-quiet {
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--dim);
  font-family: var(--ui);
  font-size: 14px;
  font-weight: 500;
  padding: 9px 16px;
  cursor: pointer;
}

.btn-quiet:hover:not(:disabled) { border-color: var(--mute); color: var(--text); }

/* .btn-quiet was only ever on modal Cancel buttons, which never carry the
   disabled attribute, so it had no disabled rule - the same gap the
   .row-btn:disabled pair above closes. Save as job DOES use the attribute
   (it is disabled until an account is ticked), and without these it kept
   cursor:pointer and still lit up on hover while inert. */
.btn-quiet:disabled { opacity: .55; cursor: default; }
.btn-quiet:disabled:hover { border-color: var(--line); color: var(--dim); }

/* Same skin as .auth-field input, plus the bits only a textarea needs. */
.auth-field textarea {
  width: 100%;
  background: var(--coal);
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--text);
  font-family: var(--ui);
  font-size: 14px;
  line-height: 1.5;
  padding: 9px 11px;
  resize: vertical;
  min-height: 76px;
}

.auth-field textarea:focus { outline: none; border-color: var(--accent); }

/* Description under a business name in the list. */
.biz-desc {
  font-size: 12.5px;
  color: var(--dim);
  margin: 0 0 4px;
  padding-bottom: 8px;
  border-bottom: 1px dashed var(--line);
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

/* body.modal-open freezes the page behind the card so a scroll gesture
   over the backdrop moves the modal, not the list underneath it. */
body.modal-open { overflow: hidden; }

/* The dialog window.rdConfirm builds (static/confirm.js). Above .modal's
   z-index 50 at :1382, because one caller - the recreate-profile check in
   front_preparation.html - opens it while that page's account modal is still
   on screen, and an equal z-index would put it behind. */
.modal.rd-confirm { z-index: 60; }
.rd-confirm .modal-card { max-width: 460px; }

/* One line of the message. .modal-body's padding is the only spacing when the
   body is empty, so it is dropped in that case rather than left as a gap. */
.rd-confirm-line {
  margin: 0 0 8px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--dim);
}
.rd-confirm-line:last-child { margin-bottom: 0; }
.rd-confirm-body:empty { display: none; }

/* A fixed button floats over whatever scrolls beneath it, so a list that sits
   under one reserves its height plus its offset at the end. Measured without
   this rule: scrolled to the bottom, the last panel's lower edge (902px) fell
   inside the button's band (878-918px), which puts a one-line panel's own
   buttons underneath it. 88px clears the tallest case - the 40px button plus
   its 30px bottom offset. */
.fab-pad { padding-bottom: 88px; }

/* One location's four fields inside the edit modal, repeated per location. */
.loc-edit {
  border-top: 1px solid var(--line-soft);
  padding-top: 12px;
  margin-top: 14px;
}

.loc-edit-head {
  font-size: 12px;
  font-weight: 500;
  color: var(--dim);
  margin-bottom: 10px;
}

/* Generated-comment rows on Jobs step 3. A comment is a paragraph, so it gets
   its own full-width line under the address instead of sharing the row with
   it: .row is a centred flex line and .row-tail is flex:none, neither of
   which a paragraph can live in without overflowing the panel. */
.gen-row {
  flex-direction: column;
  align-items: stretch;
  gap: 5px;
}

.gen-head {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.gen-head .label { min-width: 0; overflow-wrap: anywhere; }

.gen-text {
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--dim);
  /* anywhere, not break-word: a place id or url with no spaces would still
     push the row wider than the panel under break-word. */
  overflow-wrap: anywhere;
  /* The generator can return line breaks; they are kept rather than collapsed. */
  white-space: pre-wrap;
}

.gen-text.is-empty { color: var(--mute); font-style: italic; }

/* Multilogin checker rows on Settings. Same stacking reason as .gen-row: the
   detail line is a command's raw output, not a badge, so it cannot sit in a
   centred flex line without pushing past the panel. */
.chk-row {
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
}

.chk-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
.chk-head .label { min-width: 0; overflow-wrap: anywhere; }

.chk-detail {
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.5;
  color: var(--mute);
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

.chk-fix {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 12.5px;
  color: var(--dim);
}

/* ---------------- schedule calendar ---------------- */

/* Marks which of List / Calendar is on screen. .row-btn:hover recolours the
   border to --bad, so the current one is marked by the accent instead. */
.row-btn.on-view {
  border-color: var(--accent);
  color: var(--accent-lit);
  background: var(--accent-bg);
}

.cal-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  /* Was 12px 0 10px. Symmetric now: nothing about the bar's position between
     the panel header and the grid asks for one more pixel above than below. */
  padding: 12px 0;
  flex-wrap: wrap;
}

/* min-width, not width: the label is centred between the two arrows and a
   narrower month must not let them slide inward.

   118px, not 150. Measured at this font, the widest of the twelve labels is
   "September 2026" at 110px, so 150 left 40px of dead space at its widest and
   65px on "August 2026" - which is the gap that pushed the forward arrow away
   from the title. 118 clears the widest by 8. */
.cal-title {
  font-size: 13.5px;
  font-weight: 600;
  min-width: 118px;
}

/* Capped so a long IANA name cannot push the run count off the bar. */
.cal-zone {
  max-width: 210px;
  font-size: 12px;
  padding: 5px 8px;
}

.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 1px;
  background: var(--line-soft);
  border: 1px solid var(--line-soft);
  border-radius: 6px;
  overflow: hidden;
}

/* The header row and the day grid are two grids, so the header keeps its own
   height without a min-height meant for cells. */
.cal-dow {
  background: var(--panel-2);
  padding: 7px 8px;
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--mute);
}

.cal-grid + .cal-grid { margin-top: 1px; border-radius: 0 0 6px 6px; border-top: 0; }

.cal-cell {
  background: var(--panel);
  min-height: 92px;
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
  /* The whole cell opens that day's actions, so it reads as a control. */
  cursor: pointer;
}

.cal-cell:hover { background: var(--panel-2); }
.cal-cell.is-other:hover { background: var(--coal); }

/* Days of the neighbouring months that fill out the first and last weeks. */
.cal-cell.is-other { background: var(--bg); }
/* Recessive by design - the guide's muted day is --dim at .4 opacity
   (kit:285). Tokenised so it sits on the palette's hue rather than the
   neutral grey left over from before the revamp. */
.cal-cell.is-other .cal-num { color: var(--faint); }

.cal-cell.is-today { box-shadow: inset 0 0 0 1px var(--accent); }
.cal-cell.is-today .cal-num { color: var(--accent-lit); font-weight: 600; }

.cal-num { font-size: 11.5px; color: var(--dim); }

/* The pill is a WRAPPER, not the button it used to be: it now holds two
   controls - the body, which edits the schedule, and the history icon on the
   right. A button inside a button is not parseable, so the outer element had
   to stop being one. Every .cal-run selector below still matches, including
   the is-extra pair. */
.cal-run {
  display: flex;
  align-items: stretch;
  width: 100%;
  background: var(--accent-bg);
  border: 1px solid rgba(229, 72, 77, .3);
  border-radius: 4px;
  font-family: var(--ui);
  font-size: 11px;
  line-height: 1.35;
  /* One line per run: a long schedule name must not grow the row's height. */
  overflow: hidden;
}

.cal-run-main {
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
  background: transparent;
  border: 0;
  color: var(--text);
  font: inherit;
  padding: 3px 4px 3px 5px;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The history icon. flex:none so a long name shrinks the body instead of
   pushing the icon out of the pill. */
.cal-run-log {
  flex: none;
  display: flex;
  align-items: center;
  background: transparent;
  border: 0;
  border-left: 1px solid rgba(255, 255, 255, .08);
  color: var(--mute);
  padding: 0 4px;
  cursor: pointer;
}

.cal-run-log svg { display: block; width: 11px; height: 11px; }
.cal-run-log:hover { color: var(--text); background: rgba(255, 255, 255, .06); }

/* The stop square, same shape as .cal-run-log so a chip reads as one pill
   whichever of the two it carries. Rendered on every chip and disabled while
   that schedule has nothing live, rather than appearing only mid-run - a
   control that is absent when idle cannot be found when it is needed. */
.cal-run-stop {
  flex: none;
  display: flex;
  align-items: center;
  background: transparent;
  border: 0;
  border-left: 1px solid rgba(255, 255, 255, .08);
  color: var(--mute);
  font: inherit;
  font-size: 9px;
  padding: 0 4px;
  cursor: pointer;
}

.cal-run-stop:disabled { opacity: .35; cursor: default; }
.cal-run-stop:not(:disabled) { color: var(--bad); }
.cal-run-stop:not(:disabled):hover { background: rgba(248, 81, 73, .18); }

/* Armed - the second click stops. An icon has no label to swap, so the
   confirmation is carried by the fill. */
/* --bad (#ff2e42) with white measures 3.67:1 and fails AA. This is the ARMED
   state of a destructive control - the moment the label matters most - so it
   darkens to #b02525 (6.69:1) rather than keeping a red that is bright but
   unreadable. Same reasoning as the primary-button hover above. */
.cal-run-stop.is-armed {
  background: #b02525;
  border-color: #b02525;
  color: #fff;
}

/* The status dot. Its colour is the only thing that changes per status, so the
   pill keeps one shape and the state reads at a glance. */
.cal-dot {
  display: inline-block;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  margin-right: 4px;
  vertical-align: middle;
  background: var(--mute);
}

.cal-run:hover { border-color: var(--accent); background: rgba(229, 72, 77, .2); }

/* Per status. `scheduled` keeps the accent look the pill has always had; the
   other three are the same three colours .badge.ok/.warn/.bad already use, so
   a reader meets one palette across the app. */
.cal-run.is-started  { background: var(--ok-bg);   border-color: rgba(63, 185, 80, .3); }
.cal-run.is-started:hover  { border-color: var(--ok); background: rgba(63, 185, 80, .2); }
.cal-run.is-started .cal-dot { background: var(--ok); }

.cal-run.is-skipped  { background: var(--warn-bg); border-color: rgba(210, 153, 34, .3); }
.cal-run.is-skipped:hover  { border-color: var(--warn); background: rgba(210, 153, 34, .2); }
.cal-run.is-skipped .cal-dot { background: var(--warn); }

.cal-run.is-missed   { background: transparent;    border-color: var(--line); }
.cal-run.is-missed:hover   { border-color: var(--mute); background: rgba(255, 255, 255, .04); }
.cal-run.is-missed .cal-dot { background: var(--mute); }
.cal-run.is-missed .cal-run-main { color: var(--dim); }

.cal-run.is-scheduled .cal-dot { background: var(--accent); }

/* Runs past the third are in the DOM but out of the layout until the cell is
   opened, so no run is unreachable from the grid. */
.cal-run.is-extra { display: none; }
.cal-cell.is-open .cal-run.is-extra { display: flex; }

/* Per-day account counts, under the day chips. Same reason .day-picker sits
   outside .auth-field: that block's descendant rules would restyle these
   inputs into full-width fields. */
.day-counts { display: flex; flex-wrap: wrap; gap: 6px; align-items: flex-end; margin: 0 0 14px; }
.day-counts .day-lbl { width: 100%; margin: 0 0 2px; font-size: 12px; color: var(--dim); }

.daycount { display: flex; flex-direction: column; gap: 3px; }
.daycount > span { font-size: 10.5px; color: var(--mute); text-align: center; }
.daycount input {
  width: 46px;
  background: var(--coal);
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--text);
  font-family: var(--ui);
  font-size: 12px;
  padding: 5px 4px;
  text-align: center;
}

.day-total { margin-left: auto; font-size: 11.5px; color: var(--mute); padding-bottom: 6px; }
.day-total.is-bad { color: var(--bad); }
.day-total.is-ok { color: var(--ok); }

/* How many accounts a calendar pill's day warms. flex:none, so a long name
   shrinks the body rather than pushing this out of the pill. */
.cal-count {
  flex: none;
  display: flex;
  align-items: center;
  padding: 0 4px;
  font-size: 10px;
  color: var(--mute);
  border-left: 1px solid rgba(255, 255, 255, .08);
}

/* One fire, and under it the accounts it actually ran. */
.runlog-head { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; }
.runlog-sub { width: 100%; margin-top: 4px; font-size: 11.5px; color: var(--mute); overflow-wrap: anywhere; }

.runlog-accounts { width: 100%; margin: 6px 0 0; display: flex; flex-direction: column; gap: 3px; }

.runlog-acct {
  display: flex;
  gap: 8px;
  align-items: center;
  font-size: 11.5px;
  padding: 3px 6px;
  border: 1px solid var(--line-soft);
  border-radius: 4px;
}

.runlog-acct .who { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; color: var(--dim); }
.runlog-acct .pct { flex: none; font-family: var(--mono); font-size: 11px; color: var(--mute); }

/* A bar rather than a number alone: progress is the one field that says how
   far a run got before it stopped. */
.runlog-bar { flex: none; width: 54px; height: 4px; border-radius: 2px; background: var(--line); overflow: hidden; }
.runlog-bar > i { display: block; height: 100%; background: var(--mute); }
.runlog-acct.is-completed .runlog-bar > i { background: var(--ok); }
.runlog-acct.is-failed .runlog-bar > i { background: var(--bad); }
.runlog-acct.is-running .runlog-bar > i { background: var(--accent); }
.runlog-acct.is-stopped .runlog-bar > i { background: var(--warn); }

.runlog-none { width: 100%; margin-top: 6px; font-size: 11.5px; color: var(--mute); }

/* One row per recorded run, inside the history card. */
.runlog-row { display: flex; gap: 10px; align-items: baseline; }
.runlog-when { flex: none; font-family: var(--mono); font-size: 11.5px; color: var(--mute); }
.runlog-msg { min-width: 0; overflow-wrap: anywhere; }

/* A day that has already gone by. openDayModal disables "New schedule
   this day" on these, so the number is dimmed to say so before the
   click. Only the number: the runs on it stay legible and stay
   editable. */
.cal-cell.is-past .cal-num { opacity: .4; }
.cal-cell.is-open { min-height: auto; }

.cal-more {
  background: transparent;
  border: 0;
  padding: 1px 2px;
  text-align: left;
  font-family: var(--ui);
  font-size: 10.5px;
  color: var(--mute);
  cursor: pointer;
}

.cal-more:hover { color: var(--text); text-decoration: underline; }

.cal-fail { padding: 16px; font-size: 13px; color: var(--bad); }

@media (max-width: 720px) {
  .cal-cell { min-height: 64px; padding: 4px; }
  .cal-run { font-size: 10px; }
}

/* Second line of a saved-job row: what the stored configuration holds. */
.saved-sub {
  display: block;
  font-size: 12px;
  margin-top: 2px;
  overflow-wrap: anywhere;
}

@media (max-width: 720px) {
  .fab { padding: 11px 15px; font-size: 13px; }
}

/* ---------------------------------------------------------------- warm-up terminal
   Floating log panel, shown only while a warm-up is running (loadActive drives
   it from the same rows.length it already uses for setRunningState). Carries no
   data-panel attribute on purpose: showTab toggles [data-panel] elements per
   tab, and a run is in progress regardless of which tab is open.

   z-index sits between .fab (40) and .modal (50), so it covers the button but
   an open card still covers it. */
.wterm {
  position: fixed;
  z-index: 45;
  width: min(520px, calc(100vw - 32px));
  background: var(--glass-deep);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-line);
  border-radius: 8px;
  box-shadow: 0 18px 44px rgba(0, 0, 0, .62);
  font-family: var(--mono);
  overflow: hidden;
}

.wterm-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  background: rgba(255, 255, 255, .04);
  border-bottom: 1px solid var(--glass-line);
  font-size: 11.5px;
  color: var(--dim);
  cursor: move;
  /* The bar is the drag handle; without this a drag selects its label text. */
  user-select: none;
  touch-action: none;
}

.wterm-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--line);
  flex: none;
}
.wterm-dot.is-live { background: var(--ok); }

.wterm-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.wterm-body {
  max-height: 260px;
  overflow-y: auto;
  padding: 6px 0;
}

/* The rows come from loadLogs unchanged - .row/.label/.who/.badge - so they are
   restyled here rather than the renderer being rewritten. */
.wterm .rows { border-top: 0; }

.wterm .row {
  display: block;
  padding: 3px 12px;
  border-bottom: 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ok);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.wterm .row::before { content: "$ "; color: var(--mute); }
.wterm .row.is-empty { color: var(--mute); }
.wterm .row.is-empty::before { content: ""; }

.wterm .row .label { color: var(--ok); }
.wterm .row .who { color: var(--mute); }

.wterm .row .badge {
  background: transparent;
  border: 0;
  padding: 0 0 0 8px;
  color: var(--mute);
  font-family: var(--mono);
  font-size: 11px;
}

/* Three cards in one line above the stepper: Running now, Recent sessions and
   the activity terminal. auto-fit rather than repeat(3, 1fr) because .hidden is
   display:none (:392) - when no run is active the terminal leaves the flow and
   the other two take the whole row instead of holding an empty third column. */
.wcards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 12px;
  /* stretch, not start: the three hold different amounts - "Nothing yet." next
     to 47 sessions - and start leaves a ragged bottom edge across the row. */
  align-items: stretch;
  margin-bottom: 12px;
}

/* .mock-panel carries its own margin-bottom (:502); the grid gap supplies the
   spacing here, so the two would otherwise stack. */
.wcards > * { margin-bottom: 0; min-width: 0; }

/* The terminal sits in the row instead of floating over the page. static drops
   it back into normal flow - the fixed position, the drag handle and the stored
   coordinates it needed are gone with it. */
.wterm-card {
  position: static;
  z-index: auto;
  width: auto;
  box-shadow: none;
  border-radius: var(--radius);
}

.wterm-card .wterm-bar { cursor: default; }

/* One scroll height across all three, so the row keeps a flat bottom edge
   however many entries each card holds. */
.wcards .rows { max-height: 260px; overflow-y: auto; }
.wcards .wterm-body { max-height: 260px; }

/* The generated search terms. The list scrolls inside itself instead of
   growing the step: every kind is still there and every Remove still reachable,
   but the controls under it stay on screen. */
.terms-box {
  max-height: 300px;
  overflow-y: auto;
}

/* Step-5 summary: a two-column read-back of what the run will do. */
.sumgrid {
  display: grid;
  grid-template-columns: minmax(140px, 210px) 1fr;
  gap: 7px 16px;
  align-items: baseline;
  font-size: 13px;
}

.sum-h {
  grid-column: 1 / -1;
  margin-top: 6px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--glass-line);
  color: var(--dim);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.sumgrid .sum-h:first-child { margin-top: 0; }

.sum-k { color: var(--mute); }
.sum-v { color: var(--text); font-family: var(--mono); font-size: 12.5px; }

/* The qualifier after a value - where the number came from, or what it costs. */
.sum-n { color: var(--mute); font-family: var(--ui); font-size: 11.5px; }

.sum-empty {
  grid-column: 1 / -1;
  color: var(--mute);
  font-style: italic;
}

/* ============================================================
   Custom dropdowns (dropdown.js). A native <select>'s option list is drawn by
   the OS, so none of the page's colours, blur or radius reach it. The select
   itself stays in the document - invisible, still the value holder - and these
   rules draw the button and the list that replace its popup.
   ============================================================ */

.dd { position: relative; }

/* Not display:none: the element stays in the document so anything reading or
   submitting it still finds it. */
.dd-native {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  pointer-events: none;
  border: 0;
  margin: 0;
  padding: 0;
}

.dd-btn {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  border: 1px solid var(--glass-line);
  border-radius: 6px;
  background: rgba(255, 255, 255, .04);
  backdrop-filter: var(--glass-blur-sm);
  -webkit-backdrop-filter: var(--glass-blur-sm);
  color: var(--text);
  font-family: var(--mono);
  font-size: 12px;
  text-align: left;
  padding: 5px 8px;
  cursor: pointer;
  transition: border-color .12s, background .12s;
}

.dd-btn:hover:not(:disabled) { border-color: var(--dim); }
.dd-btn:disabled { opacity: .45; cursor: default; }

.dd-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.dd-caret {
  width: 0;
  height: 0;
  flex: none;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid var(--dim);
  transition: transform .12s;
}
.dd.is-open .dd-caret { transform: rotate(180deg); }
.dd.is-open .dd-btn { border-color: var(--accent); }

.dd-list {
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 4px);
  z-index: 60;
  max-height: 240px;
  overflow-y: auto;
  padding: 4px;
  border: 1px solid var(--glass-line);
  border-radius: 8px;
  /* Opaque enough to read over whatever it covers - the list floats over body
     text, not over the page gradient. */
  background: var(--glass-veil);
  backdrop-filter: var(--glass-blur-lg);
  -webkit-backdrop-filter: var(--glass-blur-lg);
  box-shadow: 0 14px 34px rgba(0, 0, 0, .55);
}

.dd.is-open .dd-list { display: block; }
.dd.is-up .dd-list { top: auto; bottom: calc(100% + 4px); }

.dd-opt {
  padding: 7px 9px;
  border-radius: 5px;
  color: var(--dim);
  font-family: var(--mono);
  font-size: 12px;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dd-opt:hover { background: rgba(255, 255, 255, .06); color: var(--text); }

.dd-opt.is-sel {
  background: var(--accent-bg);
  color: var(--text);
  box-shadow: inset 2px 0 0 var(--accent);
}

.dd-empty { padding: 7px 9px; color: var(--mute); font-size: 12px; }

/* ============================================================
   Stepper controls. .row-btn (:482) is one flat outline for every small button
   on the page, so the step chips, Generate, Clear, Remove and Next all rendered
   identically - nothing said which one advances the form.
   ============================================================ */

.step-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--dim);
  font-family: var(--ui);
  font-size: 13px;
  font-weight: 500;
  padding: 6px 15px 6px 6px;
  cursor: pointer;
  transition: border-color .12s, color .12s, background .12s;
}

/* The number rides in its own disc so the state reads at a glance without
   depending on the label colour alone. */
.step-chip .n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--panel-2);
  border: 1px solid var(--line);
  color: var(--mute);
  font-family: var(--mono);
  font-size: 11px;
  flex: none;
}

.step-chip:hover { border-color: var(--dim); color: var(--text); }

.step-chip.is-current {
  border-color: var(--accent);
  background: var(--accent-bg);
  color: var(--text);
}
.step-chip.is-current .n {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.step-chip.is-done { border-color: var(--ok); }
.step-chip.is-done .n {
  background: var(--ok-bg);
  border-color: var(--ok);
  color: var(--ok);
}

/* Back and Next. Two weights, not one: Next carries the accent fill the page
   already uses for its committing action (.auth-submit :384), Back stays an
   outline so the pair cannot be confused for each other. */
.btn-step {
  font-family: var(--ui);
  font-size: 13px;
  font-weight: 500;
  border-radius: 6px;
  padding: 9px 22px;
  cursor: pointer;
  transition: background .12s, border-color .12s, color .12s;
}

.btn-step.is-back {
  background: transparent;
  border: 1px solid var(--line);
  color: var(--dim);
}
.btn-step.is-back:hover:not(:disabled) { border-color: var(--dim); color: var(--text); }

.btn-step.is-next {
  background: var(--accent);
  border: 1px solid var(--accent);
  color: #fff;
}
.btn-step.is-next:hover:not(:disabled) {
  background: var(--accent-lit);
  border-color: var(--accent-lit);
}

/* Both buttons take the disabled attribute at the ends of the range, and
   .row-btn's disabled rule no longer applies to them. */
.btn-step:disabled { opacity: .4; cursor: default; }

/* Back / step label / Next, pinned to the bottom of the viewport for as long as
   the panel is on screen. sticky rather than fixed so it stops at the panel's
   own bottom edge instead of floating over whatever follows it. */
.wstep-nav {
  position: sticky;
  bottom: 0;
  z-index: 5;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  /* Negative margins cancel .mock-panel's 14px/16px padding (:544) so the bar
     reaches the panel edges; the padding is put back on this element. */
  margin: 0 -16px -14px;
  padding: 10px 16px;
  /* Stronger than the panels: content scrolls under this one, so it needs
     enough body to stay legible over moving text rather than only over the
     page gradient. */
  background: var(--glass-veil);
  backdrop-filter: var(--glass-blur-lg);
  -webkit-backdrop-filter: var(--glass-blur-lg);
  border-top: 1px solid var(--glass-line);
  border-radius: 0 0 var(--radius) var(--radius);
}

@media (max-width: 720px) {
  /* No width here. The terminal is a grid child now and .wterm-card's
     width:auto lets its column size it; calc(100vw - 20px) ignored
     main.content's side padding and overflowed the page by 4px. */
  .wterm-body { max-height: 200px; }
  /* One card per row; three 260px columns do not fit. */
  .wcards { grid-template-columns: 1fr; }
}


/* ===========================================================================
   Uniform action buttons.

   Placed last so it wins on the four properties it sets without each rule
   above having to be rewritten; every other property those rules carry -
   colour, border, radius, background, transition - is untouched.

   min-height rather than height: `* { box-sizing: border-box }` at :103 makes
   the border part of the box, so a bordered .row-btn and a border-less
   .auth-submit land on the same 32px, and a button whose label wraps grows
   instead of clipping.

   NOT in this list, and each for a reason read off the code:
     .cal-run-stop / .cal-run-log / .cal-more - they sit inside .cal-run, a
       pill that is 20.8px tall (:1246-1258); a 32px child would push every
       calendar chip taller and change the month grid's density.
     .mtab - a tab strip, not an action button.
     .mchk / .mtog / .mchip - checkbox, toggle and chip form controls.
   ========================================================================= */
.row-btn,
.btn-quiet,
.auth-submit,
.btn-step,
.modal-x,
.dd-btn,
.fab,
.side-foot button {
  min-height: var(--btn-h);
  padding: 0 var(--btn-px);
  font-size: var(--btn-font);
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* .dd-btn is a full-width trigger with its label left and its caret right
   (:1666-1684). The shared rule above would centre both together.

   Its horizontal padding is 10px, not the shared 14px, and its gap 6px, not
   10px: this trigger lives inside an author-sized box - front_preparation.html
   :79 puts max-width:130px on the select and dropdown.js:44-46 copies that
   inline style onto the .dd wrapper - so the padding is spent out of a fixed
   width rather than growing the control. Measured at 14px/10px the label had
   82px for text needing 86 and rendered as "25 per pa...". At 10px/4px it gets
   130 - 2 border - 20 padding - 8 caret - 4 gap = 96, against a widest option
   ("500 per page") measured at 94 - so two pixels spare rather than the zero a
   6px gap left. The height stays on --btn-h with the rest. */
.dd-btn {
  display: flex;
  justify-content: space-between;
  padding: 0 10px;
  gap: 4px;
}

/* Square, not a pill: the label is a single x. min-width holds the same 32px
   the shared rule gives its height, and the horizontal padding goes so the
   glyph is not pushed off centre. */
.modal-x,
.row-btn.is-icon {
  padding: 0;
  min-width: var(--btn-h);
}

/* .auth-submit spans its form (width:100% at :427); .form-submit narrows it
   back to the content. inline-flex above would otherwise shrink-wrap the
   full-width case. */
.auth-submit {
  display: flex;
}

.form-submit {
  display: inline-flex;
}


/* ==== REVAMP SLICE 2 =======================================================
   Component treatment from the brand guide. Everything here re-skins a
   selector that already exists; nothing renames or removes one.
   ========================================================================= */

/* ---- surfaces: the guide's card (kit:141-144) --------------------------
   Square, bordered, with an accent rail that wipes in from the top on hover.
   Bordered, with an accent rail that wipes in from the top on hover. The rail
   is a ::before on .panel, which had no pseudo-element of its own.

   Was border-radius:0. Square corners here fought every rounded control the
   panel contains, and the guide's square card was the only reason for it. */
.panel {
  border-radius: var(--radius-panel);
  border-color: var(--line);
  position: relative;
}
.panel::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 2px;
  background: var(--accent);
  transform: scaleY(0);
  transform-origin: top;
  transition: transform .25s;
  pointer-events: none;
  z-index: 1;
}
.panel:hover { border-color: var(--scar); }
.panel:hover::before { transform: scaleY(1); }

/* Panel headings take the display face, uppercase and tracked (kit:77). */
.panel > header h2 {
  font-family: var(--ui);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
}

/* ---- buttons (kit:83-93) ----------------------------------------------
   Display face, wide tracking. The 32px height agreed earlier is kept - only
   the treatment changes, so every button stays the same size it was measured
   at.

   The guide's clipped bottom-right corner is REMOVED. It cut a diagonal off
   the corner of every button, and seven selectors below existed solely to
   cancel it on the controls it broke: icon buttons, calendar runs, the
   dropdown trigger. A treatment needing seven exemptions is not a treatment.
   Its declaration is quoted nowhere here on purpose - tests/test_corner_
   treatment.py asserts the polygon is absent from this file. */
.row-btn,
.btn-quiet,
.auth-submit,
.btn-step,
.fab,
.side-foot button {
  font-family: var(--ui);
  letter-spacing: .14em;
  font-weight: 600;
  text-transform: uppercase;
  border-radius: var(--radius-btn);
  transition: background .18s, border-color .18s, color .18s, box-shadow .18s;
}

/* Filled: the primary action (kit:83-84).

   color was var(--void) - #0a0507, near-black - on var(--accent) #e10e24.
   Measured 4.13:1, which fails WCAG AA for body text (4.5:1). Dark text on a
   saturated red is the worst pairing in this palette and it was on every
   primary action: the FAB, the step-forward button, and the login submit.
   White on the same red is 4.90:1 and passes. */
.auth-submit,
.btn-step.is-next,
.fab {
  background: var(--accent);
  border: 1px solid var(--accent);
  color: #fff;
}
/* Hover DARKENS rather than brightens, and the reason is contrast, not taste.
   --ember (#ff2e42) with white text measures 3.67:1 - below the 4.5:1 AA floor
   for body text - so hovering a primary button made its label HARDER to read
   than at rest (4.90:1). The label is 13.5px at weight 500, which is not
   "large text" under WCAG, so the 3.0 allowance does not apply. #b00b1c keeps
   white at 6.66:1 and still reads as a state change. The glow carries the
   brightening that --ember was doing. */
.auth-submit:hover:not(:disabled),
.btn-step.is-next:hover:not(:disabled),
.fab:hover {
  background: #b00b1c;
  border-color: #b00b1c;
  box-shadow: 0 0 22px -6px var(--ember);
}

/* Outlined: everything else (kit:86-87). */
.row-btn,
.btn-quiet,
.btn-step.is-back,
.side-foot button {
  background: transparent;
  border: 1px solid var(--mute);
  color: var(--text);
}
.row-btn:hover:not(:disabled),
.btn-quiet:hover:not(:disabled),
.btn-step.is-back:hover:not(:disabled),
.side-foot button:hover {
  border-color: var(--accent);
  color: var(--accent-lit);
  background: transparent;
  box-shadow: none;
}

/* Destructive keeps the guide's danger treatment (kit:88-89). */
.row-btn.is-danger {
  color: var(--ember);
  border-color: var(--scar);
}
.row-btn.is-danger:hover:not(:disabled) {
  background: var(--scar);
  color: var(--text);
  border-color: var(--scar);
}

/* Icon-only buttons keep the square (kit:91) - a clipped corner on a 32px
   square eats the glyph. These already carry padding:0 + min-width. */
.modal-x,
.row-btn.is-icon,
.cal-run-stop,
.cal-run-log,
.cal-more,
.cal-run-main {
  text-transform: none;
  letter-spacing: normal;
}
.dd-btn { text-transform: none; letter-spacing: .04em; }

.row-btn:disabled,
.btn-quiet:disabled,
.auth-submit:disabled,
.btn-step:disabled {
  opacity: .4;
  cursor: not-allowed;
  box-shadow: none;
}

/* ---- tabs: the guide's underline, not a pill (kit:152-155) ------------- */
.mtabs { gap: 2px; padding-bottom: 0; }
.mtab {
  font-family: var(--ui);
  font-size: 12px;
  letter-spacing: .1em;
  text-transform: uppercase;
  padding: 11px 18px;
  border: 0;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  background: transparent;
  color: var(--mute);
}
/* --mute measured 4.13:1 here: the tab strip sits on the page gradient, whose
   red radial lifts the backdrop above the flat --bg the token was checked
   against. --dim clears AA on the same surface. */
.mtab { color: var(--dim); }
.mtab:hover { color: var(--text); background: transparent; }
.mtab.on {
  color: var(--accent-lit);
  border-bottom-color: var(--accent);
  background: transparent;
}

/* ---- badges, tags, pills: one treatment (kit:98-103) ------------------- */
.badge, .tag, .pill {
  font-family: var(--ui);
  letter-spacing: .12em;
  text-transform: uppercase;
  border-radius: 0;
}
.badge { font-size: 10.5px; padding: 3px 9px; }
.tag, .pill {
  font-size: 10px;
  padding: 4px 10px;
  border: 1px solid var(--line);
  color: var(--mute);
}
.badge.flat { border-color: var(--line); color: var(--mute); }

/* ---- inputs (kit:110-112) ---------------------------------------------- */
.mock-select,
input[type="text"], input[type="number"], input[type="password"],
input[type="email"], input[type="datetime-local"], textarea, select {
  border-radius: 0;
  background: var(--coal);
  border: 1px solid var(--line);
  color: var(--text);
  font-family: var(--mono);
  transition: border-color .16s, box-shadow .16s;
}
.mock-select:focus,
input:focus, textarea:focus, select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(225, 14, 36, .12);
}
::placeholder { color: var(--faint); }

/* Field labels: uppercase micro-caps (kit:109). */
.auth-field > span, .lbl, .field-label {
  font-family: var(--ui);
  letter-spacing: .14em;
  text-transform: uppercase;
}

/* ---- sidebar nav: the guide's left rail marker (kit:42-47) ------------- */
.nav-item {
  border-radius: 0;
  font-family: var(--ui);
  letter-spacing: .06em;
  position: relative;
}
.nav-item.is-active {
  background: var(--ash);
  color: var(--text);
}
.nav-item.is-active::before {
  content: "";
  position: absolute;
  left: 0; top: 6px; bottom: 6px;
  width: 2px;
  background: var(--accent);
}

/* ---- calendar: square cells, guide chrome ----------------------------- */
.cal-grid { border-radius: 0; }
.cal-grid + .cal-grid { border-radius: 0; }
.cal-dow {
  font-family: var(--ui);
  letter-spacing: .14em;
  /* --faint measured 1.80:1 here. These name the columns, so they are read,
     not decoration. */
  color: var(--mute);
}
.cal-run { border-radius: 0; }
.cal-num { font-family: var(--ui); }
.cal-title {
  font-family: var(--ui);
  letter-spacing: .08em;
  text-transform: uppercase;
}

/* ---- modals (kit:297-302) --------------------------------------------- */
.modal-card {
  border-radius: 0;
  border: 1px solid var(--scar);
  /* Two layers, not one. The guide's card carries a single red bloom
     (kit:297: 0 0 60px -10px var(--blood)), and setting box-shadow to that
     alone dropped the black depth shadow the modal had at :1041 - so the card
     read as a glow with nothing holding it off the page.

     The bloom is pulled in from -10px spread to -26px and from 60px blur to
     30px, which is what "reduce the red glow" asks for, and the original
     depth shadow is listed first so the card still sits above the overlay. */
  box-shadow:
    0 18px 48px rgba(0, 0, 0, .6),
    0 0 30px -26px var(--accent);
}
.modal-head h2 {
  font-family: var(--ui);
  letter-spacing: .1em;
  text-transform: uppercase;
}

/* ---- rows ------------------------------------------------------------- */
.row .label { font-family: var(--ui); letter-spacing: .03em; }
.rows .row.is-empty { color: var(--mute); }


/* CSV drop zone (preparation → Import CSV). A label-styled box rather than a
   bare <input type="file">, whose native rendering ignores every other control
   on the card. The input itself stays in the DOM and is still what opens the
   picker, so keyboard and screen-reader use go through the real control. */
.csv-drop {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 22px 16px;
  border: 1px dashed var(--glass-line);
  border-radius: 8px;
  background: var(--glass-deep);
  cursor: pointer;
  transition: border-color .15s ease, background .15s ease;
}
.csv-drop:hover, .csv-drop:focus-visible { border-color: var(--accent); outline: none; }
.csv-drop.is-over { border-color: var(--accent); background: var(--ash); }
.csv-drop strong { color: var(--text); font-weight: 600; }
@media (prefers-reduced-motion: reduce) { .csv-drop { transition: none; } }

/* One line per row in the import result list. */
.csv-res { padding: 2px 0; font-size: 13px; line-height: 1.5; }
.csv-res.is-ok { color: var(--ok); }
.csv-res.is-dup { color: var(--warn); }
.csv-res.is-bad { color: var(--bad); }


/* ============================================================
   M3 — Listings and Plans.
   Appended, not merged into the blocks above: every rule here is new and
   named for the two pages that use it, so this section can be read (or
   reverted) on its own. Nothing above this line is modified. The pages lean
   on the existing vocabulary - .mock-panel/.ph, table.stbl, .sumgrid,
   .stats/.stat, .pill, .tag, .field-note, .mock-note - and these fill only
   the gaps that vocabulary has no rule for.
   ============================================================ */

/* Listings is a wide working column (table, calculator, history) beside a
   narrow reading column (rating, identity). One column below 1080px, where
   the eight-column table already wants the full width on its own. */
.lst-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 360px);
  gap: 16px;
  align-items: start;
}

.lst-main, .lst-side { min-width: 0; }

@media (max-width: 1080px) {
  .lst-split { grid-template-columns: minmax(0, 1fr); }
}

/* A picked row. Rows are clickable because picking one is what fills the
   Identity panel, so the pointer has to say so. */
table.stbl tr[data-loc] { cursor: pointer; }
table.stbl tr[data-loc]:hover td { background: var(--panel-2); }
table.stbl tr[data-loc].is-picked td { background: var(--accent-bg); }
table.stbl tr[data-loc].is-picked td.k { color: var(--accent-lit); }

/* The business a location belongs to, under its name: the tree, kept, without
   a second column spent on it. */
.stbl-parent {
  display: block;
  font-family: var(--ui);
  font-size: 10.5px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--faint);
  margin-top: 3px;
}

/* A cell with NO SOURCE YET - not zero, not unknown-because-of-an-error.
   Dashed and dimmed so it never reads as a measured value, and every one of
   them carries a title saying which table is empty and when it fills. */
.stbl-unknown {
  font-family: var(--mono);
  color: var(--faint);
  border-bottom-style: dashed;
  cursor: help;
}

/* A field the operator has not filled in. Distinct from .stbl-unknown above:
   this one is answerable today, by typing it. */
.stbl-unset { color: var(--mute); font-style: italic; }

/* The single full-width row a table shows instead of data. */
.stbl-note {
  padding: 16px 12px 16px 0;
  font-size: 13px;
  color: var(--mute);
  line-height: 1.6;
  border-bottom: 0;
}
.stbl-note strong { color: var(--dim); font-weight: 600; }

/* An empty panel that says WHY it is empty. Every panel on these two pages
   without a data source uses this rather than a spinner or a bare "no data":
   the first line is the state, the second is the reason and the milestone. */
.empty-why {
  font-size: 12.5px;
  color: var(--mute);
  line-height: 1.6;
  padding: 4px 0 2px;
}
.empty-why strong {
  display: block;
  color: var(--dim);
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 5px;
}

/* Inline identifiers - table names, endpoints, module paths. front.css had no
   rule for a bare <code>, and these two pages name the missing table in almost
   every empty state. */
.mock-panel code,
.stbl-note code,
.empty-why code {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--dim);
  background: rgba(255, 255, 255, .04);
  border-radius: 3px;
  padding: 1px 4px;
}

/* The calculator's three headline numbers. Narrower than the default .stats
   track, because three of them share the working column rather than the page. */
.calc-stats {
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  margin: 14px 0 12px;
}

.stat .v.is-ok  { color: var(--ok); }
.stat .v.is-bad { color: var(--bad); }

/* The sentence under the numbers - the answer in words, which is the form the
   plan states it in ("A 1 star here drops you to 4.5 and costs 6 five-star
   reviews"). */
.calc-verdict {
  margin: 0 0 14px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--text);
  border-left: 2px solid var(--line);
  padding-left: 11px;
}
.calc-verdict.is-ok  { border-left-color: var(--ok); }
.calc-verdict.is-bad { border-left-color: var(--bad); color: var(--dim); }

/* ============================================================
   Listings — the write UI ported from the Businesses page.

   front_businesses.html held add / edit / delete, /businesses is now a 302 to
   /listings, and the forms moved with it. Everything that page already styled
   is reused as-is (.modal*, .auth-field, .form-grid, .field-note, .row-btn,
   .fab, .loc-edit-head, .mset); the rules below are only the parts that page
   had no equivalent of — a per-row actions cell in a table, an inert block of
   fields no endpoint accepts, and the catchment code list.
   ============================================================ */

/* The location table's ninth column. flex:none on the cell keeps the two
   buttons on one line while the eight columns to its left take the width. */
.lst-act {
  white-space: nowrap;
  text-align: right;
}
.lst-act .row-btn { margin-left: 6px; }
th.lst-act-h { text-align: right; }

/* Business rows. .mset is a space-between flex line, so the three actions need
   their own box or they space themselves out across the gap. They wrap under
   the name on a narrow panel rather than squeezing the name to nothing. */
.lst-biz-acts {
  display: flex;
  flex: none;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 6px;
}

/* ------------------------------------------------------------------
   Fields with no endpoint. Seven columns on Location that to_json()
   returns and nothing writes: shown so the values on file are visible
   and editable-looking is exactly what they must NOT be. Dimmed as a
   block, hatched down the left edge, and never mistakable for the live
   fields above them.
   ------------------------------------------------------------------ */
.lst-nofield {
  margin-top: 6px;
  padding: 12px 0 0 12px;
  border-top: 1px solid var(--line-soft);
  border-left: 2px dashed var(--line);
}

.lst-nofield-head {
  font-size: 12px;
  font-weight: 500;
  color: var(--mute);
  margin-bottom: 12px;
}
.lst-nofield-head strong { color: var(--dim); font-weight: 600; }

/* front.css had no input:disabled rule at all, so a disabled box rendered
   identically to a live one — the same gap .row-btn:disabled and
   .btn-quiet:disabled each closed for their own control. Without this the
   whole point of the block is invisible. */
.lst-nofield input:disabled,
.lst-nofield textarea:disabled {
  opacity: .55;
  cursor: not-allowed;
  background: transparent;
  border-style: dashed;
}
.lst-nofield .auth-field span { color: var(--mute); }

.lst-nofield-note {
  font-size: 12px;
  line-height: 1.6;
  color: var(--mute);
  margin: 0 0 10px;
}
.lst-nofield-note:last-child { margin-bottom: 4px; }
.lst-nofield-note strong { color: var(--dim); font-weight: 600; }
.lst-nofield-note code {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--dim);
  background: rgba(255, 255, 255, .04);
  border-radius: 3px;
  padding: 1px 4px;
}

/* ------------------------------------------------------------------
   catchment_cities. A list of Multilogin proxy city codes, edited as
   chips with add and remove — not as raw JSON. The controls are inert
   for the same reason as the rest of the block above; the shape is the
   one the column holds so it needs no rework when an endpoint accepts it.
   ------------------------------------------------------------------ */
.lst-catch-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  min-height: 30px;
  align-items: center;
  padding: 5px 6px;
  border: 1px dashed var(--line);
  border-radius: 6px;
  background: transparent;
}

.lst-catch-empty { font-size: 12px; color: var(--faint); font-style: italic; padding: 0 3px; }

.lst-catch-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 2px 4px 2px 9px;
  opacity: .7;
}
.lst-catch-chip code {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--dim);
}

.lst-catch-x {
  background: transparent;
  border: 0;
  color: var(--mute);
  font-size: 14px;
  line-height: 1;
  padding: 0 3px;
  cursor: pointer;
}
.lst-catch-x:disabled { opacity: .45; cursor: not-allowed; }

.lst-catch-add {
  display: flex;
  gap: 8px;
  margin-top: 8px;
}
.lst-catch-add input { flex: 1; min-width: 0; }
.lst-catch-add .row-btn { flex: none; }

/* ---------------------------------------------------------------------------
   M3 — the Location detail block on Listings.

   Successor to .lst-nofield*, which styled these same fields while they were
   disabled: api_v1 accepted country / region / city / catchment_cities /
   website / category / target_rating, answered 200 and discarded them, so the
   inputs were rendered dead on purpose. _apply_location_details (api_v1.py)
   now stores all seven, the inputs are live, and the dimming those rules
   carried would be a lie. The .lst-nofield* rules above are left in place
   rather than edited out: this file is append-only, and every other selector
   in that region is still used by the catchment chip editor.
   --------------------------------------------------------------------------- */

.lst-locdetail {
  margin-top: 6px;
  padding: 12px 0 0 12px;
  border-top: 1px solid var(--line-soft);
  border-left: 2px solid var(--line);
}

.lst-locdetail-head {
  font-size: 12px;
  font-weight: 500;
  color: var(--mute);
  margin-bottom: 12px;
}
.lst-locdetail-head strong { color: var(--dim); font-weight: 600; }

.lst-locdetail-note {
  font-size: 12px;
  line-height: 1.6;
  color: var(--mute);
  margin: 0 0 10px;
}
.lst-locdetail-note:last-child { margin-bottom: 4px; }
.lst-locdetail-note strong { color: var(--dim); font-weight: 600; }
.lst-locdetail-note code {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--dim);
  background: rgba(255, 255, 255, .04);
  border-radius: 3px;
  padding: 1px 4px;
}

/* The chip remove buttons are live now, so undo the :disabled dimming the
   earlier rule set on them. */
.lst-catch-x { opacity: 1; cursor: pointer; }

/* ═══════════════════════════════════════════════════════════════════════════
   ACCOUNTS  —  front_accounts.html
   ───────────────────────────────────────────────────────────────────────────
   Appended for the roster page split out of Preparation. Every rule below is
   new and prefixed .acct-; nothing above this header is touched, because
   front_preparation.html still renders from those same classes until its route
   is switched over.

   The page draws two axes that must never look like one column: Account.stage
   (durable lifecycle) and Account.status (transient runtime state). They get
   two headers, each with its own sub-label, and the sub-label is what carries
   the distinction visually.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The blocks panel sits first on the page and has to read as the thing to
   deal with, without becoming an alarm on a roster where nothing is wrong -
   so the accent is a left rule, not a filled background. */
.acct-blocks { border-left: 2px solid var(--bad); }

/* Twelve columns. .tbl-wrap already scrolls horizontally (front.css:1257);
   this stops the two long text columns collapsing the numeric ones on a
   narrow viewport. */
table.stbl.acct-tbl td.k { white-space: nowrap; }
table.stbl.acct-tbl th   { vertical-align: bottom; }

/* The second line of a two-line column header: what the column means, not
   what it is called. "durable lifecycle" under Stage and "transient runtime"
   under Status are the whole reason the two columns are not one. */
.acct-th-sub {
  display: block;
  font-family: var(--ui);
  font-size: 9.5px;
  letter-spacing: .04em;
  text-transform: none;
  font-weight: 400;
  color: var(--faint);
  margin-top: 3px;
}

/* Persona and device, under the email: two facts the old roster spent two
   columns on, kept without spending two of the twelve here. */
.acct-sub {
  display: block;
  font-family: var(--ui);
  font-size: 10.5px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--faint);
  margin-top: 3px;
}

/* A row whose account is blocked or parked. Tinted, not reddened: the row is
   still readable data, and the Blocks cell is what states the condition. */
table.stbl tr.acct-flagged td { background: var(--bad-bg); }
table.stbl tr.acct-flagged td.k { color: var(--text); }

/* ---------------------------------------------------- lifecycle, edit card */

/* Read-only block at the top of the edit card. Bordered rather than boxed in
   a panel, so it reads as a header for the form under it and not as another
   group of controls - nothing in it can be typed into. */
.acct-life {
  border: 1px solid var(--line-soft);
  border-radius: 6px;
  padding: 12px 14px;
  margin: 0 0 16px;
  background: var(--panel-2);
}

.acct-life-head {
  font-family: var(--ui);
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--mute);
  margin-bottom: 10px;
}
.acct-life-head em {
  font-style: normal;
  text-transform: none;
  letter-spacing: 0;
  color: var(--faint);
}

.acct-life-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 10px 16px;
}

.acct-life-cell { min-width: 0; }

.acct-life-k {
  display: block;
  font-family: var(--ui);
  font-size: 10px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: 3px;
}
.acct-life-k em {
  font-style: normal;
  color: var(--mute);
  text-transform: none;
  letter-spacing: 0;
}

.acct-life-v {
  display: inline-block;
  font-family: var(--mono);
  font-size: 12.5px;
  color: var(--text);
  overflow-wrap: anywhere;
}

/* .stbl-unknown (front.css:2725) dashes a table cell's own bottom border; a
   span has none, so the six columns with no API source get one here. Same
   colour, same cursor, same meaning: no source yet, not a measured zero. */
.acct-life-v.stbl-unknown {
  color: var(--faint);
  border-bottom: 1px dashed var(--faint);
}

/* One sentence stating the block, under the grid. Neutral by default: most
   accounts have nothing to say here and a permanently red line would train
   the eye to skip it. */
.acct-life-block {
  font-size: 12px;
  line-height: 1.6;
  color: var(--mute);
  margin-top: 12px;
}
.acct-life-block.is-bad  { color: var(--bad); }
.acct-life-block.is-warn { color: var(--warn); }

.acct-life .row-btn { margin-top: 8px; }

/* Start / stop the Multilogin profile, beside the profile-id box they act on. */
.acct-profile-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 0 0 12px;
}

/* ============================================================
   Listings — the collector's surface (M4 wiring)

   Appended, nothing above edited. Six groups, all scoped to the
   Listings page:

     .lst-help*     the "?" disclosures. Native <details>/<summary>,
                    not a tooltip system: a tooltip cannot be read on
                    a touch screen, cannot be copied, and disappears
                    the moment you move to write the answer down.
     .lst-read*     the 40-second read. A synchronous request that
                    drives a real browser looks identical to a hang
                    unless something visibly moves, so a counter, a
                    bar and a sentence do.
     .lst-drift*    the drift line. Three tones, and the DEFAULT one
                    is neutral - "not confirmed yet" is the ordinary
                    state of a listing read once, not a fault, and
                    colouring it red would teach the eye to ignore it.
     .lst-cov-*     coverage numbers and the service's own sentence.
     .lst-cell-soft a table cell holding a real non-numeric state.
                    Distinct from .stbl-unknown (front.css:2725),
                    which means NO SOURCE and is dashed to say so.
   ============================================================ */

/* ---------------------------------------------------- help disclosures */

.lst-help {
  margin: 12px 0 0;
  border-top: 1px solid var(--line);
  padding-top: 8px;
}
.lst-help + .lst-help { margin-top: 0; }

.lst-help > summary {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  cursor: pointer;
  list-style: none;
  padding: 4px 0;
  font-family: var(--ui);
  font-size: 12px;
  line-height: 1.5;
  color: var(--mute);
  transition: color .12s;
}
/* Safari/Chrome draw their own triangle in front of the flex row; the "?"
   badge is the marker here. */
.lst-help > summary::-webkit-details-marker { display: none; }
.lst-help > summary::marker { content: ""; }
.lst-help > summary:hover { color: var(--dim); }
.lst-help[open] > summary { color: var(--dim); }

.lst-help-q {
  flex: none;
  display: block;
  width: 16px;
  height: 16px;
  margin-top: 1px;
  border: 1px solid var(--line);
  border-radius: 50%;
  font-family: var(--mono);
  font-size: 10.5px;
  line-height: 14px;
  text-align: center;
  color: var(--dim);
  transition: border-color .12s, color .12s;
}
.lst-help[open] .lst-help-q,
.lst-help > summary:hover .lst-help-q {
  border-color: var(--accent);
  color: var(--accent-lit);
}

.lst-help-a {
  padding: 2px 0 8px 24px;
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--mute);
}
.lst-help-a p { margin: 0 0 9px; }
.lst-help-a p:last-child { margin-bottom: 0; }
.lst-help-a strong { color: var(--dim); font-weight: 600; }
.lst-help-a em { color: var(--dim); font-style: italic; }

/* ------------------------------------------------- the 40-second read */

.lst-read { margin: 0 0 4px; }

.lst-read-btn { width: 100%; }
.lst-read-btn:disabled { opacity: .45; cursor: not-allowed; }
.lst-read-btn:not(:disabled):hover { border-color: var(--accent); color: var(--accent-lit); }

.lst-read-live { margin-top: 11px; }

.lst-read-say {
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--dim);
}

.lst-read-bar {
  height: 4px;
  margin: 10px 0 6px;
  border-radius: 999px;
  background: var(--line);
  overflow: hidden;
}
/* Width is set per tick from javascript. The transition is just under the
   1s tick so the bar is always moving rather than stepping. */
.lst-read-fill {
  display: block;
  height: 100%;
  width: 0;
  background: var(--accent);
  transition: width .9s linear;
}
.lst-read-fill.is-over { background: var(--warn); }

.lst-read-meta {
  display: flex;
  gap: 6px;
  align-items: baseline;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute);
}
.lst-read-clock { color: var(--accent-lit); }

.lst-read-err {
  margin-top: 11px;
  padding: 9px 11px;
  border: 1px solid rgba(255, 46, 66, .32);
  border-radius: 6px;
  background: var(--bad-bg);
  color: var(--bad);
  font-size: 12.5px;
  line-height: 1.6;
}

/* --------------------------------------------------------- drift line */

.lst-drift {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin: 14px 0 0;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: rgba(255, 255, 255, .02);
  font-size: 12.5px;
  line-height: 1.6;
}
.lst-drift-h {
  font-family: var(--ui);
  font-size: 10.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--faint);
}
.lst-drift-v { color: var(--dim); }
.lst-drift-v strong { color: var(--text); font-weight: 600; }

.lst-drift.is-ok   { border-color: rgba(58, 212, 106, .30); background: var(--ok-bg); }
.lst-drift.is-warn { border-color: rgba(255, 176, 46, .30); background: var(--warn-bg); }
.lst-drift.is-bad  { border-color: rgba(255, 46, 66, .30);  background: var(--bad-bg); }
.lst-drift.is-bad .lst-drift-v { color: var(--bad); }

/* ------------------------------------------------------------ coverage */

/* Five short counts, narrower than the default .stats track. */
.lst-cov-stats {
  grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
  margin: 2px 0 14px;
}

/* The sentence coverage.summary_line wrote, printed verbatim. Set apart from
   the numbers above it because it is the one line an operator can act on
   without reading any of them. Hidden when empty rather than leaving a rule
   floating over nothing. */
.lst-cov-summary {
  margin: 12px 0 0;
  padding: 10px 12px;
  border-left: 2px solid var(--accent);
  border-radius: 0 6px 6px 0;
  background: var(--accent-bg);
  font-size: 12.5px;
  line-height: 1.65;
  color: var(--dim);
}
.lst-cov-summary:empty { display: none; }

/* A table cell holding a real state that is not a number - "unconfirmed",
   "3 / 11". Not .stbl-unknown: that one is dashed because it means NO SOURCE,
   and these have one. */
.lst-cell-soft { color: var(--mute); cursor: help; }

/* ============================================================
   ACCOUNTS - state filter and the two-state row
   Appended 2026-09-02. front_accounts.html was re-ordered: the roster and its
   state filter lead the page, the NEEDS YOU / RECOVERING panels sit below it
   side by side, and the old Running / Ready panel is gone - its one fact is a
   filter option now, and its one action is the per-row "Open" button. Nothing
   above this line is modified; these rules fill the gaps the existing
   vocabulary (.mock-panel/.ph, .row-btn, .pill, .field-note) has no rule for.
   ============================================================ */

/* The filter leads the roster, so it sits above the search fields and reads as
   one control rather than four buttons: a label, then the options, wrapping on
   a narrow viewport instead of forcing the panel to scroll. */
.acct-state-filter {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 0 0 12px;
}

.acct-state-lead {
  font-family: var(--ui);
  font-size: 11.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--faint);
  margin-right: 2px;
}

/* A .row-btn first - same border, radius and type as every other action on
   this page - carrying only what a toggle needs on top of it. */
.acct-state-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

/* The picked option. Border and text, not a filled block: the roster below is
   the thing being read, and a solid swatch up here would out-shout it. */
.acct-state-btn.is-on {
  border-color: var(--accent);
  background: var(--accent-bg);
  color: var(--text);
}
.acct-state-btn.is-on:hover { border-color: var(--accent); color: var(--text); }

/* The count. Monospace so four options line up as a column of numbers rather
   than drifting with the label width. */
.acct-state-n {
  font-family: var(--mono);
  font-size: 10.5px;
  padding: 1px 6px;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--mute);
  white-space: nowrap;
}

/* Coloured by what the state means for the next run, matching the two panels
   below - red is the one that needs a person, amber the one that is waiting,
   green the one that needs nothing. "All" stays neutral: it is not a state. */
.acct-state-btn[data-state="ok"] .acct-state-n {
  color: var(--ok); border-color: rgba(58, 212, 106, .32); background: var(--ok-bg);
}
.acct-state-btn[data-state="needs_you"] .acct-state-n {
  color: var(--bad); border-color: rgba(255, 46, 66, .32); background: var(--bad-bg);
}
.acct-state-btn[data-state="recovering"] .acct-state-n {
  color: var(--warn); border-color: rgba(255, 176, 46, .32); background: var(--warn-bg);
}

/* NEEDS YOU beside RECOVERING ON ITS OWN, 50/50. They are read against each
   other - "is this mine to fix, or is it just waiting" - which is the whole
   reason they are two panels and not one list, and side by side is how that
   comparison is made in one look. One column below 900px, where two columns of
   email addresses and countdown sentences stop being readable. */
.acct-state-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 12px;
  align-items: start;
  margin-bottom: 12px;
}

/* .mock-panel carries its own bottom margin for a stacked page; inside the
   grid the gap owns the spacing, so the two columns end level. */
.acct-state-split > .mock-panel { margin-bottom: 0; }

@media (max-width: 900px) {
  .acct-state-split { grid-template-columns: minmax(0, 1fr); }
}

/* ---------------------------------------------------------------------------
   CONTRAST, measured 2026-09-02 against the page background #060305.

   --text and --bone were #ede6e4 - rgb(237,230,228), a warm off-white that
   reads as light pink on this near-black red-tinted ground. Now #ffffff.
   White text is white.

   --faint was #4a3d41, which measures 1.99:1. That fails WCAG AA for body
   text (4.5:1) and fails even the large-text floor (3.0:1) - it was not dim,
   it was illegible. Now #9a8d90 at 6.0:1.

   Still outstanding and NOT changed here, because --accent is used for
   borders and fills as well as text and retoning it blind would alter the
   whole palette: --accent / --blood (#e10e24) measure 4.19:1, which fails AA
   for body text. Anywhere that colour carries WORDS should use --accent-lit
   (#ff2e42, 5.59:1) instead. That is an audit of call sites, not a token swap.
   --------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
   2FA setup: recovery codes and the TOTP secret.

   setup_2fa.html:31 and :64 render both inside <pre class="auth-codes">, and
   the class had NO rule in any stylesheet - the gate's class check caught it
   after feature/operator-2fa merged. Unstyled, a <pre> of recovery codes
   inherits the page font stack and no wrapping, which is the worst possible
   presentation for a string somebody has to transcribe exactly once and can
   never see again.

   Monospace so 0/O and 1/l are distinguishable, selectable, and wrapped rather
   than overflowing - a code cut off at the panel edge is a code that gets
   written down wrong.
   --------------------------------------------------------------------------- */
.auth-codes {
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.9;
  letter-spacing: .08em;
  color: var(--text);
  background: var(--bg-black);
  border: 1px solid var(--glass-line);
  border-radius: 6px;
  padding: 12px 14px;
  margin: 10px 0 14px;
  white-space: pre-wrap;
  word-break: break-all;
  user-select: all;
}

/* ============================================================
   Shared FAB menu — app/templates/_fab.html

   The three pages carried three single-purpose FABs (#openAdd, #openAddAcctBtn,
   #runsFab). One button now opens a menu of four actions, and the partial that
   draws it is page-agnostic, so everything below is keyed to classes only that
   partial uses. Nothing here re-declares a .fab property: the button itself is
   the existing .fab in every respect, including the primary-button colour rule
   at :2457 (white on --accent, 4.90:1). No colour is set on .fab here.
   ============================================================ */

/* Anchored to the viewport with the SAME right offset as .fab (:1354) and a
   bottom offset of the button's own height plus a 10px gap, so the panel sits
   directly on top of the button it belongs to at every clamp() breakpoint.
   The two values are the ones .fab already uses; they are repeated rather than
   tokenised because .fab is not ours to edit. z-index 41 puts it over the
   button (40) and under the warm-up terminal (45) and any card (.modal, 50). */
.fab-menu {
  position: fixed;
  right: clamp(18px, 3vw, 34px);
  bottom: calc(clamp(18px, 3vw, 30px) + var(--btn-h) + 10px);
  z-index: 41;
  width: min(300px, calc(100vw - 32px));
  display: flex;
  flex-direction: column;
  background: var(--glass-deep);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-line);
  box-shadow: 0 18px 44px rgba(0, 0, 0, .62);
  /* Matches the button's corner (--radius-btn), so the open menu reads as the
     trigger's own surface rather than a stray card. Was a clip-path polygon
     cutting the bottom-right corner; it went with the button's. */
  border-radius: var(--radius-btn);
}

/* Deliberately NOT filled with --accent on hover. The primary-button rule pairs
   --accent with #fff at 4.90:1, which is the floor with nothing to spare; a
   two-line item whose second line is --mute would drop well under it. The state
   change is carried by a raised panel tone and a 2px accent edge instead, so no
   text on this menu ever sits on the red. */
.fab-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  background: transparent;
  border: 0;
  border-left: 2px solid transparent;
  border-bottom: 1px solid var(--line-soft);
  padding: 10px 13px;
  font-family: var(--ui);
  color: var(--text);
  cursor: pointer;
  transition: background .14s, border-color .14s;
}
.fab-menu-item:last-child { border-bottom: 0; }
.fab-menu-item:hover,
.fab-menu-item:focus {
  background: var(--panel-2);
  border-left-color: var(--accent);
  outline: none;
}
/* Keyboard focus has to be distinguishable from hover: the menu is driven by
   arrow keys, and a ring is the only thing that says where the cursor is. */
.fab-menu-item:focus-visible {
  outline: 2px solid var(--accent-lit);
  outline-offset: -2px;
}

.fab-menu-label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .04em;
}

/* Where the action goes — "here" or "goes to Accounts". Written by the partial
   when the menu opens, because whether an action is local is a question about
   the page's DOM, not about this stylesheet. */
.fab-menu-go {
  font-weight: 400;
  font-size: 11px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--accent-lit);
  margin-left: 6px;
}

.fab-menu-sub {
  display: block;
  font-size: 11.5px;
  line-height: 1.35;
  color: var(--mute);
  margin-top: 2px;
}

/* A second floating button on the same page — front_runs.html's #runsFab, which
   is not a duplicate of the shared one: it is context-dependent (Start warm-up
   on the Queue tab, Create job on Saved jobs, hidden on the other two), so it
   cannot be folded into a menu that is identical on every page. Raised by the
   shared button's height plus the same 10px gap the menu uses, which is why the
   two stack cleanly instead of overlapping. Written as .fab.fab-stack so it
   beats .fab's own bottom whatever order these blocks end up in. */
.fab.fab-stack {
  bottom: calc(clamp(18px, 3vw, 30px) + var(--btn-h) + 10px);
}

/* The exclusion-state block in the review form. A rule, not an inline style, so
   it can be found: this is the one part of that card that states a fact about
   the system rather than collecting input. */
.fab-rev-safety {
  border-left: 2px solid var(--line);
  padding-left: 11px;
  margin-top: 4px;
}

/* Motion here is decoration on a state change that is already carried by colour
   and position, so there is nothing to lose by dropping it. */
@media (prefers-reduced-motion: reduce) {
  .fab-menu-item { transition: none; }
}
/* Enrolment QR. White plate under it on purpose: a QR inverted by a dark
   theme is unreadable to most phone cameras, and the SVG itself only carries
   the module colours, not a background. */
.auth-qr{display:flex;justify-content:center;margin:14px 0;}
.auth-qr svg{background:#fff;padding:10px;border-radius:8px;max-width:100%;height:auto;}

/* Clearance under TWO stacked floating buttons. .fab-pad (:1528) reserves 88px
   - one 32px button plus its 30px bottom offset, plus slack - and that is
   correct everywhere except front_runs.html, which keeps its contextual
   #runsFab above the shared one. Measured from the same offsets .fab uses:
   30 bottom + 32 (shared button) + 10 (gap, the same one .fab-menu uses)
   + 32 (#runsFab) = 104px to the top of the upper button, so 88 left the last
   panel's own row buttons underneath it at the bottom of the scroll. 120 clears
   it with the same slack .fab-pad carries.

   A second class rather than a bigger .fab-pad: the other three pages have one
   button and should not grow a dead band. Nothing above is edited - this rule
   is later in the file, so it wins on order at equal specificity. */
.fab-pad-2 { padding-bottom: 120px; }

/* ---------------------------------------------------------------------------
   setup_2fa.html's wide column.

   .auth-split and .auth-split__aside are defined in auth-2fa.css; only
   .auth-split__main was missed, and the gate's class check caught it. The rule
   lives HERE rather than beside its siblings because auth-2fa.css is being
   written by another session concurrently and an edit there loses the race -
   move it back when that settles.

   min-width: 0 is the whole point. A grid item defaults to min-width auto, so
   the <pre class="auth-codes"> inside refuses to shrink below its own text and
   pushes the QR column narrower than the 3fr it was allotted. That is the same
   reasoning auth-2fa.css writes minmax(0, ...) for on the TRACK; the track
   rule does not reach the item.
   --------------------------------------------------------------------------- */
.auth-split__main { min-width: 0; }

/* ---------------------------------------------------------------------------
   Brand lockup: wordmark over version.

   .brand is display:flex in row direction, so _brand.html's two text elements
   sat SIDE BY SIDE - the version appeared next to the wordmark rather than
   under it, and when the sidebar collapsed it pushed the globe past the left
   edge. Stacking them in one column item fixes both: the version sits where it
   was asked to sit, and the collapsed state has a single element to hide.

   Was inline-styled with var(--faint) while front.css and layout.css were held
   by other work. --faint is now 6.44:1 rather than the 1.99:1 it measured when
   that stamp was written, so the version is legible as well as placed.
   --------------------------------------------------------------------------- */
.brand-stack {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.brand-ver {
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: .10em;
  color: var(--faint);
  white-space: nowrap;
}

/* Change-password link in the sidebar footer, beside Sign out. Quiet by
   default because it is a rare action sitting next to a common one. */
.side-foot-link {
  font-size: 11.5px;
  color: var(--mute);
  text-decoration: none;
  padding: 2px 0;
}
.side-foot-link:hover { color: var(--text); text-decoration: underline; }

/* Separates one business's group (its .mset header plus its own nested
   locations table) from the next on Listings. Only the gap BETWEEN businesses
   is needed here: .mset already draws the header's own dashed border, and
   everything the group wraps - .mset, .tbl-wrap, .stbl - is styled already. */
.lst-biz-group + .lst-biz-group {
  margin-top: 18px;
  padding-top: 18px;
  border-top: 1px solid var(--line-soft);
}

/* The Proxy cell was the widest column on the roster - measured 361px of a
   1585px table in a 1432px viewport, which is most of the 153px that made the
   table scroll sideways. It concatenates city, region and a spelled-out country
   ("los_angeles, california United States"), so it grows with the longest of
   the three. Capped and truncated; the full string stays in the cell's title. */
.acct-proxy {
  display: inline-block;
  max-width: 118px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}

.dash-split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 22px;
  align-items: start;
}
.dash-split > .panel { margin: 0; }

/* Row to row. gap only spaces the panels INSIDE one grid, and .panel's own
   margin is zeroed above, so a second .dash-split under the first sat flush
   against it - the dashboard read as one tall box with internal rules rather
   than as separate rows. The same 22px the grid uses between columns, so the
   spacing is square in both directions. */
.dash-split + .dash-split { margin-top: 22px; }
@media (max-width: 860px) { .dash-split { grid-template-columns: 1fr; } }

/* A footnote that follows a panel contributed nothing to the gap below it.
   Every other sibling in that column carries the same 22px - .stats:638,
   .panel:673 - while .note:748 is margin:0, so the note kept its 22px above
   and sat flush against whatever came next. On the dashboard that is the
   .dash-split grid (front.html:55), which declares no margin of its own.
   Only where something actually follows: a trailing note (front.html:90) has
   no gap to carry, and main's own 44px bottom padding (front.css:605) is the
   spacing there. Scoped to `.panel + .note` so the two notes inside
   front_runs.html - which follow a div, not a panel - keep their own spacing,
   including the inline margin-top on #schedNote. */
.panel + .note:not(:last-child) { margin-bottom: 22px; }

/* Change password sat directly against Sign out in .side-foot, which is a
   misclick between "edit a credential" and "end the session". A rule and a
   real gap, so the two are not one visual block. */
.side-foot form {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--line-soft);
}

/* ---- city code picker -------------------------------------------------- */
/* A typeahead over data/city_codes.json rather than a bare text input: the
   vendor's own city list answers HTTP 500 for every region tried, so the codes
   cannot be validated at submit time - the only defence against a typo is
   picking from a list that says which codes are PROVEN by a working row. */
.city-pick { position: relative; }
.city-pick-list {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 40;
  max-height: 260px;
  overflow-y: auto;
  background: var(--modal-bg);
  border: 1px solid var(--glass-line);
  border-radius: var(--radius);
  box-shadow: 0 12px 32px rgba(0, 0, 0, .55);
}
.city-pick-opt {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  padding: 8px 12px;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line-soft);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.city-pick-opt:last-child { border-bottom: 0; }
.city-pick-opt:hover,
.city-pick-opt.is-active { background: var(--ash); }
/* PROVEN vs candidate is the whole point of the control, so it is a visible
   badge and not a tooltip. */
.city-pick-flag {
  flex: none;
  font-size: 10px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--mute);
}
.city-pick-opt.is-proven .city-pick-flag { color: var(--ok); }
.city-pick-empty { padding: 10px 12px; color: var(--mute); font-size: 13px; }

/* ---- unblock form ------------------------------------------------------ */
.unblock-why {
  border-left: 2px solid var(--warn);
  padding: 8px 0 8px 12px;
  margin: 0 0 14px;
  color: var(--dim);
  font-size: 13px;
}
.unblock-mode {
  font-size: 11px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--mute);
  margin: 0 0 6px;
}

/* "Make default" sits inside a table cell next to the location name, so it is
   quieter than a row action: the name is what the eye is scanning for. */
.lst-make-default {
  font-size: 10.5px;
  padding: 1px 7px;
  opacity: .72;
}
.lst-make-default:hover { opacity: 1; }

/* A PLAN DAY IS NOT A SCHEDULED RUN, so it must not look like one. A schedule
   occurrence is one firing at an instant; a plan day is a count of reviews owed
   on a calendar date. .cal-run carries the accent fill and red border those
   firings use, so a plan reusing it unchanged read as a scheduled run at a
   glance - which is the one thing this rendering exists to prevent.

   Dashed and unfilled, reusing the "different KIND of thing" convention already
   on this surface rather than inventing a hue: the palette is red on black and
   has no free colour that would not read as a severity. */
.cal-run.is-plan {
  background: transparent;
  border: 1px dashed var(--line);
}
.cal-run.is-plan:hover {
  border-color: var(--dim);
  background: rgba(255, 255, 255, .04);
}
.cal-run.is-plan .cal-dot { background: var(--dim); }
/* Not a button - it carries no schedule data-* and opens nothing of its own. */
.cal-run.is-plan .cal-run-main { cursor: default; }

/* Near-full-width card, for a modal whose body is side-by-side fields rather
   than a paragraph. No vw unit: .modal already reserves 16px of padding each
   side and a vw length counts the scrollbar that padding does not, so 96vw
   overflows by the scrollbar's width on a page long enough to have one. 100%
   resolves against .modal's content box, which has that padding subtracted
   already; the px cap stops fields stretching to unreadable line lengths. */
.modal-card.is-full { max-width: 1600px; }


/* ---------------- rail log entries ---------------- */

/* One entry, three lines: when, what, who. The rail log used to render the
   shared .row - a flex line with the message in .label and a .row-tail of
   fixed-width badges. At the rail's 240-640px (front_base.html:153) the
   fixed side won and .label's overflow-wrap:anywhere (front.css:714) wrapped
   the message a character at a time. These rules are scoped to their own
   classes rather than overriding .row, because .row is the list line on every
   other panel on the front and none of them are in a 240px column. */
.log-entry {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 11px 20px 12px;
  border-bottom: 1px solid var(--line-soft);
  font-size: 13.5px;
}

.log-entry:last-child { border-bottom: 0; }

/* Line 1 - when, and the severity when the line carries one. The badge sits
   on this line, not beside the message, so nothing competes with the message
   for width. */
.log-when {
  display: flex;
  align-items: center;
  gap: 8px;
}

.log-stamp {
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: .04em;
  color: var(--mute);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Line 2 - the message, across the full width of the rail. break-word, NOT
   anywhere: a word longer than the column is still broken, but only once the
   line has genuinely run out, which is the difference between a wrapped
   sentence and the one-character column this replaced. */
.log-msg {
  color: var(--text);
  line-height: 1.45;
  overflow-wrap: break-word;
}

/* Line 3 - which account. anywhere is right here and wrong above: an address
   is one unbroken token, and breaking it is better than letting it overflow
   a 240px rail. */
.log-who {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--dim);
  overflow-wrap: anywhere;
}

/* A line that arrived on the last poll. The wash is a brightness change, not
   a hue: --bad and --warn already mean failed and warning on the badge two
   inches away, and a red row would read as a third severity rather than as
   "this one is new". The left bar is --accent, the same mark the active nav
   item carries - the product pointing at something, not grading it. */
.log-entry.is-new {
  animation: log-arrive 6s ease-out both;
}

@keyframes log-arrive {
  0%, 10% {
    background: rgba(255, 255, 255, .075);
    box-shadow: inset 3px 0 0 var(--accent);
  }
  100% {
    background: transparent;
    box-shadow: inset 3px 0 0 transparent;
  }
}

/* No animation, but still marked: the point is that the reader can tell the
   line is new, and that survives without motion. The script strips .is-new
   after the same interval either way. */
@media (prefers-reduced-motion: reduce) {
  .log-entry.is-new {
    animation: none;
    background: rgba(255, 255, 255, .075);
    box-shadow: inset 3px 0 0 var(--accent);
  }
}

/* Three across, for the row of section cards. Same grid as .dash-split, one
   more column: these carry two or three short rows each, so a half-width panel
   would leave most of its box empty. It collapses with its parent - .dash-split
   already turns single-column at the narrow breakpoint and this inherits that,
   because the override is only on the column count. */
.dash-split.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* The panel headings on those cards are links to the page each summarises. No
   underline and no accent: they are headings first, and the whole row already
   reads as a way in. */
.panel header h2 a { color: inherit; text-decoration: none; }
.panel header h2 a:hover { color: var(--accent-lit); }

/* Accounts roster - the selection action bar.
 *
 * LEFT TO RIGHT, WRAPPING. The two claims this block used to make were both
 * measured false. It said the bar "fits without wrapping or a sideways
 * scrollbar", and it said it "scrolls sideways rather than stacking" - but the
 * rule declared no overflow property at all, so the computed value was
 * `visible` and nothing scrolled. With flex-wrap:nowrap and flex:0 0 auto on
 * every button (below), the eleven controls measure 1678px against a 704px box
 * at a 1440 viewport: seven of them painted outside the bar, up to 588px past
 * it, and document.body.scrollWidth ran 588px wider than
 * documentElement.clientWidth - the whole page scrolled sideways.
 *
 * Wrap, not overflow-x:auto, and the difference is measured rather than
 * preferred: overflow on one axis forces the other from `visible` to `auto`
 * (CSS Overflow 3), and [data-tip]::after below is absolutely positioned at
 * top:calc(100% + 6px) - BELOW this box. Under overflow-x:auto the bar reported
 * scrollHeight 113 against clientHeight 48, i.e. 65px of tooltip sitting in its
 * scrollable overflow, clipped and behind a second scrollbar. Wrapping keeps
 * overflow `visible`, so the tips still paint over the page.
 *
 * Wrapping does not cost the ordering the old comment cared about: flex line
 * breaking follows document order, so the controls keep their sequence. What it
 * costs is height, and only when the viewport cannot hold the row - measured
 * 50px at 2560, 88px at 1920 and 1600, 126px at 1440, 164px at 1280.
 *
 * It is toggled with the hidden attribute (syncSelection sets el.hidden), and
 * the base sheet's [hidden]{display:none!important} beats this display:flex -
 * that is why the toggle is an attribute and not an inline style here. */
.acct-selbar {
  display: flex;
  align-items: center;
  /* Shorthand: this is the row gap between wrapped lines as well as the gap
     between controls, so wrapping needs no second declaration. */
  gap: 6px;
  flex-wrap: wrap;
  margin: 0 0 12px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  background: var(--panel-2, var(--panel));
}

/* Single-line buttons: the glyph then the label, detail on hover. */
.acct-selbar > button {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 10px;
  white-space: nowrap;
}

.acct-selbar-n {
  flex: 0 0 auto;
  margin-right: 4px;
  font-size: 12px;
  color: var(--dim);
  white-space: nowrap;
}

/* Buttons keep their intrinsic width - the default flex-shrink would squeeze
 * the labels out rather than move a control to the next line. 0 0 auto is what
 * makes the wrap above break lines instead of compressing them. */
.acct-selbar > button {
  flex: 0 0 auto;
}

/* Icon and label sit on one baseline; .ic holds the injected <svg>, .lb holds
 * the text bulk() rewrites with a running count. */
.acct-selbar .ic {
  display: inline-flex;
  align-items: center;
  margin-right: 6px;
}

.acct-selbar .ic > svg {
  display: block;
}

/* A bar mid-run is not idle: every button is disabled and the pressed one
 * counts up, so the cursor says so too. */
.acct-selbar[aria-busy="true"] {
  cursor: progress;
}

/* The account id, shown ahead of the name and address on the roster.
   Every account carries a permanent number and every log row is stamped with
   one, but the screens led with the address - so a problem was reported as
   "cabajesjed225@gmail.com failed" rather than "#48 failed". These addresses
   are near-identical strings nobody can hold in their head or read aloud.
   Mono and tabular so the column of numbers aligns and scans. */
.acct-id {
  display: block;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .04em;
  color: var(--dim);
}

/* Review history on the account card. Read-only: these rows are written when a
   review is accepted, never from this screen.
   The award column can read "not read" rather than a number - Google shows the
   Local Guide award once, on the confirmation panel, and a null is "never read"
   rather than zero. The two must not look alike here either. */
.acct-hist {
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 12px 14px;
  margin-bottom: 14px;
  background: var(--panel-2);
}
.acct-hist-summary {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin: 8px 0 10px;
}
.acct-hist-k {
  font-family: var(--ui);
  font-size: 11px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--mute);
}
.acct-hist-v {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 15px;
  font-weight: 700;
  color: var(--ok);
}
.acct-hist-rows { display: flex; flex-direction: column; gap: 1px; }
.acct-hist-row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 12px;
  align-items: baseline;
  padding: 6px 0;
  border-top: 1px solid var(--line-soft);
  font-size: 12.5px;
}
.acct-hist-when {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--mute);
}
.acct-hist-pts {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--ok);
  min-width: 4ch;
  text-align: right;
}
.acct-hist-unread { color: var(--mute); font-style: italic; }
.acct-hist-empty { color: var(--mute); border-top: none; }

/* ---- roster: the progress cell -----------------------------------------
   Two bars, not one, because lifecycle.ready_to_advance is two conditions and
   both must pass - sessions AND days, "ten sessions in one afternoon is not
   ten days of history". A single merged bar would let one full half hide an
   empty other. */
.prog-fig {
  display: block;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.prog-bars {
  display: flex;
  gap: 3px;
  margin-top: 3px;
  min-width: 72px;
}

.prog-bar {
  position: relative;
  flex: 1;
  height: 3px;
  background: var(--line);
  overflow: hidden;
}

.prog-bar > i {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  background: var(--dim);
}

/* Full means the requirement is met; anything short stays neutral rather than
   colouring a row that is simply still growing. */
.prog-bar > i[style*="width:100%"] { background: var(--ok, #3fb950); }

/* A business name is a link to that business alone. Styled as text until
   hovered: it sits in a heading, and underlining every group title would read
   as a page full of links rather than a list with addresses. */
.lst-permalink { color: inherit; text-decoration: none; }
.lst-permalink:hover { text-decoration: underline; }

/* The row or group a slug URL landed on. Fades rather than persists - it marks
   where the reader arrived, it does not mark a state of the data. */
.lst-biz-group.is-focused,
tr.is-focused > td { animation: focus-fade 2.4s ease-out 1; }

@keyframes focus-fade {
  from { background: var(--accent-soft, rgba(225, 14, 36, .16)); }
  to { background: transparent; }
}

/* ---- action tooltips ---------------------------------------------------
   data-tip rather than title: a native tooltip waits on a delay the browser
   owns, cannot be styled, and truncates differently everywhere - and these
   strings are long on purpose, because each names what its endpoint does
   rather than restating the label beside it.

   The element setting data-tip must establish a stacking context of its own,
   which the position:relative below does; the card is a ::after so it never
   affects layout. */
[data-tip] { position: relative; }

[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  left: 0;
  top: calc(100% + 6px);
  z-index: 40;
  width: max-content;
  max-width: 320px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  background: var(--panel-2, var(--panel));
  color: var(--fg, var(--dim));
  font-family: var(--ui);
  font-size: 11.5px;
  font-weight: 400;
  line-height: 1.45;
  letter-spacing: 0;
  text-transform: none;
  text-align: left;
  white-space: normal;
  box-shadow: 0 8px 22px rgba(0, 0, 0, .38);
  opacity: 0;
  visibility: hidden;
  transition: opacity .12s ease-out;
  pointer-events: none;
}

/* Focus as well as hover, or the explanation is mouse-only. */
[data-tip]:hover::after,
[data-tip]:focus-visible::after {
  opacity: 1;
  visibility: visible;
}

/* The last three controls used to be right-anchored here, on the premise that
   a tip on them "would overflow the viewport to the right". That premise was
   positional and the bar now wraps, so those three start the LAST LINE at its
   left edge: right-anchoring pushed their tips 210px left of their own button,
   over the nav beside the table. Measured with the tips forced visible and
   their real ::after boxes read at 1024, 1152, 1280, 1440, 1600, 1920 and 2560:
   with the override gone, no tip crosses either viewport edge at any of them. */

@media (prefers-reduced-motion: reduce) {
  [data-tip]::after { transition: none; }
}

/* ---- dashboard date-range control -------------------------------------- */
.dash-head-right { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }

.range-bar {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: var(--radius-btn, 8px);
  background: var(--panel-2, var(--panel));
}

.range-opt {
  font-family: var(--ui);
  font-size: 12px;
  letter-spacing: .02em;
  padding: 4px 10px;
  border: 0;
  border-radius: var(--radius-btn, 8px);
  background: transparent;
  color: var(--dim);
  cursor: pointer;
}
.range-opt:hover { color: var(--fg, var(--ink)); }
.range-opt.on { background: var(--accent); color: #fff; }

/* Hard-refresh button in the dashboard header. Same size/typeface as the range
   buttons, but bordered so it reads as an action rather than a toggle. */
.refresh-btn {
  font-family: var(--ui);
  font-size: 12px;
  letter-spacing: .02em;
  padding: 4px 10px;
  border: 1px solid var(--glass-line);
  border-radius: var(--radius-btn, 8px);
  background: var(--glass);
  color: var(--dim);
  cursor: pointer;
  white-space: nowrap;
  transition: color .12s, border-color .12s;
}
.refresh-btn:hover { color: var(--fg, var(--ink)); border-color: var(--accent); }

/* The Accounts page head's right-hand group: selection count, Refresh, and the
   time of the last successful read. A row rather than three loose children,
   because .page-head is space-between - without a wrapper the middle item is
   pushed to the middle of the header instead of sitting beside its button. */
.acct-head-right {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
/* A refresh in flight must LOOK unavailable, not merely ignore the click -
   the handler already guards re-entry, and a button that swallows a press
   with no visible change reads as a broken button and gets pressed again. */
.refresh-btn[disabled] { opacity: .55; cursor: progress; border-color: var(--line); }

.range-compare {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--ui);
  font-size: 12px;
  color: var(--dim);
  padding-left: 6px;
}

.range-custom { display: inline-flex; align-items: center; gap: 4px; }
.range-custom input[type="date"] {
  font-family: var(--ui);
  font-size: 12px;
  padding: 3px 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-btn, 8px);
  background: var(--panel-2, var(--panel));
  color: var(--fg, var(--ink));
}
.range-dash { color: var(--dim); }

/* ── business-search result list (front_listings.html, #lkResults) ─────────

   Replaces the .mset/.mtxt/.lbl/.sub rows this list borrowed from the member
   sets. Three literal reasons for its own block rather than reuse:

   1. `.lbl` is a BARE selector (:2586) carrying text-transform:uppercase and
      letter-spacing:.14em. `.mset .lbl` (:989) sets only font-size and colour,
      so it overrode neither. A result row's label is a business NAME read off
      Google and written verbatim into #b_name on a pick, so the rendered text
      and the stored value disagreed - "KATZ'S DELICATESSEN" on screen against
      `Katz's Delicatessen` in the field. .lst-pick-name is not .lbl, so the
      uppercase rule does not reach it.

   2. The row is a <button>. It was a <div> whose only interactive element was
      a small trailing button, so the name and address - the part being read -
      were inert, and the row could not be reached by Tab or activated by
      Enter. A button needs the appearance reset below; .mset never did.

   3. The list needs states .mset has no vocabulary for: a busy row while the
      metered search runs, and a chosen row that stays on screen after a pick.
   ------------------------------------------------------------------------ */

.lst-pick-head {
  font-size: 12px;
  color: var(--mute);
  padding: 2px 0 4px;
}

.lst-pick-busy {
  font-size: 12px;
  color: var(--dim);
  font-family: var(--ui);
  padding: 9px 0;
  border-bottom: 1px dashed var(--line);
}

.lst-pick {
  /* Appearance reset: this is a <button>, and the UA sheet would otherwise
     give it a background, a centred single-line label and its own font. */
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  text-align: left;
  background: transparent;
  border: 0;
  border-bottom: 1px dashed var(--line);
  border-radius: 0;
  padding: 9px 6px 9px 4px;
  margin: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  transition: background-color .12s, border-color .12s;
}

.lst-pick:last-of-type { border-bottom: 0; }
.lst-pick:hover:not(:disabled) { background: var(--ash); }
.lst-pick:focus-visible {
  outline: none;
  background: var(--ash);
  box-shadow: inset 2px 0 0 var(--accent);
}
.lst-pick:disabled { cursor: not-allowed; opacity: .5; }

.lst-pick-txt {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.lst-pick-name {
  font-size: 13px;
  color: var(--text);
  /* Verbatim: this string is what a pick writes into #b_name. */
  text-transform: none;
  letter-spacing: normal;
}

.lst-pick-sub {
  font-size: 11.5px;
  color: var(--mute);
  font-family: var(--mono);
}

.lst-pick-cta {
  flex: none;
  font-family: var(--ui);
  font-size: 12px;
  color: var(--dim);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 4px 10px;
}

.lst-pick:hover:not(:disabled) .lst-pick-cta { border-color: var(--accent); color: var(--text); }

/* The row that was picked stays rendered instead of the list being emptied,
   so a wrong pick costs one place-details read rather than a second search. */
.lst-pick-chosen { background: var(--ash); box-shadow: inset 2px 0 0 var(--accent); }
.lst-pick-chosen .lst-pick-cta { border-color: var(--accent); color: var(--text); }

/* The business picker (#pickModal) opens ON TOP of #listModal, which is also a
   .modal. Both would take z-index:50 from front.css:1286, leaving the stacking
   order to depend on which one appears later in the document - not a contract
   the markup states anywhere. 60 states it. */
.lst-pickmodal { z-index: 60; }

/* #listModal is .is-full (max-width 1600px, :3840). The picker is a list of a
   handful of rows and takes .modal-card's own 620px, so it reads as a dialog
   over that one rather than a second full-width surface covering it. */
.lst-pickmodal .modal-body { padding-top: 12px; }
.lst-pickmodal #lkResults { min-width: 0; }
