/* ==========================================================================
   JCF Campaign Studio - design system

   This file is the console half of docs/UI-SPEC.md. Sections 2 to 9 of that
   document are the contract: colour, elevation, radius, the 4px spacing scale,
   the type ramp, the icon rules, motion and controls. Where a value here and
   the spec disagree, the spec is right and this file is wrong.

   Flat charcoal surfaces, hairline borders, one blue accent spent on an edge, a
   single filled button and the focus ring. No gradient as a surface, no glow,
   no webfont, no icon font, no framework: the box has to render from one jar
   with zero external requests under a default-src 'self' policy.
   ========================================================================== */

:root {
  /* The single most important line for "why is this blue". Without it the user
     agent renders every native control it owns - select popups, date pickers,
     scrollbars, the autofill wash, spin buttons - in its light theme, which is
     blue and white, and no token in this file reaches any of them. There is one
     theme and no prefers-color-scheme block, by section 2. */
  color-scheme: dark;

  /* ---- 2. colour: flat charcoal, never navy, never purple ---- */
  --bg:            #141414;   /* page ground */
  --surface:       #1b1b1b;   /* rail, topbar, sunken areas */
  --panel:         #202020;   /* cards, sheets, list rows */
  --panel-2:       #252525;   /* raised inside a panel */
  --panel-3:       #2c2c2c;   /* hover */
  --panel-4:       #343434;   /* pressed, avatar ground */
  --field:         #262626;   /* input rest */
  --field-focus:   #2b2b2b;   /* input focus */

  --border:        rgba(255, 255, 255, .075);  /* hairline between rows */
  --border-strong: rgba(255, 255, 255, .14);   /* input outline, card edge */
  --border-soft:   rgba(255, 255, 255, .045);  /* inside a panel */

  --text:          #ededed;
  --text-dim:      #b9b9b9;
  --text-mute:     #949494;
  /* Measured at 3.96:1 on --panel, so this is a graphic and edge colour, not a
     text colour. Used as body text it is a contrast failure, which is why the
     nav group headings and the chart empty line moved up to --text-mute. */
  --text-faint:    #7d7d7d;

  --primary:       #2f6fed;
  /* Section 2 gives this as #3a7bf5, a lift, and the spec is wrong about it.
     White on #2f6fed measures 4.55:1, which clears AA with 0.05 to spare, so
     any hover lighter than the rest state fails: #3a7bf5 measures 3.94:1 and a
     primary button therefore drops under AA for exactly as long as a pointer is
     over it, which is exactly when it is about to be pressed. Darkening the
     label was the other way out and it is not one - a filled accent button with
     a dark label is a different button. So the hover darkens instead: #2b66da
     is the rest colour at 92% and measures 5.23:1. Its luminance sits 0.030
     below the rest state, against the 0.036 the lift sat above it, so the state
     change is the same size, just in the direction that keeps the label
     readable while it is being read. */
  --primary-hover: #2b66da;
  --primary-fg:    #7aa8ff;   /* accent text on dark, 6.88:1 on --panel */
  --success:       #1f9d55;   --success-fg: #4cc38a;
  --warning:       #d99e0b;   --warning-fg: #e3b341;
  --danger:        #e0483c;   --danger-fg:  #f2776b;
  --accent:        #19a7a0;   /* second series colour in charts only */

  /* Derived, not in section 2. The status pills need a second blue so that
     Scheduled and Sending are not the same swatch, and the tinted grounds are
     written once here rather than as eight separate rgba() literals. */
  --primary-3:     #82b1ff;
  --primary-soft:  rgba(47, 111, 237, .14);
  --tint-primary:  rgba(47, 111, 237, .12);
  --tint-success:  rgba(31, 157, 85, .16);
  --tint-warning:  rgba(217, 158, 11, .14);
  --tint-danger:   rgba(224, 72, 60, .12);
  --tint-neutral:  rgba(255, 255, 255, .06);

  /* ---- 3. elevation: a border plus a shadow, never a lighter fill alone ---- */
  --e0: none;
  --e1: 0 1px 2px rgba(0, 0, 0, .32);    /* cards, rows */
  --e2: 0 4px 14px rgba(0, 0, 0, .40);   /* dropdown, popover */
  --e3: 0 12px 34px rgba(0, 0, 0, .50);  /* modal, sheet */
  --e4: 0 22px 60px rgba(0, 0, 0, .58);  /* full screen phone sheet */

  /* ---- 4. radius and the 4px spacing scale ---- */
  --r-xs:   5px;    /* pills, badges, nav items */
  --r-sm:   8px;    /* buttons, inputs */
  --r-md:   12px;   /* cards, panels */
  --r-lg:   16px;   /* sheets, phone modals */
  --r-full: 999px;  /* avatars, FAB */

  --sp-1:  4px;  --sp-2:  8px;  --sp-3: 12px;  --sp-4: 16px;  --sp-5: 20px;
  --sp-6: 24px;  --sp-7: 32px;  --sp-8: 40px;  --sp-9: 48px;

  /* ---- 5. type ---- */
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, "SF Mono", "Cascadia Code", "JetBrains Mono", Consolas, monospace;

  --fs-display: 30px;   --lh-display: 1.15;
  --fs-h1:      20px;   --lh-h1:      1.25;
  --fs-h2:      16px;   --lh-h2:      1.3;
  --fs-h3:      14px;   --lh-h3:      1.35;
  --fs-body:    13.5px; --lh-body:    1.5;
  --fs-sm:      12.5px; --lh-sm:      1.45;
  --fs-cap:     11.5px; --lh-cap:     1.4;
  --fs-over:    10.5px; --lh-over:    1.3;

  /* Tracking is negative on anything 16px and up, because a system stack set at
     display sizes on a dark ground reads loose at its default spacing. */
  --tr-16: -.01em;  --tr-20: -.02em;  --tr-30: -.03em;

  /* ---- 7. motion. Duration and easing travel together so a transition is
     written as one token and every surface eases the same way. ---- */
  --t-fast: 120ms cubic-bezier(.2, .7, .3, 1);   /* hover, focus, colour */
  --t-base: 200ms cubic-bezier(.2, .7, .3, 1);   /* sheets, panes, expand */
  --t-slow: 320ms cubic-bezier(.2, .7, .3, 1);   /* page-level push */

  /* ---- layout constants ---- */
  --sidebar-w:  220px;
  --drawer-w:   min(86vw, 272px);
  --topbar-h:   56px;
  --main-px:    12px;   /* the phone gutter, mirrored by the topbar bleed rule */
  --main-pt:    12px;

  /* ---- compatibility aliases ----
     console.html and console.js carry inline styles that name the older tokens.
     Those files belong to another commit, so the old names stay defined and
     resolve on to the new scale rather than being deleted out from under them. */
  --radius:        var(--r-sm);
  --radius-sm:     var(--r-sm);
  --radius-xs:     var(--r-xs);
  --shadow:        var(--e1);
  --shadow-pop:    var(--e3);
  --bg-2:          var(--bg);
  --panel-solid:   var(--panel);
  --panel-hover:   var(--panel-3);
  --sidebar-bg:    var(--surface);
  --field-bg:      var(--field);
  --field-bg-focus:var(--field-focus);
  --primary-2:     var(--primary-3);
  --info:          var(--primary);
  --info-fg:       var(--primary-fg);
}

* { box-sizing: border-box; }

html, body {
  margin: 0; padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
}

/* Numbers that sit in a column, a badge or a timestamp must not jitter as they
   tick, so the digits are fixed width wherever a figure is read against another
   figure. Prose keeps proportional digits, which is why this is not on body. */
table.data, .kpi .value, .nav-badge, .num, .count, .progress-meta, .health-row .v,
.chart-bar-value, .panel-note, .mono, time { font-variant-numeric: tabular-nums; }

h1, h2, h3, h4 { margin: 0; }
h1 { font-size: var(--fs-h1); line-height: var(--lh-h1); font-weight: 640; letter-spacing: var(--tr-20); }
h2 { font-size: var(--fs-h2); line-height: var(--lh-h2); font-weight: 620; letter-spacing: var(--tr-16); }
h3 { font-size: var(--fs-h3); line-height: var(--lh-h3); font-weight: 620; }

a { color: var(--primary-fg); text-decoration: none; }
a:hover { text-decoration: underline; }
a:visited { color: var(--primary-fg); }

/* Section 8: one focus ring, the same everywhere, never removed. Offset 2px so
   it clears a filled control's own edge instead of sitting on it. */
:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

::selection { background: rgba(47, 111, 237, .35); color: #fff; }
mark { background: rgba(217, 158, 11, .30); color: var(--text); }

/* Every control the user agent tints for itself. accent-color covers ticks,
   radios, ranges and progress; caret-color covers the text cursor. Left unset
   each one falls back to the platform blue.

   accent-color sits on :root as well as on the three control tags, because it
   inherits and the tag list only reaches the controls that exist today. A
   measured sweep found :root computing accent-color: auto, so anything the
   platform tints that is not an input, a textarea or a select was still on the
   platform blue by default. Nothing on the page is in that position right now,
   which is exactly why it is worth closing before something is. */
:root { accent-color: var(--primary); }
input, textarea, select { accent-color: var(--primary); caret-color: var(--primary); }
progress { accent-color: var(--primary); }

/* The placeholder is set again on the bare tags, under the .input rule further
   down, because that rule is the only one in this file that names a placeholder
   and it only reaches a control that was given the class. A field written
   without it would take the user agent grey against --field, and the user agent
   grey is not a colour this file has ever chosen. */
input::placeholder, textarea::placeholder { color: var(--text-mute); opacity: 1; }

/* An option list is drawn by the platform, so the ground under it was the
   operating system's choice while the text on it was ours. That is the same
   split that put dark text on a dark ground in the reader. Naming both ends it.
   The popup is an operating system surface and does not appear in a page
   screenshot, so this one is reasoned rather than measured. */
option, optgroup { background-color: var(--panel); color: var(--text); }

/* The autofill wash. Chrome paints a filled field from its own palette and wins
   with a user agent important rule that no background-color can beat, so the
   fill has to be forged from an inset shadow and the text recoloured directly.
   Without this a remembered address arrives on a ground this file never picked.
   Headless Chromium cannot be made to autofill, so unlike the rest of this
   block it is not measured here. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill {
  -webkit-text-fill-color: var(--text);
  -webkit-box-shadow: inset 0 0 0 100px var(--field);
  caret-color: var(--primary);
}
input:-webkit-autofill:focus { -webkit-box-shadow: inset 0 0 0 100px var(--field-focus); }
/* A select is kept out of the fill above on purpose. An inset shadow is painted
   over the background image and under the border, so filling a select that way
   would hide the chevron for as long as the value was autofilled, which is the
   same disappearance the focus rule further down was just fixed for. The text
   colour is safe to state on its own and the field already carries an opaque
   background of its own, so only the wash would be left showing, and only on a
   select Chrome decided to fill. */
select:-webkit-autofill { -webkit-text-fill-color: var(--text); caret-color: var(--primary); }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
/* Where a horizontal and a vertical bar meet. Styling any part of the scrollbar
   hands the whole widget to this file, the corner included, and an unnamed
   corner is then the user agent's to fill. It measures as the panel colour
   today, so this names what is already true rather than repainting it. */
::-webkit-scrollbar-corner { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,.14); border-radius: 6px; border: 2px solid transparent; background-clip: content-box; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.24); background-clip: content-box; }

/* Visually gone, still read aloud. An icon-only control needs a real name in
   the accessibility tree, and a name carried only by an attribute is lost to a
   translation layer that rewrites text nodes and nothing else. */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ---------- 6. icons ----------
   One system, the sprite in templates/fragments/icons.html, used as
   <svg class="ic" aria-hidden="true"><use href="#i-name"/></svg>.
   No emoji, no dingbats, no text glyphs as icons: those are font characters, so
   the device decides how they look and a padlock arrives orange on Windows,
   grey on a Mac and as an empty box where no emoji font is installed. */
.ic {
  width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor;
  stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round;
  /* Not cosmetic. A click on a button whose only child is this svg reports the
     svg as e.target, so any handler reading e.target.dataset silently stops
     firing the moment a text label becomes an icon. */
  pointer-events: none;
}
.ic-lg { width: 20px; height: 20px; }
.ic-sm { width: 15px; height: 15px; stroke-width: 1.9; }
.ic-32 { width: 32px; height: 32px; stroke-width: 1.5; color: var(--text-faint); }

/* ---------- ambient background ----------
   Neutralised. The console is flat, so this layer is now just the page colour.
   The rule is kept so any view that mounts the element still paints correctly. */
.aurora-bg { position: fixed; inset: 0; z-index: -1; overflow: hidden; background: var(--bg); }
.aurora-bg::after { content: ""; position: absolute; inset: 0; background-image: none; mask-image: none; }

/* ---------- type helpers ---------- */
/* No gradient text in a console. Kept as a selector, painted solid. */
.text-gradient {
  background: none;
  -webkit-background-clip: border-box; background-clip: border-box;
  -webkit-text-fill-color: currentColor;
  color: var(--text);
}
.eyebrow, .overline {
  font-size: var(--fs-over); line-height: var(--lh-over); font-weight: 700;
  letter-spacing: .09em; text-transform: uppercase; color: var(--text-mute);
}
.muted { color: var(--text-dim); }
.mono { font-family: var(--mono); font-size: var(--fs-sm); }
.nowrap { white-space: nowrap; }

/* ---------- shell ---------- */
.app { display: flex; min-height: 100vh; min-height: 100dvh; }

.sidebar {
  position: fixed; inset: 0 auto 0 0; width: var(--sidebar-w);
  display: flex; flex-direction: column; gap: 1px;
  padding: var(--sp-3) var(--sp-2);
  overflow-y: auto; overscroll-behavior: contain;
  background: var(--surface);
  border-right: 1px solid var(--border);
  z-index: 40;
}
.brand { display: flex; align-items: center; gap: 9px; padding: var(--sp-1) 6px var(--sp-4); }
.brand-mark {
  width: 28px; height: 28px; flex: none; border-radius: var(--r-xs);
  background: var(--primary);
  display: grid; place-items: center; font-weight: 700; font-size: var(--fs-body); color: #fff;
}
.brand-text { font-weight: 600; font-size: var(--fs-h3); line-height: 1.25; }
.brand-text small { display: block; font-weight: 400; font-size: var(--fs-over); color: var(--text-mute); }

.nav-group {
  font-size: var(--fs-over); line-height: var(--lh-over); font-weight: 700;
  letter-spacing: .09em; text-transform: uppercase;
  color: var(--text-mute); padding: var(--sp-4) 10px var(--sp-1);
}
.nav-item {
  display: flex; align-items: center; gap: 9px;
  padding: 7px 10px; border-radius: var(--r-xs);
  color: var(--text-dim); font-weight: 500; font-size: var(--fs-body); line-height: 18px;
  cursor: pointer; border: 1px solid transparent;
  box-shadow: inset 0 0 0 0 transparent;
  transition: background var(--t-fast), color var(--t-fast), box-shadow var(--t-fast);
  user-select: none; -webkit-tap-highlight-color: transparent;
}
.nav-item:hover { background: var(--panel-2); color: var(--text); }
.nav-item:active { background: var(--panel-4); }
/* The current item is a lifted slab with a blue left edge. The edge is an inset
   shadow rather than a border, so the label never shifts sideways by 3px at the
   moment a row becomes current. */
.nav-item.active {
  background: var(--panel-3); color: var(--text); font-weight: 600;
  border-color: transparent;
  box-shadow: inset 3px 0 0 var(--primary);
}
.nav-item .ico, .nav-item .ic { width: 16px; height: 16px; flex: none; opacity: .85; }
.nav-item.active .ico, .nav-item.active .ic { opacity: 1; }
.nav-badge {
  margin-left: auto; font-size: var(--fs-over); font-weight: 600; padding: 0 5px;
  border-radius: 3px; background: var(--tint-neutral); color: var(--text-dim);
  line-height: 16px;
}
.nav-item.active .nav-badge { background: rgba(255,255,255,.18); color: #fff; }
.sidebar-foot { margin-top: auto; padding-top: var(--sp-3); border-top: 1px solid var(--border); }

/* An anchor in the rail has to be told to look like the divs around it. */
a.nav-item, a.nav-item:hover { text-decoration: none; }

.user-chip {
  display: flex; align-items: center; gap: 9px; padding: 7px var(--sp-2);
  border-radius: var(--r-xs); background: var(--panel); border: 1px solid var(--border);
  margin-bottom: 6px;
}
.avatar {
  width: 26px; height: 26px; flex: none; border-radius: var(--r-full);
  background: var(--panel-4);
  display: grid; place-items: center; font-size: var(--fs-cap); font-weight: 600; color: var(--text);
}
.user-chip .who { min-width: 0; }
.user-chip .who b { display: block; font-size: var(--fs-sm); font-weight: 600;
                    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-chip .who span { display: block; font-size: var(--fs-over); color: var(--text-mute);
                       overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.main { flex: 1; margin-left: var(--sidebar-w); min-width: 0; padding: 18px var(--sp-6) 56px; }

.topbar { display: flex; align-items: flex-start; gap: var(--sp-4); margin-bottom: var(--sp-4); flex-wrap: wrap; }
.topbar h1 { margin: 0; }
.topbar .sub { margin: var(--sp-1) 0 0; font-size: var(--fs-sm); line-height: var(--lh-sm); color: var(--text-mute); max-width: 660px; }
.topbar-actions { margin-left: auto; display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap; }

/* ---------- the phone chrome ----------
   Both of these are inert above 900px and neither exists in the desktop layout.
   They are declared out here rather than inside the phone block so that the
   closed state of the scrim is defined once, at every width, and cannot be
   reached by a class left behind on a resize. */
.mobile-topbar { display: none; }
.scrim {
  position: fixed; inset: 0; z-index: 60;
  background: rgba(0, 0, 0, .58);
  opacity: 0; visibility: hidden; pointer-events: none;
  /* Visibility is paired with the fade and flips instantly on the way in, then
     waits for the fade on the way out. Opacity alone would leave a full
     viewport layer in the hit test and in the accessibility tree while it is
     invisible. */
  transition: opacity var(--t-base), visibility 0s linear 200ms;
}

/* ---------- panels ---------- */
.panel {
  background: var(--panel); border: 1px solid var(--border-strong); border-radius: var(--r-md);
  box-shadow: var(--e1); overflow: hidden;
}
.panel-head {
  display: flex; align-items: center; gap: 10px; padding: 10px var(--sp-4);
  border-bottom: 1px solid var(--border); background: var(--surface); flex-wrap: wrap;
  min-height: 44px;
}
.panel-head h3 { margin: 0; font-size: var(--fs-h2); line-height: var(--lh-h2); font-weight: 620;
                 letter-spacing: var(--tr-16); color: var(--text); }
.panel-head .spacer { margin-left: auto; }
.panel-body { padding: var(--sp-4); }
.panel-body.tight { padding: 0; }

.grid { display: grid; gap: var(--sp-3); }
.grid-kpi { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 1320px) { .grid-kpi { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* Two abreast all the way down, and deliberately never one. There used to be a
   560px rule below this one that collapsed the band to a single column, and on
   a 390px phone that made the eight overview tiles 881px tall: 1.04 of the
   viewport, so every reader had to scroll past the whole band before the first
   chart existed, on a page whose entire subject is the shape of those numbers.
   The height is bought back from the gutter and not from the numbers. The
   widest value this console renders is the Indian-format "1,28,401", 95px of
   ink at the phone's 26px value size, and a tile two abreast at 390 still
   offers 145px of line, so no value is clipped to pay for the saving. */
@media (max-width: 900px)  { .grid-kpi { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.grid-2 { grid-template-columns: minmax(0,1.5fr) minmax(0,1fr); }
.grid-half { grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); }
@media (max-width: 1180px) { .grid-2 { grid-template-columns: 1fr; } }

/* ---------- KPI ---------- */
.kpi {
  position: relative; padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md); background: var(--panel);
  border: 1px solid var(--border-strong); box-shadow: var(--e1); overflow: hidden;
}
/* A flat 2px status edge down the left, which is the only place a tile spends
   colour. Tinting the tile face instead would put four blue slabs across the
   top of the overview, which is the template look section 13 rules out. */
.kpi::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 2px; background: var(--primary); }
.kpi .label {
  font-size: var(--fs-over); line-height: var(--lh-over); color: var(--text-mute);
  font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
}
.kpi .value {
  font-size: 24px; font-weight: 640; letter-spacing: var(--tr-20);
  margin-top: 6px; line-height: 1.2; color: var(--text);
}
.kpi .foot { font-size: var(--fs-cap); line-height: var(--lh-cap); color: var(--text-dim); margin-top: var(--sp-1); }
.kpi.accent::before { background: var(--accent); }
.kpi.warn::before   { background: var(--warning); }
.kpi.danger::before { background: var(--danger); }
.kpi.good::before   { background: var(--success); }

/* ---------- controls ---------- */
label.field { display: block; margin-bottom: var(--sp-3); }
label.field > span {
  display: block; font-size: var(--fs-cap); line-height: var(--lh-cap); font-weight: 600;
  letter-spacing: .01em; color: var(--text-dim); margin-bottom: 5px;
}
label.field > span .hint { letter-spacing: 0; font-weight: 400; color: var(--text-mute); margin-left: 6px; }

.input, select.input, textarea.input {
  width: 100%; padding: 7px 10px; min-height: 34px;
  background: var(--field); border: 1px solid var(--border-strong);
  border-radius: var(--r-sm); color: var(--text);
  font-family: inherit; font-size: var(--fs-body); line-height: 18px;
  transition: border-color var(--t-fast), background var(--t-fast);
}
.input::placeholder { color: var(--text-mute); opacity: 1; }
.input:hover:not(:disabled) { border-color: rgba(255,255,255,.22); }
/* The ring itself comes from :focus-visible, which a text field matches on a
   click as well as on a tab. The border and the fill move too, so a field that
   was focused programmatically still reads as the live one. */
/* background-color, not the background shorthand, and this is load bearing. The
   chevron on select.input below is a background IMAGE, and the shorthand resets
   background-image to none. Both of these rules match a select and outrank
   select.input, so writing them as a shorthand deleted the arrow for exactly as
   long as the control was focused or disabled: measured at 4.99:1 against the
   field at rest and at zero ink in both of those states, on a control that also
   carries appearance: none and so has no native arrow to fall back on. A
   focused select looked like a text box. .skeleton and .jrn-canvas already
   split the colour from the image for the same reason. */
.input:focus { border-color: var(--primary); background-color: var(--field-focus); outline: none; }
.input:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.input:disabled { background-color: var(--panel-2); color: var(--text-mute); cursor: not-allowed; }
textarea.input { resize: vertical; font-family: var(--mono); font-size: var(--fs-sm); line-height: 1.6; min-height: 120px; padding: var(--sp-2) 10px; }
select.input {
  appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path d='M2 4.5L6 8.5L10 4.5' stroke='%23949494' stroke-width='1.6' fill='none' stroke-linecap='round'/></svg>");
  background-repeat: no-repeat; background-position: right 10px center; padding-right: 30px;
}
/* The tick and the dot are drawn here rather than left to accent-color. Section
   8 wants a 44x44 target on a phone that does not inflate the glyph, and a
   native control cannot give both: its tick is painted by the user agent at the
   size of its border box, so a 44px input paints a 44px tick. With appearance
   none the element and the ink are separate, which is what the 900 block uses.
   The second reason is the one in this file's header: a native box is the last
   surface where the operating system, not this stylesheet, decides what the
   product looks like, and shipping the icons as symbols rather than as font
   glyphs is the same argument.

   Each image carries two colour literals, because a url() is a separate
   document and cannot read a custom property: --field #262626 and --primary
   #2f6fed, both opaque, both from section 2. The edge is written as white at
   .22 alpha instead of a hex so that it still composites over --panel and over
   --surface the way --border-strong does. If section 2 moves, these four
   strings move with it. */
input[type="checkbox"], input[type="radio"] {
  /* The prefix is carried here and nowhere else in the file on purpose. Where
     appearance is unsupported a select simply gets two arrows, which is untidy;
     here the control would keep its native tick and then be told to be 44px
     wide below 900, which paints a 44px tick and is exactly what section 8
     forbids. Safari before 15.4 is the case that matters. */
  -webkit-appearance: none;
  appearance: none; width: 15px; height: 15px; flex: none;
  margin: 0; padding: 0; border: 0; background-color: transparent;
  background-repeat: no-repeat; background-position: center; background-size: 15px 15px;
  cursor: pointer;
}
input[type="checkbox"] {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 22'><rect x='.7' y='.7' width='20.6' height='20.6' rx='5' fill='%23262626' stroke='white' stroke-opacity='.22' stroke-width='1.4'/></svg>");
}
input[type="checkbox"]:checked {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 22'><rect x='.7' y='.7' width='20.6' height='20.6' rx='5' fill='%232f6fed'/><path d='M6.2 11.3l3.3 3.4 6.3-6.6' fill='none' stroke='white' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/></svg>");
}
input[type="radio"] {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 22'><circle cx='11' cy='11' r='10.3' fill='%23262626' stroke='white' stroke-opacity='.22' stroke-width='1.4'/></svg>");
}
input[type="radio"]:checked {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 22'><circle cx='11' cy='11' r='10.3' fill='%232f6fed'/><circle cx='11' cy='11' r='4.2' fill='white'/></svg>");
}
input[type="checkbox"]:disabled, input[type="radio"]:disabled { opacity: .45; cursor: not-allowed; }
.check { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-body); color: var(--text-dim); cursor: pointer; }

.row { display: flex; gap: 10px; flex-wrap: wrap; }
.row > * { flex: 1 1 190px; min-width: 0; }

/* Button hierarchy, section 8. Secondary is the default: a panel-3 face with a
   real edge. Primary is the one filled accent on the screen. Ghost carries no
   edge at all until it is hovered, which is what stops a toolbar of six actions
   reading as six competing buttons. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 7px var(--sp-4); min-height: 34px; border-radius: var(--r-sm);
  border: 1px solid var(--border-strong); background: var(--panel-3);
  color: var(--text); font-family: inherit; font-size: var(--fs-body); font-weight: 500; line-height: 18px;
  cursor: pointer; white-space: nowrap; -webkit-tap-highlight-color: transparent;
  transition: background var(--t-fast), border-color var(--t-fast), transform var(--t-fast);
}
.btn:hover { background: var(--panel-4); border-color: rgba(255,255,255,.26); }
/* One pixel down, not a scale. A button that shrinks under the finger drags its
   neighbours in a flex row on to new subpixels and the whole toolbar shimmers. */
.btn:active { background: var(--panel-4); transform: translateY(1px); }
.btn:disabled, .btn[aria-disabled="true"] {
  background: var(--panel-2); border-color: var(--border); color: var(--text-mute);
  cursor: not-allowed; box-shadow: none; transform: none;
}
.btn-primary {
  background: var(--primary); border-color: var(--primary); color: #fff;
  font-weight: 600; box-shadow: var(--e1);
}
.btn-primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
/* Follows the hover down. Once hover is the rest colour at 92%, the old pressed
   value at 88% is only 0.012 of luminance darker than it, which no eye reads as
   a second step. 85% restores an even ladder: 0.181, 0.151, 0.127. Contrast on
   white goes 4.55, 5.23, 5.94, so the label gets more readable as the button is
   worked, never less. */
.btn-primary:active { background: #285ec9; border-color: #285ec9; }
.btn-danger { color: var(--danger-fg); border-color: rgba(224,72,60,.42); background: var(--tint-danger); }
.btn-danger:hover { background: rgba(224,72,60,.20); border-color: rgba(224,72,60,.60); }
.btn-ghost { background: transparent; border-color: transparent; color: var(--text-dim); }
.btn-ghost:hover { background: var(--panel-3); color: var(--text); border-color: var(--border-strong); }
/* Small enough that a row of table actions does not push the row past 48px. */
.btn-sm { padding: 2px 10px; min-height: 26px; font-size: var(--fs-sm); border-radius: var(--r-xs); line-height: 18px; }
.btn .ic { margin-left: -2px; }

/* Section 8: an async action shows its pending state on the control that was
   pressed, never as a page level spinner. Set aria-busy on the button and the
   ring appears in front of the label, which stays readable and stays measured,
   so the toolbar does not reflow while the request is in flight. */
.btn[aria-busy="true"], .btn.is-busy { pointer-events: none; }
.btn[aria-busy="true"]::before, .btn.is-busy::before {
  content: ""; width: 14px; height: 14px; flex: none; border-radius: var(--r-full);
  border: 2px solid currentColor; border-top-color: transparent; opacity: .75;
  animation: spin 700ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* An icon-only control. The drawing stays 18px and the hit area grows around
   it, which is the section 8 rule: pad the target, never inflate the glyph. */
.icon-btn, #navToggle {
  display: inline-grid; place-items: center; width: 36px; height: 36px; padding: 0;
  border-radius: var(--r-sm); border: 1px solid transparent; background: transparent;
  color: var(--text-dim); cursor: pointer; flex: none; -webkit-tap-highlight-color: transparent;
  transition: background var(--t-fast), color var(--t-fast);
}
.icon-btn:hover, #navToggle:hover { background: var(--panel-3); color: var(--text); }
.icon-btn:active, #navToggle:active { background: var(--panel-4); }

/* ---------- pills ---------- */
.pill {
  display: inline-flex; align-items: center; gap: 5px; padding: 1px 7px;
  border-radius: var(--r-xs); font-size: var(--fs-over); font-weight: 600; letter-spacing: .05em;
  text-transform: uppercase; border: 1px solid transparent; white-space: nowrap;
  line-height: 17px;
}
.pill::before { content: ""; width: 5px; height: 5px; border-radius: var(--r-full); background: currentColor; flex: none; }
.pill.plain::before { display: none; }
.pill-draft     { color: var(--text-dim);   background: var(--tint-neutral);   border-color: var(--border); }
.pill-sending   { color: var(--primary-fg); background: var(--tint-primary);   border-color: rgba(47,111,237,.34); }
.pill-scheduled { color: var(--primary-3);  background: var(--tint-primary);   border-color: rgba(76,141,255,.30); }
.pill-sent      { color: var(--success-fg); background: var(--tint-success);   border-color: rgba(31,157,85,.40); }
.pill-failed    { color: var(--danger-fg);  background: var(--tint-danger);    border-color: rgba(224,72,60,.36); }
.pill-pending   { color: var(--warning-fg); background: var(--tint-warning);   border-color: rgba(217,158,11,.34); }
.pill-skipped   { color: var(--text-mute);  background: rgba(255,255,255,.05); border-color: var(--border); }
.pill-info      { color: var(--primary-fg); background: var(--tint-primary);   border-color: rgba(47,111,237,.34); }

/* ---------- tables ----------
   The wrapper is a query container so that anything inside it can be sized
   against the width that is actually available rather than against the
   viewport. A data table is the one place in this file where a child's fixed
   max-width leaks upwards: table layout is auto, so every cell asks for its
   max-content width and the table becomes the sum of those asks, whatever the
   wrapper is. One 300px cap inside a half-width panel therefore drags the whole
   table past the edge. See .truncate below for the measured case. */
.table-wrap { width: 100%; overflow-x: auto; container-type: inline-size; }
table.data { width: 100%; border-collapse: collapse; font-size: var(--fs-body); }
table.data th {
  text-align: left; padding: var(--sp-2) var(--sp-4); font-size: var(--fs-cap); font-weight: 600;
  letter-spacing: .06em; text-transform: uppercase; color: var(--text-mute);
  background: var(--surface); border-bottom: 1px solid var(--border); white-space: nowrap;
}
table.data td {
  padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--border);
  color: var(--text-dim); vertical-align: middle; line-height: 19px;
}
/* The row is what the pointer is over, so the whole row lifts together. Tinting
   cell by cell leaves a seam wherever one cell sets a fill of its own. */
table.data tbody tr { transition: background var(--t-fast); }
table.data tbody tr:hover td { background: var(--panel-2); color: var(--text); }
table.data tbody tr.clickable { cursor: pointer; }
table.data tbody tr.clickable:active td { background: var(--panel-3); }
table.data tbody tr:last-child td { border-bottom: none; }
table.data td.num { text-align: right; }
table.data td strong { color: var(--text); font-weight: 600; }
table.data td.actions { display: flex; gap: 6px; padding-top: var(--sp-2); padding-bottom: var(--sp-2); }
/* The cap has to follow the container, not a number picked for a full-width
   table. 300px was that number, and in the transactional view the delivery log
   is a .grid-2 cell 694px wide: the SES error line under the status pill asked
   for its full 300px, the Status column became 332px, and the table asked for
   825px. Measured at 1440, that was 131px of the table hidden behind a
   scrollbar in a panel with room to spare, because the four other columns only
   wanted 493px between them. 20cqi is 20% of the wrapper's own inline size, so
   the same rule gives that cell 139px and a full-width log 234px. Outside a
   wrapper cqi falls back to the viewport, which lands near the old 300px, so a
   .truncate that ever escapes a table is no worse off than it was. */
.truncate { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-block; vertical-align: bottom; }
/* Second declaration, not merged into the one above, because an engine that
   cannot parse cqi drops the whole line: keeping the flat 300px first means
   such an engine lands on the old behaviour rather than on max-width none,
   which would let every truncated cell ask for its full natural width. */
.truncate { max-width: min(300px, 20cqi); }

.pager {
  display: flex; align-items: center; gap: var(--sp-2); padding: 9px var(--sp-4);
  border-top: 1px solid var(--border); background: var(--surface);
}
.pager .count { font-size: var(--fs-sm); color: var(--text-mute); margin-right: auto; }

/* ---------- 9. empty, loading and error ----------
   Three states, always all three, never a blank box. An empty state is a 32px
   icon in --text-faint, one line of what would be here, and the action that
   puts something here. */
.empty { padding: var(--sp-8) var(--sp-5); text-align: center; color: var(--text-mute); font-size: var(--fs-body); }
.empty .ic-32 { display: block; margin: 0 auto var(--sp-3); }
/* Legacy slot: console.js still writes a ring character in here. It is painted
   as a graphic rather than as text so it clears the 3:1 floor until that call
   site becomes <svg class="ic ic-32">, which is what section 6 asks for. */
.empty .big { font-size: 26px; line-height: 1; color: var(--text-faint); display: block; margin-bottom: var(--sp-2); }
.empty b { display: block; color: var(--text-dim); font-weight: 600; margin-bottom: var(--sp-1); }
.empty .btn { margin-top: var(--sp-4); }
.empty.error .ic-32 { color: var(--danger-fg); }

/* Loading is a skeleton that matches the real row geometry, never a spinner on
   a list. A spinner says something is happening; a skeleton says what is about
   to arrive and holds the space so nothing jumps when it does. */
.skeleton {
  display: block; border-radius: var(--r-xs); background-color: var(--panel-2);
  background-image: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.055) 50%, transparent 100%);
  background-repeat: no-repeat; background-size: 220% 100%; background-position: 130% 0;
  /* Only background-position moves. Section 7 forbids animating width, height,
     top or left, and the usual shimmer is built on width, which is that mistake
     repeated sixty times a second. */
  animation: skeleton-shimmer 1500ms linear infinite;
}
@keyframes skeleton-shimmer { to { background-position: -130% 0; } }
.skeleton-row {
  display: grid; grid-template-columns: minmax(0,2fr) minmax(0,1fr) minmax(0,1fr) 80px;
  gap: var(--sp-4); align-items: center;
  padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--border);
}
.skeleton-row .skeleton { height: 12px; }
/* Widths rotate over three values in CSS rather than coming from Math.random,
   so the placeholder is stable across renders and does not read as a barcode. */
.skeleton-row:nth-child(3n+1) .skeleton:first-child { width: 78%; }
.skeleton-row:nth-child(3n+2) .skeleton:first-child { width: 62%; }
.skeleton-row:nth-child(3n+3) .skeleton:first-child { width: 88%; }
.skeleton-row:nth-child(3n+2) .skeleton:nth-child(2) { width: 70%; }
.skeleton-row:nth-child(3n+3) .skeleton:nth-child(3) { width: 55%; }

/* ---------- tabs / segmented ---------- */
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin-bottom: var(--sp-3); flex-wrap: wrap; }
.tab { padding: var(--sp-2) var(--sp-3); font-size: var(--fs-body); font-weight: 500; color: var(--text-mute);
       cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px;
       transition: color var(--t-fast), border-color var(--t-fast); -webkit-tap-highlight-color: transparent; }
.tab:hover { color: var(--text); }
.tab.active { color: var(--text); font-weight: 600; border-bottom-color: var(--primary); }
/* Inset, because the strip's own bottom rule would slice a 2px offset ring. */
.tab:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

.chip-row { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: var(--sp-2); }
.chip {
  font-family: var(--mono); font-size: var(--fs-cap); padding: 2px 7px; border-radius: var(--r-xs);
  background: var(--panel-3); border: 1px solid var(--border);
  color: var(--primary-fg); cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast);
}
.chip:hover { background: var(--panel-4); border-color: var(--border-strong); }
.chip.static { cursor: default; }

/* ---------- preview ----------
   The one surface in the console that is deliberately light, because it shows
   somebody else's email and an email is composed against white. The literal
   #fff is right for that reason and is not a token.

   color-scheme has to be said out loud here. It inherits, so this element was
   computing dark while painting white and hosting light content, and the used
   scheme of an embedded document is taken from the embedder. Chrome does not
   currently push it across a sandbox="" boundary, so the frame measures light
   at 16 to 21:1 either way today; that is luck, not a decision, and it is the
   same shape of accident as text that keeps its colour when its ground moves.
   Declared light, the ground, any control inside a creative and the frame's own
   scrollbar all agree without depending on that. */
.preview-frame { width: 100%; height: 430px; border: 1px solid var(--border);
                 border-radius: var(--r-sm); background: #fff; color-scheme: light; }
.preview-frame.mobile { width: 390px; max-width: 100%; }
.preview-shell { display: flex; justify-content: center; }

/* ---------- progress ---------- */
.progress-card { margin-top: var(--sp-3); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-sm);
                 background: var(--surface); border: 1px solid var(--border);
                 border-left: 2px solid var(--primary); }
.progress-track { height: 6px; border-radius: 3px; background: rgba(255,255,255,.10); overflow: hidden; margin: var(--sp-2) 0 7px; }
.progress-fill { height: 100%; border-radius: 3px; background: var(--primary); width: 0; transition: width var(--t-slow); }
.progress-meta { display: flex; gap: var(--sp-4); font-size: var(--fs-sm); color: var(--text-dim); flex-wrap: wrap; }
.progress-meta b { color: var(--text); font-weight: 600; }

/* ---------- health rows ---------- */
.health-row { display: flex; align-items: center; gap: 9px; padding: var(--sp-2) 0;
              border-bottom: 1px solid var(--border-soft); font-size: var(--fs-body); min-height: 36px; }
.health-row:last-child { border-bottom: none; }
.health-row .k { color: var(--text-mute); }
.health-row .v { margin-left: auto; font-weight: 600; text-align: right; color: var(--text); }
.quota-track { height: 6px; border-radius: 3px; background: rgba(255,255,255,.10); overflow: hidden; margin-top: 9px; }
.quota-fill { height: 100%; background: var(--success); border-radius: 3px; }
.status-dot { width: 7px; height: 7px; border-radius: var(--r-full); flex: none; background: var(--success-fg);
              box-shadow: 0 0 0 2px rgba(31,157,85,.22); }
.status-dot.bad  { background: var(--danger-fg);  box-shadow: 0 0 0 2px rgba(224,72,60,.22); }
.status-dot.warn { background: var(--warning-fg); box-shadow: 0 0 0 2px rgba(217,158,11,.22); }

/* ---------- dropzone ---------- */
.dropzone {
  border: 1px dashed var(--border-strong); border-radius: var(--r-sm);
  padding: var(--sp-5); text-align: center; color: var(--text-mute); cursor: pointer;
  background: var(--surface);
  transition: border-color var(--t-fast), background var(--t-fast), color var(--t-fast);
  font-size: var(--fs-body);
}
.dropzone:hover, .dropzone.over { border-color: var(--primary); background: var(--primary-soft); color: var(--text-dim); }
.dropzone b { color: var(--text); font-weight: 600; }

/* ---------- code block ---------- */
pre.code {
  margin: 0; padding: var(--sp-3) var(--sp-4); border-radius: var(--r-sm);
  background: #161616; border: 1px solid var(--border);
  font-family: var(--mono); font-size: var(--fs-sm); line-height: 1.6;
  color: #d0d0d0; overflow-x: auto; white-space: pre;
}
pre.code .k { color: var(--primary-fg); }
pre.code .s { color: #3ec2b6; }
pre.code .c { color: var(--text-mute); }

/* ---------- modal ---------- */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 150; display: none;
  background: rgba(0, 0, 0, .62);
  align-items: center; justify-content: center; padding: var(--sp-6);
}
.modal-backdrop.open { display: flex; }
.modal {
  width: 100%; max-width: 520px; max-height: 88vh; overflow-y: auto; overscroll-behavior: contain;
  background: var(--panel); border: 1px solid var(--border-strong);
  border-radius: var(--r-md); box-shadow: var(--e3);
  animation: fade-up 160ms cubic-bezier(.2,.7,.3,1);
}
.modal.wide { max-width: 760px; }
.modal-head { display: flex; align-items: center; gap: 10px; padding: var(--sp-3) var(--sp-5);
              border-bottom: 1px solid var(--border); background: var(--surface); }
.modal-head h3 { margin: 0; font-size: var(--fs-h2); line-height: var(--lh-h2); letter-spacing: var(--tr-16); font-weight: 620; }
.modal-body { padding: var(--sp-5); }
.modal-foot { display: flex; gap: var(--sp-2); justify-content: flex-end; padding: var(--sp-3) var(--sp-5);
              border-top: 1px solid var(--border); background: var(--surface); }

/* ---------- toast ---------- */
#toasts { position: fixed; right: var(--sp-4); bottom: var(--sp-4); display: flex; flex-direction: column;
          gap: var(--sp-2); z-index: 200; max-width: 400px; }
.toast {
  padding: var(--sp-3) var(--sp-4); border-radius: var(--r-sm); background: var(--panel-2);
  border: 1px solid var(--border-strong); border-left: 3px solid var(--primary);
  box-shadow: var(--e2); font-size: var(--fs-body); line-height: var(--lh-sm); color: var(--text);
  animation: toast-in 180ms cubic-bezier(.2,.9,.3,1);
}
.toast.ok { border-left-color: var(--success); }
.toast.err { border-left-color: var(--danger); }
.toast.warn { border-left-color: var(--warning); }
@keyframes toast-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

/* ---------- views ---------- */
.view { display: none; animation: fade-up 180ms cubic-bezier(.2,.7,.3,1); }
.view.active { display: block; }
@keyframes fade-up { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: none; } }

/* ---------- auth ---------- */
.auth-wrap { min-height: 100vh; min-height: 100dvh; display: grid; place-items: center; padding: var(--sp-6); }
.auth-card {
  width: 100%; max-width: 400px; padding: var(--sp-7);
  background: var(--panel); border: 1px solid var(--border-strong); border-radius: var(--r-md);
  box-shadow: var(--e3);
  animation: fade-up 200ms cubic-bezier(.2,.7,.3,1);
}
.alert {
  padding: 9px var(--sp-3); border-radius: var(--r-sm); font-size: var(--fs-sm); line-height: var(--lh-sm);
  margin-bottom: var(--sp-4);
  background: var(--tint-danger); border: 1px solid rgba(224,72,60,.36); color: var(--danger-fg);
  border-left: 3px solid var(--danger);
}
.alert.ok { background: var(--tint-success); border-color: rgba(31,157,85,.36); border-left-color: var(--success); color: var(--success-fg); }
.alert.warn { background: var(--tint-warning); border-color: rgba(217,158,11,.32); border-left-color: var(--warning); color: var(--warning-fg); }
.alert.info { background: var(--tint-primary); border-color: rgba(47,111,237,.32); border-left-color: var(--primary); color: var(--primary-fg); }

/* ---------- explanatory note above a screen ---------- */
/* Reads as prose rather than as an alert, so a permanent explanation on a screen
   does not look like something went wrong. */
.note {
  padding: 10px var(--sp-3); border-radius: var(--r-sm);
  background: var(--surface); border: 1px solid var(--border);
  color: var(--text-dim); font-size: var(--fs-sm); line-height: 1.6;
}
.note:empty { display: none; }
.note b { color: var(--text); font-weight: 600; }

/* ==========================================================================
   Merge tag test fields
   Two per row on a wide panel, one on a narrow one. A creative with fifteen
   tags has to stay usable inside the composer's right hand column.
   ========================================================================== */
.merge-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--sp-2) 10px;
  max-height: 260px;
  overflow-y: auto;
  padding-right: 2px;
}
.merge-grid .merge-field { margin: 0; }
.merge-grid .merge-field > span {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--text-mute);
  letter-spacing: .2px;
  text-transform: none;
  font-weight: 400;
}
.merge-grid .merge-input { padding: var(--sp-1) var(--sp-2); min-height: 28px; font-size: var(--fs-sm); }

/* ==========================================================================
   Journey flowchart
   Plain HTML boxes positioned absolutely over one SVG layer of wires. No
   library: the console has no build step and no npm, and a canvas that needs
   a bundler would be the only thing here that does.
   ========================================================================== */

.jrn-shell {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 360px;
  gap: var(--sp-3);
  align-items: start;
}
@media (max-width: 1180px) { .jrn-shell { grid-template-columns: minmax(0, 1fr); } }

.jrn-canvas-panel .panel-head { flex-wrap: wrap; gap: 6px; }

.jrn-canvas {
  position: relative;
  height: 560px;
  overflow: auto;
  border-radius: 0;
  background-color: var(--bg);
  /* A faint grid so a dragged box has something to line up against. */
  background-image:
    linear-gradient(rgba(255, 255, 255, .04) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, .04) 1px, transparent 1px);
  background-size: 28px 28px;
  outline: none;
}
/* An inset ring rather than an outline, because the canvas is the scroll port
   and an outline on a scroller is painted outside the clip and half eaten. */
.jrn-canvas:focus-visible { box-shadow: inset 0 0 0 2px var(--primary); }

.jrn-surface { position: relative; width: 2400px; height: 1600px; }
.jrn-wires { position: absolute; inset: 0; width: 2400px; height: 1600px; pointer-events: none; }

.jrn-node {
  position: absolute;
  width: 216px;
  padding: 9px 11px 10px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-strong);
  border-left-width: 3px;
  background: var(--panel-3);
  box-shadow: var(--e1);
  cursor: grab;
  user-select: none;
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.jrn-node:hover { box-shadow: var(--e2); border-color: rgba(255,255,255,.26); }
.jrn-node.dragging { cursor: grabbing; box-shadow: var(--e3); z-index: 40; }
.jrn-node.selected { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary); }
.jrn-node.connect-source { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

/* Type is never signalled by colour alone: every node also carries its kind in words. */
.jrn-node[data-kind="SOURCE"]    { border-left-color: var(--accent); }
.jrn-node[data-kind="EMAIL"]     { border-left-color: var(--primary); }
.jrn-node[data-kind="SPLIT"]     { border-left-color: var(--primary-3); }
.jrn-node[data-kind="CONDITION"] { border-left-color: var(--warning); }
.jrn-node[data-kind="WAIT"]      { border-left-color: var(--text-mute); }
.jrn-node[data-kind="EXIT"]      { border-left-color: var(--success); }

.jrn-kind { font-size: 10px; letter-spacing: .08em; text-transform: uppercase; color: var(--text-mute); font-weight: 600; }
.jrn-name { font-size: var(--fs-body); font-weight: 600; margin: 3px 0 var(--sp-1); line-height: 1.3; word-break: break-word; }
.jrn-meta { font-size: var(--fs-cap); color: var(--text-dim); line-height: 1.45; }
.jrn-here {
  display: inline-block; margin-top: 6px; padding: 1px 7px; border-radius: var(--r-xs);
  font-size: var(--fs-over); font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
  background: var(--primary); color: #fff;
}
.jrn-port {
  position: absolute; right: -9px; bottom: -9px; width: 20px; height: 20px;
  border-radius: var(--r-full); border: 1px solid var(--border-strong);
  background: var(--panel-3); color: var(--text-dim);
  font-size: var(--fs-sm); line-height: 1; display: grid; place-items: center; cursor: crosshair;
}
.jrn-port:hover { background: var(--primary); color: #fff; border-color: var(--primary); }

.jrn-status { min-height: 18px; padding: 7px 2px 0; font-size: var(--fs-sm); color: var(--text-dim); }
.jrn-side .panel-body { max-height: 620px; overflow-y: auto; }

.jrn-branch {
  display: flex; align-items: center; gap: var(--sp-2); padding: 7px 9px;
  border: 1px solid var(--border); border-radius: var(--r-sm); margin-bottom: 6px;
  font-size: var(--fs-sm); background: var(--surface);
}
.jrn-branch .grow { flex: 1; min-width: 0; }

.kind-card {
  padding: 11px var(--sp-3); border: 1px solid var(--border); border-radius: var(--r-sm);
  margin-bottom: var(--sp-2); cursor: pointer; background: var(--surface);
  transition: border-color var(--t-fast), background var(--t-fast);
}
.kind-card:hover { border-color: var(--primary); background: var(--panel-2); }
.kind-card b { font-size: var(--fs-body); font-weight: 600; }
.kind-card p { margin: var(--sp-1) 0 0; font-size: var(--fs-sm); color: var(--text-dim); line-height: 1.5; }

/* ---------- charts ----------
   Drawn as inline SVG by charts.js. The empty state is a real element rather
   than an unpainted box, because a silent blank frame reads as a broken page.
   flex-direction column is load bearing: as a row the SVG and its legend become
   siblings on one line and the legend is pushed off the panel edge. */
.chart-host {
  min-height: 210px; display: flex; flex-direction: column;
  align-items: center; justify-content: center; width: 100%; min-width: 0;
}
.chart-host > svg { max-width: 100%; }
.chart-empty { color: var(--text-mute); font-size: var(--fs-sm); text-align: center; padding: var(--sp-8) var(--sp-3); width: 100%; }
.chart-donut-wrap { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; justify-content: center; width: 100%; }
.chart-donut { flex: 0 0 auto; }
.chart-donut-value { fill: var(--text); font-size: 26px; font-weight: 650; }
.chart-donut-label { fill: var(--text-mute); font-size: 11px; letter-spacing: .04em; text-transform: uppercase; }
.chart-area { display: block; width: 100%; overflow: visible; }
.chart-axis { fill: var(--text-mute); font-size: 11px; }
.chart-axis-sm { fill: var(--text-mute); font-size: 9.5px; }
.chart-gauge { display: block; margin: 0 auto; }
.chart-gauge-value { fill: var(--text); font-size: 24px; font-weight: 650; }
.chart-spark { width: 100%; height: 32px; display: block; }

.chart-legend { display: flex; flex-wrap: wrap; gap: 6px var(--sp-4); margin-top: var(--sp-3); justify-content: center; }
.chart-legend-item { display: inline-flex; align-items: center; gap: 7px; font-size: var(--fs-sm); color: var(--text-dim); }
.chart-legend-item i { width: 9px; height: 9px; border-radius: 2px; display: inline-block; flex: 0 0 auto; }
.chart-legend-item b { color: var(--text); font-weight: 600; }

.chart-bars { width: 100%; display: flex; flex-direction: column; gap: 10px; }
.chart-bar-row { display: grid; grid-template-columns: minmax(90px, 30%) 1fr auto; align-items: center; gap: var(--sp-3); }
.chart-bar-label { font-size: var(--fs-sm); color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chart-bar-track { height: 8px; border-radius: 4px; background: rgba(255,255,255,.07); overflow: hidden; }
.chart-bar-track i { display: block; height: 100%; border-radius: 4px; }
.chart-bar-value { font-size: var(--fs-sm); color: var(--text-mute); }

.panel-note { margin-left: auto; font-size: var(--fs-sm); color: var(--text-mute); }

/* DNS values are long TXT strings. They must wrap inside the cell rather than
   force the whole table wider than the panel. */
.dns-value { font-size: var(--fs-sm); color: var(--text-dim); word-break: break-all; line-height: 1.5; padding: 1px 0; }

/* ==========================================================================
   PHASE 2 SURFACES

   Seven components the mailbox work needs at once: recipient chips, the compose
   formatting toolbar, the bulk selection bar, the checkbox column on a message
   row, the keyboard shortcut sheet, the dropdown menu and the send later
   picker. They are written here, once, so that agents working in parallel
   inherit one set of measurements instead of each inventing a chip.

   Two facts shaped the rules below and are worth stating before the first
   declaration, because they explain choices that otherwise look arbitrary.

   The first is that templates/mail.html does not link this file. It carries its
   own copy of the token block and its own components, so a rule written here
   reaches the console and nothing else until somebody lifts this section into
   that page. This section is written to survive that move, and the move needs
   exactly one thing done first: mail.html's :root has to gain the type scale
   (--fs-body, --fs-sm, --fs-cap, --fs-over, --lh-over), the 4px spacing scale
   (--sp-1 to --sp-7), --r-full, --field and --field-focus. Section 14.3 already
   sends tokens that way, so this is the same kind of ask and not a new one.
   What this section will not do is name --panel-2, which is #252525 here and
   #1c1c1c there. That token is lighter than --panel in the console and darker
   than --panel in the mailbox, so a popover grounded on it would arrive raised
   on one page and sunken on the other. Popovers here are grounded on --panel-3,
   which means the same thing in both.

   The second is section 8. Below 900px every control here is 44px in at least
   one dimension and 44x44 where it is a square, and where the visual has to
   stay small - the remove button on a 34px chip, a checkbox beside 12.5px
   text - the hit area grows through a transparent pseudo element rather than
   the glyph growing. The breakpoint is 899.98px and not 900px for the reason
   section 14.3 gives: max-width:900px and min-width:900px both match at exactly
   900.
   ========================================================================== */

/* ---------- recipient chips ----------
   A recipient is something you can point at and take back off again, not a
   substring of one comma separated line. The committed addresses sit in front
   of the input as its siblings inside the same bordered box, so the field's
   label still points at one real input and every address still owns a tap
   target of its own.

   .chip is scoped under .chips throughout. The console already ships a .chip of
   its own for merge tags, in a mono face at 11.5px, and an unscoped rule here
   would repaint every merge tag in the template editor. */
.chips {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px; width: 100%;
  min-height: 34px; padding: 4px 6px; cursor: text; position: relative;
  background: var(--field); border: 1px solid var(--border-strong);
  border-radius: var(--r-xs);
  transition: border-color var(--t-fast), box-shadow var(--t-fast), background var(--t-fast);
}
/* :focus-within covers the entry box and the chip buttons alike; the .focus
   class exists for the case where script has to hold the state open across a
   click on an autocomplete row, which moves focus out of the box for a frame. */
.chips:focus-within, .chips.focus {
  border-color: var(--primary); background: var(--field-focus);
  box-shadow: 0 0 0 2px rgba(47, 111, 237, .25);
}
.chips .chip {
  display: inline-flex; align-items: center; gap: 1px; max-width: 100%;
  height: 26px; padding: 0 2px 0 10px; border-radius: var(--r-full);
  background: var(--panel-3); border: 1px solid var(--border);
  font-family: var(--sans); font-size: var(--fs-sm); line-height: 1;
  color: var(--text-dim); white-space: nowrap;
}
.chips .chip-label {
  position: relative; align-self: stretch;
  display: inline-flex; align-items: center;
  min-width: 0; max-width: 200px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding: 0; border: 0; background: none; color: inherit; font: inherit; cursor: pointer;
}
/* Positioned, because below 900px the hit area is a pseudo element hung off
   this button rather than a larger circle. */
.chips .chip-remove {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; flex: none; padding: 0;
  background: none; border: 0; border-radius: var(--r-full);
  color: var(--text-mute); cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--t-fast), color var(--t-fast);
}
.chips .chip-remove:hover { background: var(--panel-4); color: var(--text); }
.chips .chip-remove .ic { width: 12px; height: 12px; }
/* An address that will not parse is kept and marked rather than dropped. A
   typed address that silently disappears is a worse failure than one that is
   visibly wrong, and the send refusal can then name it. --danger-fg and not
   --danger: the audit already has --danger failing at 4.00:1 as text. */
.chips .chip.chip-invalid {
  border-color: var(--danger); background: rgba(224, 72, 60, .10); color: var(--danger-fg);
}
.chips .chip.chip-invalid .chip-remove { color: var(--danger-fg); }
/* Backspace on an empty entry arms the last chip before it removes it, so a
   held key cannot walk backwards through a recipient list two at a time. */
.chips .chip.chip-armed { outline: 2px solid var(--primary); outline-offset: 1px; }
/* The one control in this file that removes its own outline, and it is removed
   rather than hidden: the ring belongs to the bordered box, not to the bare
   input inside it, or focusing the entry paints a rectangle inside a rectangle.
   The indicator is the :focus-within rule above, which is the same 2px of
   --primary section 8 asks for, drawn one element out. */
.chips .chip-entry {
  flex: 1 1 130px; min-width: 130px; width: auto; padding: 0 4px; min-height: 24px;
  background: none; border: 0; box-shadow: none; outline: none;
  color: var(--text); font: inherit; font-size: var(--fs-body); line-height: 20px;
}
.chips .chip-entry:focus { border: 0; box-shadow: none; background: none; }

/* ---------- compose formatting toolbar ----------
   The bar wraps and never scrolls. Section 14.3 threw the rail's horizontal
   scroller out for hiding its own tail, and a toolbar of ten 44px buttons is
   440px against a 390px screen, which is the same defect one control at a time:
   the last commands would sit off the right edge with nothing saying so.
   Wrapping costs a second 44px row on the narrowest phone and hides nothing. */
.rte-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 2px; padding: 4px 6px;
  background: var(--panel-3); border: 1px solid var(--border-strong); border-bottom: 0;
  border-radius: var(--r-xs) var(--r-xs) 0 0;
}
.rte-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  flex: none; min-height: 32px; min-width: 32px; padding: 0 8px;
  background: none; border: 1px solid transparent; border-radius: var(--r-xs);
  color: var(--text-dim); font: inherit; font-size: var(--fs-sm); font-weight: 500;
  white-space: nowrap; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.rte-btn:hover { background: var(--panel-4); color: var(--text); }
/* .on and aria-pressed are one state under two names. The attribute is what a
   screen reader reads and the class is what a handler is likely to toggle, and
   a toolbar where those two can disagree is a toolbar that lies to one of
   them. */
.rte-btn.on, .rte-btn[aria-pressed="true"] {
  background: var(--panel-4); color: var(--text); border-color: var(--border-strong);
}
.rte-btn:disabled, .rte-btn[aria-disabled="true"] {
  color: var(--text-mute); background: none; cursor: not-allowed;
}
/* --border-strong and not --border. Shot at 1440 the group divider in a bar
   grounded on --panel-3 was a 1px line at rgba(255,255,255,.075), which is
   below the 3:1 an interface edge is held to and read as a gap rather than as
   a rule. A divider nobody can see is a divider that groups nothing. */
.rte-sep { flex: none; width: 1px; align-self: stretch; margin: 4px 3px; background: var(--border-strong); }

/* ---------- bulk selection bar ----------
   It replaces the list header rather than sitting under it, so the count it is
   acting on cannot scroll away from the buttons that act on it. The blue edge
   is the same device the console uses for a current nav item and it is the only
   accent here: a selection is a mode, and a mode has to be visible at a glance
   without spending a filled button on it. */
.bulkbar {
  display: flex; align-items: center; gap: 4px; min-height: 44px;
  padding: 6px 8px 6px 12px; position: sticky; top: 0; z-index: 30;
  background: var(--panel-3); border-bottom: 1px solid var(--border);
  box-shadow: inset 2px 0 0 var(--primary);
}
.bulkbar-n {
  margin-right: auto; font-size: var(--fs-body); font-weight: 600; color: var(--text);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.bulkbar-act {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  flex: none; min-height: 34px; min-width: 34px; padding: 0 8px;
  background: none; border: 1px solid transparent; border-radius: var(--r-xs);
  color: var(--text-dim); font: inherit; font-size: var(--fs-sm); cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--t-fast), color var(--t-fast);
}
.bulkbar-act:hover { background: var(--panel-4); color: var(--text); }
.bulkbar-act.danger { color: var(--danger-fg); }
.bulkbar-act:disabled, .bulkbar-act[aria-disabled="true"] {
  color: var(--text-mute); background: none; cursor: not-allowed;
}

/* ---------- the checkbox column on a message row ----------
   A real input rather than a styled span, because accent-color already paints
   it in --primary on both pages, the space bar already toggles it, and a screen
   reader already announces it as a checkbox with a state. Nothing hand rolled
   would arrive with all three.

   align-self:start with a 2px nudge, so on the three line phone row the box
   lines up with the sender rather than floating at the vertical centre of a
   76px row. */
/* This has to be a <label> wrapping the input, not a <span> beside it. Below
   900px the last 16px of the tap target is a transparent pseudo element, and a
   pseudo element on a span is not a target for anything, while a label's
   generated box activates the control the label owns. A bare 18px checkbox
   measured 18x18 at 390px, which is the defect this shape exists to close. */
.msg-check {
  position: relative; display: grid; place-items: center;
  width: 24px; height: 24px; flex: none; align-self: start; margin-top: 2px;
}
.msg-check input[type="checkbox"] { width: 18px; height: 18px; margin: 0; cursor: pointer; }

/* ---------- keyboard shortcut sheet ----------
   Two columns on a desktop, because the list runs to about twenty rows and a
   single column of twenty rows is a scroll inside a dialog whose whole job is
   to be read at a glance. */
.kbd-sheet { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--sp-7); }
.kbd-group { break-inside: avoid; margin-bottom: var(--sp-4); min-width: 0; }
.kbd-group-h {
  font-size: var(--fs-over); line-height: var(--lh-over); font-weight: 700;
  letter-spacing: .09em; text-transform: uppercase; color: var(--text-mute);
  padding: var(--sp-3) 0 var(--sp-1);
}
.kbd-row {
  display: flex; align-items: center; gap: var(--sp-3); min-height: 32px; padding: 2px 0;
  border-bottom: 1px solid var(--border-soft);
  font-size: var(--fs-body); color: var(--text-dim); min-width: 0;
}
.kbd-row:last-child { border-bottom: 0; }
.kbd-row > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.kbd-keys { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; flex: none; }
.kbd-then { font-size: var(--fs-cap); color: var(--text-mute); }
/* An element selector, because a key cap is a <kbd> and giving it a class as
   well means a cap written without the class arrives as the user agent's idea
   of monospace on a dark ground. The inset shadow is the only place in this
   file that draws a physical object, and it is one pixel of it. */
kbd {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 22px; height: 22px; padding: 0 6px;
  font-family: var(--mono); font-size: var(--fs-cap); line-height: 1;
  color: var(--text); background: var(--panel-4);
  border: 1px solid var(--border-strong); border-radius: var(--r-xs);
  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .45);
}

/* ---------- dropdown menu ----------
   z-index 160 puts this above the 150 section 15 gives to sheets and modals and
   below the 200 it gives to toasts. That is an addition to the table rather
   than a contradiction of it: the first real consumer is the send later menu,
   which opens from a button inside the compose sheet, and a popover that its
   own sheet paints over is a popover nobody can use. It stays under toasts
   because a toast must never be covered and is never interactive. */
.menu {
  position: absolute; z-index: 160; min-width: 200px; max-width: min(92vw, 320px);
  max-height: min(60vh, 360px); overflow-y: auto; overscroll-behavior: contain;
  padding: 4px; background: var(--panel-3);
  border: 1px solid var(--border-strong); border-radius: var(--r-sm);
  box-shadow: var(--e3);
}
.menu[hidden] { display: none; }
.menu-item {
  display: flex; align-items: center; gap: 10px; width: 100%; min-height: 34px;
  padding: 5px 9px; background: none; border: 0; border-radius: var(--r-xs);
  color: var(--text-dim); font: inherit; font-size: var(--fs-body);
  text-align: left; text-decoration: none; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--t-fast), color var(--t-fast);
}
.menu-item:hover, .menu-item[aria-selected="true"] {
  background: var(--panel-4); color: var(--text); text-decoration: none;
}
.menu-item .ic { color: var(--text-mute); }
.menu-item:hover .ic, .menu-item[aria-selected="true"] .ic { color: var(--text-dim); }
/* The trailing half of a row: a resolved time, a count, a key cap. Pushed right
   rather than laid out as a second column, so a menu holding one note and six
   plain rows does not reserve a column for the one. */
.menu-note {
  margin-left: auto; flex: none; font-size: var(--fs-cap); color: var(--text-mute);
  font-variant-numeric: tabular-nums;
}
.menu-item.danger { color: var(--danger-fg); }
.menu-item:disabled, .menu-item[aria-disabled="true"] {
  color: var(--text-mute); background: none; cursor: not-allowed;
}
.menu-sep { height: 1px; margin: 4px 6px; background: var(--border); border: 0; }

/* ---------- the send later picker ----------
   Presets first and the custom row last, because the presets answer the
   question almost every time and a date box opening first makes everybody read
   a calendar to say "tomorrow morning".

   The two boxes are native date and time inputs. color-scheme:dark is already
   on :root in both files, and that is what makes the platform paint its own
   calendar popup and its own spin buttons dark; without it this is the exact
   control that arrives white on a near black page. */
.when-pick { min-width: 236px; }
.when-preset {
  display: flex; align-items: center; gap: 10px; width: 100%; min-height: 38px;
  padding: 5px 9px; background: none; border: 0; border-radius: var(--r-xs);
  color: var(--text-dim); font: inherit; font-size: var(--fs-body);
  text-align: left; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--t-fast), color var(--t-fast);
}
.when-preset:hover { background: var(--panel-4); color: var(--text); }
.when-preset .when-at {
  margin-left: auto; flex: none; font-size: var(--fs-cap); color: var(--text-mute);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.when-custom { display: flex; align-items: center; gap: var(--sp-2); padding: 6px 9px 8px; }
.when-custom input {
  flex: 1 1 0; min-width: 0; min-height: 34px; padding: 5px 8px;
  background: var(--field); border: 1px solid var(--border-strong);
  border-radius: var(--r-xs); color: var(--text);
  font: inherit; font-size: var(--fs-sm);
}
/* Border and ground only. The ring is the one on :focus-visible at the top of
   this file, and drawing a second one here as a shadow would put two rings of
   two different blues around the same box. */
.when-custom input:focus { border-color: var(--primary); background: var(--field-focus); }
.when-foot { display: flex; align-items: center; gap: var(--sp-2); padding: 0 9px 8px; }
.when-foot .spacer { flex: 1 1 auto; }

/* ---------- phase 2 surfaces below 900px ----------
   Section 8: 44x44, and the hit area grows rather than the glyph. */
@media (max-width: 899.98px) {
  /* 44 for the entry box plus 5px of padding either side. The chip is 40 and
     not 44 so that a wrapped row of chips is not four fifths air, and the last
     4px of its two buttons are bought back below with a pseudo element. */
  .chips { min-height: 54px; padding: 5px 6px; }
  .chips .chip { height: 40px; padding: 0 3px 0 12px; font-size: 13.5px; }
  /* 40 plus 2 either side is 44. Two is also what fits: wrapped chip rows are
     6px apart, so a taller spill would put this row's hit area inside the row
     above it and a tap on the gap would edit the wrong recipient. */
  .chips .chip-label::after { content: ""; position: absolute; inset: -2px 0; }
  .chips .chip-remove { width: 30px; height: 30px; }
  /* 30 plus 7 either side is 44. The inset spills into the 6px gap between
     chips, which is the gap and not the next chip's own remove button. */
  .chips .chip-remove::after { content: ""; position: absolute; inset: -7px; }
  /* An input cannot be given a pseudo element in every engine, so this one is
     physically 44 rather than 24 wearing a bigger hit box. 16px, or iOS Safari
     zooms the whole page on focus and never zooms back: the rule that does this
     elsewhere matches input and textarea by tag inside .field, and this box is
     neither of those things once it is inside .chips. */
  .chips .chip-entry { min-height: 44px; font-size: 16px; }

  .rte-btn { min-height: 44px; min-width: 44px; padding: 0 12px; font-size: 13.5px; }
  .rte-sep { margin: 6px 3px; }

  .bulkbar { min-height: 56px; padding: 8px 8px 8px 12px; }
  .bulkbar-act { min-height: 44px; min-width: 44px; padding: 0 10px; }

  .msg-check { width: 28px; height: 28px; }
  .msg-check::after { content: ""; position: absolute; inset: -8px; }

  .kbd-sheet { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .kbd-row { min-height: 44px; }
  kbd { min-width: 26px; height: 26px; font-size: var(--fs-sm); }

  .menu { min-width: min(92vw, 260px); }
  .menu-item { min-height: 44px; }

  .when-pick { min-width: 0; }
  .when-preset { min-height: 48px; }
  .when-custom input { min-height: 44px; font-size: 16px; }
}

/* ==========================================================================
   RESPONSIVE SYSTEM

   Measured before writing: at 390px, 14 of 17 console views had content running
   past the right edge of the viewport. In every case the cause was the same one
   thing, a data table. Storage, Created and all three row action buttons on the
   Mailboxes screen sat off-screen and could not be reached at all, and the row
   actions are the entire purpose of that page.

   A table cannot be made to fit a phone by shrinking it. Below 760px each row
   therefore stops being a row and becomes a card of label/value pairs, with the
   actions as a full width strip at the bottom. The column headings are carried
   across by a data-label attribute that console.js stamps from <thead>, so this
   works on all seventeen views without touching a single one of them.

   The rail was the other half of that measurement and it was fixed wrongly.
   Turning it into a horizontal scroller moved the problem rather than solving
   it: measured again at 390px, the rail reported scrollWidth 2275 against
   clientWidth 390, and the fifteen rows from Domains to Audit log sat past the
   visible edge with scrollbar-width:none hiding the only hint that a tail
   existed at all. Sign out went with them. Below 900px the rail is now a drawer
   that slides in over a scrim, which is section 11 of docs/UI-SPEC.md, and
   every row is a full width 44px target with its label attached.

   Breakpoints, and the one thing each is responsible for:
     1320   KPI grid 4 -> 3 columns
     1180   two column page grids collapse to one
      900   the rail becomes a slide-in drawer, phone chrome appears
      760   data tables become cards, modals go full height
      560   KPI grid to a single column

   The drawer query is max-width:900px and not 899.98px, and it has to stay that
   way: console.js reaches for the same breakpoint through
   matchMedia('(max-width: 900px)') to decide when the rail is inert and out of
   the tab order. At a viewport of exactly 900 a 899.98 stylesheet would paint a
   permanent desktop rail while the script had already marked it inert, so the
   rail would be on screen and unreachable. There is no min-width:900px rule in
   this file for it to collide with, which is the condition that forces the
   fractional value in mail.html.
   ========================================================================== */

/* ---------- 900: the rail becomes a drawer ----------

   The contract with console.html and console.js, which own the markup and the
   wiring, is exactly these five names:

     #navDrawer      the rail element itself, aside.sidebar
     .mobile-topbar  the phone header that carries the hamburger
     #navToggle      the hamburger, with aria-expanded and aria-controls
     .scrim          one empty div, at body level
     .drawer-open    the class on <body> that opens both

   This file owns the geometry and the transition. The three ways to close it
   are all script: a click on .scrim, the Escape key, and a click on a nav row.
   A stylesheet cannot listen for any of the three, so a build where console.js
   has not landed yet has a drawer that opens and does not close.

   The selector is written as #navDrawer plus .app > aside.sidebar so it is
   correct whichever way the markup lands. If the id goes on the aside, both
   halves select the same element. If the aside is wrapped in a #navDrawer div
   instead, the child combinator stops matching and only the wrapper slides, so
   the translate is never applied twice down a nested pair. */
@media (max-width: 900px) {

  /* min-height goes with the rail. .mobile-topbar is a body level sibling of
     .app, so a shell still holding 100dvh would add its 56px on top of a full
     viewport and leave every short page with a 56px scroll and nothing in it. */
  .app { display: block; min-height: 0; }
  .main { margin-left: 0; padding: var(--main-pt) var(--main-px) var(--sp-8); }

  #navDrawer, .app > aside.sidebar {
    position: fixed; inset: 0 auto 0 0; z-index: 70;
    width: var(--drawer-w);
    display: flex; flex-direction: column;
    padding: calc(var(--sp-3) + env(safe-area-inset-top, 0px)) var(--sp-2)
             calc(var(--sp-4) + env(safe-area-inset-bottom, 0px));
    overflow-y: auto; overscroll-behavior: contain;
    background: var(--surface);
    border-right: 1px solid var(--border-strong);
    box-shadow: var(--e3);
    /* translateX only, per section 7. Animating left would relayout the drawer
       on every frame and would never reach the compositor. */
    transform: translateX(-100%);
    /* Visibility is paired with the transform and the pair is asymmetric on
       purpose: instant on the way in, held to the end of the slide on the way
       out. A pane parked at -100% with visibility left alone stays tab
       focusable, stays in the accessibility tree, and still contributes to
       scrollWidth, which is how a closed drawer paints a horizontal scrollbar
       across the bottom of a phone. */
    visibility: hidden;
    transition: transform var(--t-base), visibility 0s linear 200ms;
  }
  body.drawer-open #navDrawer, body.drawer-open .app > aside.sidebar {
    /* translateX(0), never transform:none. A transform other than none makes
       the element the containing block for any fixed descendant, so a value of
       none here would switch that behaviour on for the 200ms of the slide and
       off again on arrival, which is a bug that cannot be reproduced on demand
       in devtools. */
    transform: translateX(0);
    visibility: visible;
    transition: transform var(--t-base), visibility 0s;
  }
  body.drawer-open .scrim {
    opacity: 1; visibility: visible; pointer-events: auto;
    transition: opacity var(--t-base), visibility 0s;
  }
  /* The page behind a modal drawer must not scroll under it. overflow on body
     keeps the scroll offset, which the position:fixed trick does not. */
  body.drawer-open { overflow: hidden; }

  /* Everything the horizontal rail hid comes back: the group headings that said
     which section a row belonged to, and the whole foot, which is where the SES
     status, the signed-in identity and Sign out live. */
  .brand-text, .nav-group, .sidebar-foot { display: block; }
  .brand { padding: var(--sp-1) 6px var(--sp-3); }
  .nav-item {
    display: flex; align-items: center; gap: 10px;
    padding: var(--sp-2) 10px; min-height: 44px; border-radius: var(--r-xs);
    font-size: var(--fs-body); white-space: nowrap;
  }
  .nav-item .label { display: inline; }
  .nav-item .ico, .nav-item .ic { width: 18px; height: 18px; }
  .nav-item.active { box-shadow: inset 3px 0 0 var(--primary); }
  .sidebar-foot .btn { width: 100%; min-height: 44px; }

  .mobile-topbar {
    display: flex; align-items: center; gap: 10px;
    position: sticky; top: 0; z-index: 45;
    min-height: var(--topbar-h);
    padding: calc(var(--sp-2) + env(safe-area-inset-top, 0px)) var(--main-px) var(--sp-2);
    background: var(--surface); border-bottom: 1px solid var(--border);
  }
  /* Bleeds to both edges only when it actually sits inside the padded content
     column. As a body level sibling it keeps its own box, because a negative
     margin there would push it past the viewport and produce exactly the
     horizontal bleed this whole section exists to remove. */
  .main > .mobile-topbar {
    margin: calc(var(--main-pt) * -1) calc(var(--main-px) * -1) var(--sp-3);
  }
  .mobile-topbar .brand-mark { width: 26px; height: 26px; font-size: var(--fs-sm); }
  .mobile-topbar .mobile-title, .mobile-topbar h1 {
    font-size: var(--fs-h2); line-height: var(--lh-h2); font-weight: 620; letter-spacing: var(--tr-16);
    color: var(--text); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .mobile-topbar .spacer { margin-left: auto; }
  #navToggle { width: 44px; height: 44px; margin-left: -6px; color: var(--text); }
  body.drawer-open #navToggle { background: var(--panel-3); }

  .topbar { flex-wrap: wrap; gap: 10px; }
  .topbar-actions { width: 100%; display: flex; flex-wrap: wrap; gap: var(--sp-2); }
  .topbar-actions .btn { flex: 1 1 auto; }
  .grid-2 { grid-template-columns: 1fr; }

  /* Section 8: 44x44 is the floor for anything that can be tapped. The 16px
     input floor is here rather than at 760 because an 850px tablet is a touch
     device too, and below 16px iOS Safari zooms the whole page on focus and
     never zooms back. */
  .btn { min-height: 44px; }
  .btn-sm { min-height: 44px; padding: 6px var(--sp-3); }
  .icon-btn { width: 44px; height: 44px; }
  .input, select.input, textarea.input { min-height: 44px; font-size: 16px; }
  .tab { padding: 11px var(--sp-3); min-height: 44px; }
  /* No padding of its own: the 44px input below already sets the row height, so
     the padding only made the label 52px and broke the rhythm of the column. */
  .check { min-height: 44px; padding: 0; font-size: var(--fs-h3); }
  /* The element is the tap target, not the label wrapped around it. Section 8
     asks for the hit area to grow and the glyph to stay put, and both happen
     here: the input is 44x44, the ink stays 22px because it is a background
     centred in that box, and the negative margin hands the extra 22px back to
     the layout so the row keeps the geometry it had. Leaning on the label was
     the alternative and it does work - a click at the far corner of the 110x44
     label does toggle the box, measured - but the label is the wrong element to
     rely on: verifyForce sits in a panel head, another checkbox could be
     written without a wrapping label tomorrow, and an audit that measures the
     control is right to measure the control. Measured at 390: input 22x22 to
     44x44, label height 44 before and after, and the glyph now lands on the
     field column's left edge at x=27 instead of 4px inside it. */
  input[type="checkbox"], input[type="radio"] {
    width: 44px; height: 44px; margin: -11px; background-size: 22px 22px;
  }
  /* The ring belongs around the ink, not around the hit area. Pulling it in by
     11px less the 2px the global rule offsets leaves it 2px clear of the 22px
     glyph, the same as it sits on every other control. */
  input[type="checkbox"]:focus-visible, input[type="radio"]:focus-visible { outline-offset: -9px; }
  /* A link inside a table cell is that row's real action on a phone, which at
     an 18px line box it cannot be. */
  table.data td a { display: inline-flex; align-items: center; min-height: 44px; }

  /* Between 760 and 900 a data table is still a table inside a horizontal
     scroller, and there its width matters more than the desktop row rhythm.
     Measured at 768px, 16px of cell padding pushed the message log to 940px
     against a 742px port where 12px holds it to 896px, which is where it sat
     before this pass. Below 760 the card rules replace this padding entirely,
     so the rule only ever applies in that 140px band. */
  table.data th, table.data td { padding-left: var(--sp-3); padding-right: var(--sp-3); }
}

/* ---------- 760: tables become cards ---------- */
@media (max-width: 760px) {

  /* The horizontal scroller is what hid the amputated columns. Once rows are
     cards there is nothing left to scroll, so it must not clip either. */
  .table-wrap { overflow-x: visible; }

  table.data { display: block; font-size: var(--fs-h3); }
  table.data thead { display: none; }
  table.data tbody { display: block; }

  table.data tbody tr {
    display: block;
    background: var(--panel);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    padding: var(--sp-1) var(--sp-3) 6px;
    margin-bottom: 10px;
  }
  table.data tbody tr:hover td { background: none; }
  table.data tbody tr:last-child { margin-bottom: 2px; }

  /* Block, not grid. A cell can hold several inline children (a pill next to a
     value, a link next to a note) and each one would become its own grid item,
     stretched to the column width. Block layout lets them flow as written. */
  table.data td {
    display: block;
    padding: 7px 0;
    border-bottom: 1px solid var(--border-soft);
    line-height: 1.45;
    white-space: normal;
    word-break: break-word;
  }
  table.data tbody tr td:last-child { border-bottom: none; }

  table.data td::before {
    content: attr(data-label);
    display: block; margin-bottom: 2px;
    font-size: 11px; font-weight: 600; letter-spacing: .05em;
    text-transform: uppercase; color: var(--text-mute);
    line-height: 1.5;
  }
  /* Numbers stop being right aligned once the column is gone; against a label
     on the left, right alignment just floats them away from what names them. */
  table.data td.num { text-align: left; }

  /* Action cells: no label, full width, real tap targets. */
  table.data td.actions,
  table.data td.row-actions,
  table.data td[data-actions] {
    display: flex; flex-wrap: wrap; gap: var(--sp-2);
    padding-top: 10px; margin-top: 2px;
    border-top: 1px solid var(--border); border-bottom: none;
  }
  table.data td.actions::before,
  table.data td.row-actions::before,
  table.data td[data-actions]::before { content: none; }
  table.data td.actions .btn,
  table.data td.row-actions .btn,
  table.data td[data-actions] .btn { flex: 1 1 auto; }

  /* A cell the loader deliberately left blank should collapse, not leave an
     orphan heading with nothing under it. */
  table.data td[data-empty]::before { content: none; }
  table.data td[data-empty] { display: none; }

  /* Empty state and pager are not rows and must not be carded. */
  table.data td[colspan] { display: block; border: none; padding: 0; }
  table.data td[colspan]::before { content: none; }
  table.data tr:has(td[colspan]) { background: none; border: none; padding: 0; margin: 0; }

  .truncate { max-width: none; white-space: normal; overflow: visible; }

  .pager { flex-wrap: wrap; gap: var(--sp-2); }
  .pager .count { width: 100%; margin-right: 0; }
  .pager .btn { flex: 1 1 auto; }

  /* Modals: a centred box on a phone leaves content under the fold with no way
     to reach it, so they become a full height sheet. The sheet is a flex column
     and the body is the only scrolling part of it, which is what puts the
     action bar on the bottom edge of the screen. Measured before this: with the
     sheet stretched to 844px and only 520px of content, a sticky foot pinned
     nothing and the Cancel and Create pair floated in the middle of the sheet
     with 330px of empty panel under them. */
  .modal {
    width: 100%; max-width: none; height: 100dvh; max-height: 100dvh; border-radius: 0;
    display: flex; flex-direction: column; overflow: hidden;
  }
  .modal-backdrop { padding: 0; align-items: stretch; }
  .modal-head { flex: none; }
  .modal-body { flex: 1 1 auto; min-height: 0; max-height: none; overflow-y: auto;
                overscroll-behavior: contain; padding: var(--sp-4); }
  .modal-head, .modal-foot { padding: var(--sp-3) var(--sp-4); }
  .modal-foot { flex: none; position: static; background: var(--surface); }
  .modal-foot .btn { flex: 1 1 auto; }

  .row { grid-template-columns: 1fr; }
  .panel-head { gap: 6px; }
  .panel-note { margin-left: 0; width: 100%; }
  .skeleton-row { grid-template-columns: minmax(0,1fr) 80px; row-gap: var(--sp-2); }

  /* Charts: axis labels at 11px on a phone are decoration, not information. */
  .chart-axis { font-size: 12.5px; }
  .chart-axis-sm { font-size: 11.5px; }
  .chart-donut-value { font-size: 24px; }
  .chart-legend { gap: 6px var(--sp-3); }
  .chart-legend-item { font-size: var(--fs-sm); }
  .chart-bar-row { grid-template-columns: 1fr; gap: var(--sp-1); }
  .chart-bar-value { justify-self: start; }

  /* Chips are clickable merge tags, so they are targets too. The .static ones
     are labels and are left at their reading size. */
  .chip { min-height: 44px; display: inline-flex; align-items: center; padding: var(--sp-1) var(--sp-3); font-size: var(--fs-sm); }
  .chip.static { min-height: 0; padding: 2px 7px; }

  /* Micro labels sit at 10.5px on a desktop, where they are read at arm's length
     against a large surface. On a phone held closer but with far less light and
     far more motion they stop being legible, so the whole small end of the scale
     lifts by a point. */
  .kpi .label { font-size: var(--fs-cap); }
  .kpi .value { font-size: 26px; }
  .kpi .foot  { font-size: var(--fs-sm); }
  .sub, .hint { font-size: var(--fs-sm); }
  .pill { font-size: 11px; }
  table.data th { font-size: var(--fs-sm); }
}

/* ---------- 560 ---------- */
@media (max-width: 560px) {
  .main { padding: var(--main-pt) 10px var(--sp-8); }
  .main > .mobile-topbar { margin-left: -10px; margin-right: -10px; }
  h1, .topbar h1 { font-size: var(--fs-h1); }
  table.data td::before { font-size: var(--fs-over); }
  .modal-body { padding: var(--sp-3); }

  /* A code block is a scroller on a phone whatever we do: the API example is one
     62 character curl line set in the mono ramp, 454px of ink against a 390px
     port, and shrinking the type to make it fit would be lying about the size of
     the thing. What it must not do is pay for the same gutter twice. The panel
     body already insets 16px and the block was insetting another 16px inside
     that, so 302px of the line was visible and 152px was not. Taking the panel's
     full inner width and keeping 12px of its own padding, so the code still does
     not touch the edge, shows 344px instead. Measured at 390: 152px hidden, down
     to 110px, against 126px before this pass. The side border and the radius go
     with the inset, because a band that reaches the panel wall should read as a
     band and not as a card that has burst its box. */
  .panel-body > pre.code {
    margin-left: calc(var(--sp-4) * -1); margin-right: calc(var(--sp-4) * -1);
    padding-left: var(--sp-3); padding-right: var(--sp-3);
    border-left: 0; border-right: 0; border-radius: 0;
  }
}

/* ==========================================================================
   Reduced motion

   Section 7: everything drops to none. One blanket rule rather than a list,
   because the list is the part that goes stale. A transition added six months
   from now is covered by the blanket and would not be covered by a list naming
   the eight selectors that happened to exist today. Durations rather than the
   transition shorthand, so a property still lands on its final value and any
   state that is only reachable through a transition end is still reachable.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0s !important;
    transition-delay: 0s !important;
    scroll-behavior: auto !important;
  }
  /* The drawer and the scrim pair a zero length visibility transition with the
     movement, and zeroing the delay on the closing half would leave a closed
     drawer on screen. Both are restated so the pair still resolves. */
  #navDrawer, .app > aside.sidebar, .scrim { transition: none !important; }
  /* A shimmer with no motion is a flat bar, which is the correct still frame:
     it still reserves the row and still reads as not-yet-content. */
  .skeleton { animation: none !important; background-image: none; }
}
