/* =========================================================================
   wireagent — design system
   =========================================================================

   WHAT WAS TAKEN, AND FROM WHERE. All of it verified against the sources,
   not recalled.

   ── Base44 ────────────────────────────────────────────────────────────
   · Palette shape: "a bright, energetic green and deep charcoal, balanced
     by ample white". That is the palette below, exactly.
   · "Geometric sans-serif headers for authority and legibility with
     approachable body copy" — headings are geometric and tightly tracked,
     body is a neutral UI face at a comfortable size.
   · "Sticky and ultra-clean" navigation. See .side (the rail; there is no top bar).
   · CTAs "bold but not aggressive" — solid green fill, no gradient, no glow
     on the button itself.
   · Radii taken verbatim: 8px inputs/cards, 30px large cards, 9999px
     buttons/tags. Spacing on a 4px base: 4 8 12 16 20 24 28 32 40 80 120.
   · Their own stated motion principle, honoured literally:
       "Motion should support clarity, not hide weak structure."
     Every animation here either shows where something came from, or
     confirms a state change. Nothing moves for decoration.
   · Their docs also say to respect reduced-motion. See the media query at
     the bottom — it disables all of it, not just some.
   · Glassmorphism is one of the five design themes Base44 names. That is
     the licence for the liquid-glass material below.

   ── Palantir (Blueprint, Apache-2.0) ──────────────────────────────────
   · The neutral ramp, verbatim: #111418 → #404854 → #5F6B7C → #C5CBD3.
     Cool greys, which is why the green reads as a signal rather than as
     decoration — warm greys would fight it.
   · Monospace for values, never for prose.
   · "Competence over friendliness" — the console states facts and does not
     congratulate you.

   ── The material: liquid glass ────────────────────────────────────────
   Glass only works if there is something behind it worth refracting, so
   the page carries slow green aurora behind everything. Glass is applied
   to CHROME ONLY — app bar, cards, drawer. Dense list rows stay opaque,
   because blurring the thing behind a 40-row list costs legibility and
   buys nothing. That split is the whole trick to using it well.

   =========================================================================
   2026-08-11 RESTRUCTURE — what changed, and why it is not cosmetic
   =========================================================================

   1. THE COLUMN IS CENTRED. The old shell was `grid-template-columns:
      236px 1fr` with a 236px sticky rail, and `.content` carried
      `max-width: 1040px` with NO auto margin. So every screen was pinned
      to the left edge of the remaining space and, on a wide monitor, the
      whole product sat left-of-centre with a large dead area to its right.
      Navigation now lives in a centred top bar whose inner width matches
      the content column, so the logo, the nav and the content all share
      one vertical rhythm and the eye has a single centre to return to.

   2. CONTRAST IS MEASURED, NOT ESTIMATED. Every foreground token below
      carries its computed WCAG ratio against the WORST background it can
      land on (--surface-2 #1c2127, the lightest list-row fill). The old
      --faint #5f6b7c scored 2.99:1 there and was the colour of every
      10px micro-label in the product — the single worst legibility
      decision in the file. It is now #7e8896, the first value along the
      ramp that clears 4.5:1, and those labels are 11px rather than 10px.

   3. FOCUS IS VISIBLE, AND SURVIVES HIGH CONTRAST. Focus rings are drawn
      with `outline`, never with `box-shadow` alone: Windows High Contrast
      / forced-colors drops box-shadow entirely, which silently removes
      the only focus indicator on the page. See the forced-colors block.

   4. TARGETS ARE BIG ENOUGH TO HIT. Interactive controls have a minimum
      height (--tap 40px, --tap-sm 32px) rather than being sized purely by
      their text.

   Anything not listed here is unchanged on purpose. The palette, the
   glass, the motion principle and the provenance above are the product's
   identity and were not the problem.

   =========================================================================
   2026-08-15 RE-THEME — the console joins the front door
   =========================================================================

   The landing and the coming-soon gate moved to a "night" language: a deep
   INDIGO base, Fraunces for display type, and WHITE as the one accent — a
   monochrome, editorial look. The console now speaks the same language, so
   walking through the front door no longer changes products.

   · The neutral ramp is the landing's indigo ramp, not Blueprint's cool
     grey. Every ratio below is RE-MEASURED against the new lightest
     surface, --surface-2 #15203c — the numbers are computed, not copied,
     and test/a11y.mjs recomputes them.
   · --green now holds WHITE. It is the ACCENT SLOT, kept under its old
     name so its ~40 consumers need no rename: buttons, focus rings,
     selected tabs and chart marks all read from it. --green-dim is the
     mid step (quiet bars, secondary donut segments), --green-deep the
     deep track behind meters and gauges.
   · The landing's own --control-border (#3b4a70) measures 1.84:1 on the
     new --surface-2 and FAILS WCAG 1.4.11, so the console carries its own
     #66759f (3.54:1). A marketing page can afford a decorative input
     edge; a console cannot.
   · Status stays semantic: --warn and --bad are unchanged, and with the
     accent monochrome they are the only saturated colours left — which is
     exactly why they read as signals.
   · Fraunces carries h1/h2 only. Values stay in the monospace face
     (Palantir's rule outlives Palantir's palette), and the logo wordmark
     stays sans, as on the landing.
   · Structure, motion, radii, spacing and the a11y work above are
     untouched. This is a re-skin.
   ========================================================================= */

/* The same self-hosted faces the landing embeds: Fraunces (roman + true
   italic) for display, Inter variable for body. Declared here as well
   because the console never loads site.css. */
@font-face {
  font-family: "Fraunces";
  src: url("/fonts/fraunces.woff2") format("woff2");
  font-weight: 200 900; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Fraunces";
  src: url("/fonts/fraunces-italic.woff2") format("woff2");
  font-weight: 200 900; font-style: italic; font-display: swap;
}
@font-face {
  font-family: "InterVar";
  src: url("/fonts/inter.woff2") format("woff2");
  font-weight: 100 900; font-style: normal; font-display: swap;
}

:root {
  /* ── Night */
  --void:      #05070f;
  --bg:        #0a0f1e;

  /* ── Indigo ramp: the landing's, promoted to the whole product */
  --surface:       #0f1730;
  --surface-2:     #15203c;
  --surface-3:     #1c294a;
  --border:        #232f4e;
  --border-strong: #33436b;
  --grey:          #6b7aa0;
  --grey-2:        #8b9ac0;
  --grey-3:        #c4d0ec;

  /* ── Text. Ratios are measured against --surface-2 #15203c, the lightest
        background any of them can sit on, so they hold everywhere else too. */
  --text:  #eaf0ff;   /* 14.12:1  AAA */
  --white: #ffffff;
  --muted: #9daecf;   /*  7.20:1  AA  — secondary prose */
  --faint: #808fb6;   /*  5.00:1  AA  — micro-labels, counters, timestamps */

  /* Borders. --glass-edge is decorative (it edges panels); --control-border
     is the one that must clear WCAG 1.4.11 non-text contrast at 3:1,
     because it is the only thing defining an input or a ghost button.
     NOT the landing's #3b4a70, which measures 1.84:1 here and fails. */
  --control-border: #66759f;  /* 3.54:1 on --surface-2, 4.19:1 on --bg */

  /* ── The accent: WHITE, as on the landing. The slot keeps its old name
        so its ~40 consumers need no rename — read "--green" as "the
        accent", not as a hue. */
  --green:        #ffffff;    /* 16.11:1 on --surface-2 */
  --green-bright: #f2f5ff;
  --green-dim:    #8fa2dd;    /* mid step: quiet bars, secondary segments — 6.43:1 */
  --green-deep:   #1e2a52;    /* deep track behind meters and gauges */
  --green-ink:    #0a1024;    /* 18.88:1 inside a white button */

  /* Status. With the accent monochrome these are the only saturated
     colours in the system, which is exactly why they read as signals. */
  --warn:      #ec9a3c;       /*  7.10:1 */
  --warn-soft: #2a1f10;
  --bad:       #e76a6e;       /*  5.13:1 */
  --bad-soft:  #2b1416;

  /* ── Radii, from Base44 */
  --r-sm:   8px;
  --r:      8px;
  --r-lg:   30px;
  --r-pill: 999px;

  /* ── Glass. The tints are indigo light now, matching the aurora the
        panes refract — the landing's values, verbatim. */
  --glass:        rgba(130, 150, 225, .05);
  --glass-strong: rgba(130, 150, 225, .08);
  --glass-edge:   rgba(140, 162, 230, .16);
  --glass-spec:   rgba(200, 215, 255, .16);
  --blur: blur(22px) saturate(150%);

  --lift: 0 6px 20px rgba(0, 0, 0, .07);          /* Base44's single shadow */
  --lift-glass: 0 24px 70px -22px rgba(0, 1, 8, .8);

  /* ── Layout.
   *
   * 2026-09-08: THE CONSOLE COLUMN IS FLUID. `--page` was 72rem, a 1152px cap,
   * and the column stopped growing above roughly a 1400px viewport. Measured on
   * the machine this is built on: 31% of a 1920px screen was empty margin and
   * 50% of a 2560px one, while cards inside that column hid their own content.
   *
   * A CRM is judged on how much you can see at once, so the container fills the
   * window and the MEASURE moves to the text instead, which is where it always
   * belonged: `--measure` below caps a line of prose without capping the layout
   * around it. A table gets the whole window; a paragraph gets 70 characters.
   *
   * `--page` stays as a name because the gate screens and the prose pages still
   * want a real cap, and because one token that means "how wide may a column
   * be" is easier to reason about than three.
   */
  --page:        100%;    /* the console column: as wide as the window */
  --page-narrow: 32rem;   /* single-purpose gate screens: unlock, sign in */
  --page-prose:  46rem;   /* anything that is mostly words */
  --measure:     70ch;    /* a readable line, applied to text and not to layout */
  --gutter: clamp(16px, 4vw, 32px);

  /* Minimum hit target. WCAG 2.5.8 asks for 24px; 40 is comfortable and
     still dense enough for a console. */
  --tap:    40px;
  --tap-sm: 32px;

  --ease: cubic-bezier(.22, 1, .36, 1);

  /* The mark is drawn with these so it can invert between the dark console
     and the light landing page without a second copy of the SVG. */
  --logo-tile: var(--white);
  --logo-ink:  var(--void);

  --mono: ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, monospace;
  --sans: "InterVar", "Inter", ui-sans-serif, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --serif: "Fraunces", Georgia, "Times New Roman", serif;
}

* { box-sizing: border-box; }

/**
 * Load-bearing. The UA's `[hidden] { display: none }` loses to any author
 * rule that sets display, and this file sets display on .card, .panel and
 * #view-signedin (the panel/signedin grid rule lives with the panel
 * styles; it was missing until 2026-08-17 and every panel's boxes sat
 * flush). Without this !important, every panel renders at once, stacked.
 */
[hidden] { display: none !important; }

/* scrollbar-gutter: a page that does not scroll is 15px wider than one that
   does, so the centred column jumped sideways between one page and the next.
   Reserving the gutter keeps every page on the same centre. */
html { color-scheme: dark; scrollbar-gutter: stable; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  /* 16px base. The old 14px pushed the whole scale down to 10px labels,
     which is the root of "hard to see" rather than any one colour. */
  font: 16px/1.6 var(--sans);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  position: relative;
  /* `dvh` with `vh` above it as the fallback. On iOS Safari `100vh` counts the
     space behind the collapsing address bar, so the bottom of the page sits
     under browser chrome and cannot be reached. comingsoon.html has used
     `100dvh` since it was written; the console never got the change. */
  min-height: 100vh;
  min-height: 100dvh;
}

/**
 * The aurora. Slow indigo fields with a white pulse, far behind everything,
 * so the glass has something to refract. Without this, "glass" is just a
 * grey box. The same weather as the landing, still drifting.
 */
body::before {
  content: "";
  position: fixed;
  inset: -20vmax;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(42vmax 42vmax at 14% 2%,  rgba(255, 255, 255, .12), transparent 60%),
    radial-gradient(52vmax 52vmax at 86% 98%, rgba(66, 92, 214, .34), transparent 62%),
    radial-gradient(38vmax 38vmax at 74% 8%,  rgba(210, 220, 255, .06), transparent 66%),
    radial-gradient(46vmax 46vmax at 8% 84%,  rgba(88, 70, 200, .20), transparent 64%);
  filter: blur(24px);
  animation: drift 34s var(--ease) infinite alternate;
}

@keyframes drift {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(2.5%, -2%, 0) scale(1.08); }
}

/* =========================================================================
   Type

   Palantir's scale, shifted up one step. Their ramp is built for dense
   analyst tooling on large monitors; this is a personal console, and the
   brief was "easy to see", so every step gains 1-2px and line-heights
   loosen slightly. The tight tracking on headings is Base44's and stays.
   ========================================================================= */

h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -.022em; }
/* Display type is Fraunces, as on the landing — but only where a heading is
   a TITLE (h1/h2). h3/h4 caption figures and controls, and a serif at 14px
   reads as decoration, so they stay in the working sans. */
h1, h2 { font-family: var(--serif); font-weight: 540; letter-spacing: -.012em; }
h1 { font-size: 24px; line-height: 30px; }
h2 { font-size: 20px; line-height: 26px; }
h3 { font-size: 17px; line-height: 23px; }
h4 { font-size: 15px; line-height: 20px; }

p { margin: 0; }

a { color: var(--green); text-decoration-color: color-mix(in srgb, var(--green) 40%, transparent); }
a:hover { color: var(--green-bright); }

.muted { color: var(--muted); }
.small { font-size: 13.5px; line-height: 1.5; }

/** Uppercase micro-label. Base44's 0.18em tracking, verbatim — but at 11px,
    not 10px, and in --faint, which now actually passes AA. */
.eyebrow, .stat__l, .identity__badge, .nav__group {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .18em;
}

/**
 * Prose measure. An unconstrained paragraph inside a 72rem card runs to
 * ~140 characters, which is roughly twice the comfortable reading width
 * and is a legibility problem independent of size or colour.
 */
.card > p, .card > .stack > p, .prose { max-width: var(--page-prose); }

/* =========================================================================
   Focus

   One ring, everywhere, drawn with `outline`. Never box-shadow alone:
   forced-colors mode discards box-shadow and would leave no indicator at
   all. `:focus-visible` so a mouse click does not ring the control, but
   `:focus` is kept for elements scripted with .focus().
   ========================================================================= */

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

/* Skip link: present for keyboard users, out of the way for everyone else. */
.skip {
  position: absolute; left: 50%; top: 8px;
  transform: translate(-50%, -200%);
  z-index: 100;
  background: var(--green); color: var(--green-ink);
  font-weight: 700; font-size: 14px;
  padding: 10px 18px; border-radius: var(--r-pill);
  text-decoration: none;
  transition: transform .18s var(--ease);
}
.skip:focus { transform: translate(-50%, 0); color: var(--green-ink); }

/** Visible only to assistive tech. Used for the live region and for labels
    whose visual equivalent is carried by an icon or by position. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* =========================================================================
   Glass material
   ========================================================================= */

.glass {
  position: relative;
  background: linear-gradient(160deg, var(--glass-strong), var(--glass));
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--glass-edge);
  box-shadow:
    inset 0 1px 0 var(--glass-spec),          /* specular top edge */
    inset 0 -1px 0 rgba(0, 0, 0, .28),        /* shaded bottom bevel */
    var(--lift-glass);
}

/** The sheen: light catching the curved top-left of the pane. */
.glass::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(140deg, rgba(255, 255, 255, .09) 0%, transparent 38%);
}

/* Fallback: without backdrop-filter, glass would be a washed-out smear. */
@supports not (backdrop-filter: blur(1px)) {
  .glass { background: var(--surface); }
}

/* =========================================================================
   Shell: a grouped left rail, and a content column centred BESIDE it
   =========================================================================

   2026-09-02 (docs/console-crm-2026-09-02.md). The rail returns, and the
   record says why that is not the thing the 2026-08-11 decision removed: the
   objection then was a content column with a max-width and no auto margin,
   which sat the product in the left third of a wide screen. `.wrap` fixed
   that and is still asserted. The column below is centred in the space
   beside the rail by exactly that rule, and the probe measures it at 1600
   and 2560, in both rail states.

   Glass on chrome only: the rail and the top bar are chrome, so they carry
   the blur; list rows stay opaque. */

/* 248, not 232 (2026-09-04): the search pill moved into the rail on
   2026-09-02 sized for the 22rem top bar, and at 232 its min-content
   (icon, "Search or jump to", the Ctrl K hint, gaps and padding: 226px)
   exceeded the rail's 207px content box. The grid column grew to fit it and
   EVERY rail row was stretched 6px past the rail's border into the column.
   The pill is also shrinkable now (min-width: 0 below), so a longer label
   can only ever truncate, never widen the rail again. */
:root { --side: 248px; --side-collapsed: 64px; }

/**
 * ONE SCREEN PER PAGE (2026-09-08, docs/console-one-screen-2026-09-08.md).
 *
 * `height`, not `min-height`. That one word is the defect this reverses: with
 * min-height the shell grew with its content, so the console was a scrolling
 * DOCUMENT wearing an app's clothes. Measured before the change, 10 of 13
 * panels ran past one screen at 1440x900, and with 40 rows of real data
 * Follow-ups reached 95 screens.
 *
 * The `vh` declaration before the `dvh` one is the same fallback `body`
 * already carries: a browser without `dvh` ignores the second and keeps the
 * first. Without it such a browser gets NO height at all and the shell
 * collapses, which is worse than the scrolling it replaced.
 *
 * `overflow` deliberately does NOT go here. `.side` is `overflow: visible` on
 * purpose, because the account menu opens upward out of the rail and is wider
 * than it, so clipping at this level would clip that menu. The clip belongs on
 * `.maincol`, the column that actually holds the pages.
 */
/*
 * ONE SCREEN IS A MINIMUM, NOT A CEILING (2026-09-08).
 *
 * This was `height: 100dvh`, which bound the whole console to exactly one
 * screen. Everything else followed from it: `.maincol` had to clip, `.content`
 * had to be 100% tall, panels became equal-height grid rows, and any card that
 * did not fit its cell scrolled INSIDE ITSELF.
 *
 * That last part was always known to be wrong, and the rule that did it said so
 * in as many words: "That is a worse experience and it is meant to be [...] the
 * cards that hit it are the ones phases 4 to 6 shorten so they stop needing
 * it." Those phases were never done, so the stopgap became the product. On
 * Settings, three cards were hiding 404px of themselves on a 2560px screen that
 * had half its width empty.
 *
 * `min-height` instead: the shell still fills a short screen, and a tall page
 * scrolls the PAGE, which is the one scrollbar people expect. This behaviour is
 * not new or untested. The small-screen block near the end of this file has
 * been doing exactly it for every viewport under 900px wide or 720px tall, so
 * it is what the console already does on a phone.
 */
.shell {
  min-height: 100vh; min-height: 100dvh;
  display: grid;
  grid-template-columns: var(--side) minmax(0, 1fr);
  transition: grid-template-columns .22s var(--ease);
}
.shell.side-collapsed { grid-template-columns: var(--side-collapsed) minmax(0, 1fr); }

/**
 * Load-bearing, and the cause of four separate horizontal-overflow bands until
 * 2026-08-21. A grid item defaults to `min-width: auto`: it refuses to shrink
 * below its own min-content width, so the column was sized by its contents
 * rather than by the viewport. `.maincol` is the grid item now and needs the
 * same rule, one level up from where it used to bind.
 */
.shell > *,
.maincol,
.wrap,
.content,
.card,
.stack { min-width: 0; }

/* One row: the column, and nothing above it. The bar and the band that sat
   here for one evening are gone (docs/console-reanalysis-2026-09-02.md).

   `align-content: start` is gone with them: the row must STRETCH to the shell's
   height or the column would size to its content again and there would be
   nothing for a page to fill. `overflow: hidden` is the clip, placed here
   rather than on `.shell` so the rail's account menu can still open past its
   own edge. */
/*
 * 2026-09-08: the clip is gone with the ceiling above it.
 *
 * `overflow: hidden` here was the thing that turned "the page is taller than
 * the screen" into "the content is cut off with no scrollbar to say so", which
 * is why every card underneath had to grow its own. With the shell free to be
 * taller than the viewport there is nothing to clip: the column grows and the
 * page scrolls.
 *
 * The row is `auto` rather than `minmax(0, 1fr)` for the same reason. Stretching
 * it to the shell's height was only necessary while that height was fixed.
 */
.maincol {
  display: grid;
  grid-template-rows: auto;
  min-height: 0;
}

/**
 * Every band in the layout uses this. It is what makes the product read as
 * centred: the search, the status pills and the content beneath all align to
 * the same two edges instead of each finding their own.
 */
.wrap {
  width: 100%;
  max-width: var(--page);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* ── the rail */

.side {
  position: sticky; top: 0;
  height: 100vh; height: 100dvh;
  /* Four rows: the head, the search, the nav (the one that stretches and
     scrolls), the account. The search was briefly in the 1fr row and became
     a 200px circle; the probe measures its height now. */
  display: grid; grid-template-rows: auto auto minmax(0, 1fr) auto;
  gap: 8px;
  padding: 14px 12px max(14px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
  background: rgba(10, 15, 30, .72);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border-right: 1px solid var(--glass-edge);
  box-shadow: inset -1px 0 0 rgba(200, 215, 255, .04);
  /* Visible, not clipped: the account menu opens upward out of the rail's
     bottom row and is wider than the rail. The NAV scrolls instead. */
  overflow: visible;
  z-index: 11;
}
@supports not (backdrop-filter: blur(1px)) { .side { background: var(--surface); } }

.side__head { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; padding: 2px 4px 6px; }
.side__collapse { flex: none; }
.side__collapse svg { transition: transform .22s var(--ease); }
.side-collapsed .side__collapse svg { transform: rotate(180deg); }

.rail {
  display: grid; gap: 2px; align-content: start; min-width: 0;
  /* The middle row of the rail's grid: min-height 0 lets it shrink, and it
     scrolls on its own so the head and the account block stay put. */
  min-height: 0; overflow: hidden auto; scrollbar-width: thin;
}

.rail__group { padding: 14px 10px 4px; }
.rail__title {
  font-family: var(--mono); font-size: 11px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--faint);
}
.rail__rule { border: 0; border-top: 1px solid var(--glass-edge); margin: 12px 8px 6px; }

.rail__item {
  font: inherit; font-size: 14px; font-weight: 600;
  background: transparent; border: none;
  color: var(--muted);
  min-height: var(--tap);
  padding: 8px 10px;
  border-radius: 10px;
  cursor: pointer;
  display: flex; align-items: center; gap: 11px;
  text-align: left; white-space: nowrap; min-width: 0;
  position: relative;
  transition: color .18s var(--ease), background .18s var(--ease);
}
.rail__item:hover { background: rgba(255, 255, 255, .06); color: var(--text); }
.rail__label { min-width: 0; overflow: hidden; text-overflow: ellipsis; flex: 1 1 auto; }
.rail__ico { width: 18px; height: 18px; flex: none; opacity: .85; }

/* The selected item is carried by fill, weight and a left bar, never colour
   alone: the bar survives forced-colors and reads for anyone who cannot tell
   a bright grey from a dim one. */
.rail__item--on {
  background: linear-gradient(180deg, rgba(255, 255, 255, .16), rgba(255, 255, 255, .08));
  color: var(--text);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .10), 0 1px 2px rgba(0, 0, 0, .3);
}
.rail__item--on::before {
  content: ""; position: absolute; left: -12px; top: 9px; bottom: 9px; width: 3px;
  border-radius: 0 3px 3px 0; background: var(--green); box-shadow: 0 0 8px var(--green);
}
.rail__item--on .rail__ico { opacity: 1; }
.rail__item .tab__badge { margin-left: auto; }

/* The live-run dot on the Discover item: on when a run is going, gone
   otherwise. A dot and not a count, deliberately (docs/console-reanalysis-2026-09-02.md). */
.rail__live {
  flex: none; margin-left: auto;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--green); box-shadow: 0 0 0 3px rgba(255, 255, 255, .18);
  animation: pulse 2.6s var(--ease) infinite;
}
.rail__live[hidden] { display: none; }

/* Collapsed: icons only, labels and titles gone, badges become a dot. The
   accessible name stays on the button (aria-label is set by app.js from the
   label), so a screen reader still hears "Approval".

   Desktop only (2026-09-04). The collapse is remembered in localStorage, and
   below 900px the rail is a row with the collapse control hidden, so a
   collapse chosen at a desk used to follow the person onto a phone: the row
   went icon-only with no way back, and the search pill shrank to an 18px
   sliver. The row form has its own rules in the 900px block. */
@media (min-width: 900.02px) {
.side-collapsed .side { padding-inline: 10px; }
.side-collapsed .logo__word,
.side-collapsed .rail__label,
.side-collapsed .rail__title,
.side-collapsed .side__acct .whoami__body,
.side-collapsed .side__acct .whoami__chev { display: none; }
.side-collapsed .rail__group { padding: 10px 0 0; }
.side-collapsed .rail__item { justify-content: center; padding-inline: 0; }
.side-collapsed .rail__item--on::before { left: -10px; }
.side-collapsed .rail__item .tab__badge {
  position: absolute; top: 6px; right: 6px; margin: 0;
  min-width: 8px; width: 8px; height: 8px; padding: 0; font-size: 0;
}
/* Collapsed, the mark and the expand button STACK: side by side they are
   76px in a 44px box, and the mark was pushed off the left edge of the
   screen. */
.side-collapsed .side__head { flex-direction: column; justify-content: center; gap: 4px; padding-bottom: 2px; }
.side-collapsed .side__acct .whoami { justify-content: center; padding-inline: 0; }
.side-collapsed .rail__live { position: absolute; top: 6px; right: 6px; margin: 0; }
/* Three classes, not two, and the reason is the one already written above
   .side .side__search: that rule is LATER in the file at equal specificity,
   so its `padding` shorthand was quietly winning over this rule's
   padding-inline and leaving the magnifier a pixel right of centre in a 64px
   rail. Out-specifying it makes source order stop mattering. */
.side-collapsed .side .side__search { justify-content: center; padding-inline: 0; }
.side-collapsed .side__search .search__label,
.side-collapsed .side__search .search__kbd { display: none; }
}

/* The rail is signed-in chrome. On a gate screen (sign in, challenge,
   reconnect) it goes, so the one decision on screen stays centred in the
   viewport and the gate probe is not relaxed to make room for it. */
body.gated .side { display: none; }
body.gated .shell { grid-template-columns: minmax(0, 1fr); }

/* ── the account, bottom-left of the rail
   Was the headroom footer ("75 invitations left today"), removed 2026-09-02
   evening at the owner's order; docs/console-reanalysis-2026-09-02.md. */

.side__acct {
  border-top: 1px solid var(--glass-edge);
  padding-top: 8px;
  min-width: 0;
}
.side__acct .whoami { width: 100%; padding: 6px 8px; border-radius: 10px; }
.side__acct .whoami img { width: 34px; height: 34px; }
.side__acct .whoami__name { color: var(--text); }
/* The menu opens UPWARD from the rail's bottom row, and the rail's overflow
   is visible so it is not clipped at the rail's edge. */
.side__acct .acctmenu__panel { top: auto; bottom: calc(100% + 8px); left: 0; right: auto; }
.side__acct .acctmenu__panel:not([hidden]) { transform-origin: bottom left; animation-name: acctmenu-up; }
@keyframes acctmenu-up {
  from { opacity: 0; transform: translateY(6px) scale(.985); }
  to   { opacity: 1; transform: none; }
}
/* The two status pills, moved in from the top bar. */
.acctmenu__status { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 12px 2px; }

/* ── search, at the top of the rail
   There is no top bar (2026-09-02 evening, docs/console-reanalysis-2026-09-02.md):
   it held the search and two status chips, and painted a second dark band
   above the column to do it. The search sits under the logo instead. */

.search {
  font: inherit; font-size: 13.5px;
  display: inline-flex; align-items: center; gap: 10px;
  min-height: var(--tap-sm);
  padding: 6px 10px 6px 12px;
  width: min(100%, 22rem);
  color: var(--muted);
  background: rgba(255, 255, 255, .04);
  border: 1px solid var(--control-border);
  border-radius: var(--r-pill);
  cursor: pointer;
  text-align: left;
  transition: border-color .18s var(--ease), background .18s var(--ease), color .18s var(--ease);
}
.search:hover { background: rgba(255, 255, 255, .07); color: var(--text); border-color: var(--grey-2); }
.search svg { flex: none; }
.search__label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.search__kbd,
.palette kbd {
  font-family: var(--mono); font-size: 11px; line-height: 1;
  padding: 4px 6px; border-radius: 6px;
  color: var(--faint);
  border: 1px solid var(--glass-edge);
  background: rgba(255, 255, 255, .04);
  white-space: nowrap;
}
/* The pill in the rail. After .search, so these win: the earlier rule sat
   before .search at the same specificity and lost both its width and its
   margin to it. min-width: 0 is the fix for the rail overflow of 2026-09-04:
   a grid item's automatic minimum is its min-content, and at 226px that was
   wider than the rail. Now the label truncates (it already declares ellipsis)
   and the rail keeps its width whatever the pill says. The gap is a touch
   tighter than the bar's: the rail is a narrower home. */
.side .side__search { width: 100%; min-width: 0; margin: 2px 0 4px; gap: 8px; padding: 6px 8px 6px 10px; }

/* ── view switcher: a segmented tablist under the page title */

.views {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 4px;
  background: rgba(255, 255, 255, .04);
  border: 1px solid var(--glass-edge);
  border-radius: var(--r-pill);
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
  justify-self: start;
}
.views::-webkit-scrollbar { display: none; }
.views--single { display: none; }

.tab {
  font: inherit; font-size: 13.5px; font-weight: 600;
  background: transparent; border: none;
  color: var(--muted);
  min-height: var(--tap-sm);
  padding: 6px 14px;
  border-radius: var(--r-pill);
  cursor: pointer;
  display: inline-flex; align-items: center; gap: 8px;
  white-space: nowrap;
  position: relative;
  transition: color .18s var(--ease), background .18s var(--ease);
}
.tab:hover { background: rgba(255, 255, 255, .06); color: var(--text); }

/* Selected state is carried by fill + weight, and the badge inverts. */
.tab--on {
  background: linear-gradient(180deg, rgba(255, 255, 255, .18), rgba(255, 255, 255, .08));
  color: var(--text);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .10), 0 1px 2px rgba(0, 0, 0, .3);
}

.tab__badge {
  background: var(--green); color: var(--green-ink);
  font-size: 11px; font-weight: 700; font-family: var(--mono);
  min-width: 20px; padding: 1px 6px;
  border-radius: var(--r-pill); text-align: center;
}

/* ── the palette */

.palette {
  position: fixed; z-index: 40;
  top: min(14vh, 120px); left: 50%;
  width: min(92vw, 34rem);
  display: grid; gap: 0;
  background: linear-gradient(160deg, var(--surface-2), var(--surface));
  border: 1px solid var(--glass-edge);
  border-radius: 14px;
  box-shadow: inset 0 1px 0 var(--glass-spec), var(--lift-glass);
  overflow: hidden;
  /* It arrives with its backdrop rather than after it (2026-09-07). The
     backdrop has faded over .24s since it was written and the dialog had no
     animation at all, so the box was painted at full opacity on the first
     frame over a ground still arriving, and the pair read as a flicker. Every
     other overlay here animates in: the drawer slides, the account menu
     scales. Slightly shorter than the backdrop so the box is settled by the
     time the ground finishes. The translate has to be repeated in the
     keyframe: an animated transform replaces the declared one outright. */
  transform: translateX(-50%);
  animation: palette-in .2s var(--ease);
  transform-origin: top center;
}
@keyframes palette-in {
  from { opacity: 0; transform: translateX(-50%) translateY(-8px) scale(.985); }
  to   { opacity: 1; transform: translateX(-50%); }
}
.palette__input {
  font: inherit; font-size: 16px;
  width: 100%; padding: 14px 16px;
  background: transparent; border: 0; border-bottom: 1px solid var(--glass-edge);
  color: var(--text); outline: none;
}
.palette__input::placeholder { color: var(--faint); }
/* The list is measured in ROWS, not in rems (2026-09-07).
   `max-height: min(52vh, 24rem)` had no relation to the row height, so at a
   1000px viewport it left 372px of usable space against a 40.4px row and
   ended on an 8.5px SLIVER of the ninth — a strip of half-cut text sitting
   against the hint bar, which is what made the palette read as broken rather
   than as scrollable. Eight whole rows and a deliberate half is the same
   "there is more below" affordance done on purpose: the half-row is now a
   signal instead of an accident. The row height is pinned here rather than
   left to padding and line-height, or the arithmetic drifts the next time
   the type scale moves. 12px is this list's own top and bottom padding.

   overscroll-behavior: reaching either end of the results used to hand the
   wheel to the console behind the dialog, which is the page you just covered
   with a backdrop to stop reading. */
.palette__list {
  --pal-row: 40px;
  list-style: none; margin: 0; padding: 6px;
  max-height: min(52vh, calc(var(--pal-row) * 8.5 + 12px));
  overflow-y: auto;
  overscroll-behavior: contain;
  /* No scroll-behavior: smooth here. The cursor is driven by held arrow keys,
     and a smooth scroll queues behind them and arrives late. */
}
.palette__item {
  display: flex; align-items: center; gap: 10px;
  /* A pinned height, so the list's row arithmetic above stays true. It was
     40.4px by accident of padding plus line-height, and 0.4px per row is
     enough to turn a clean edge into a sliver over nine of them. */
  width: 100%; height: var(--pal-row); padding: 0 10px;
  font: inherit; font-size: 14px; text-align: left;
  color: var(--text); background: transparent; border: 0; border-radius: 8px;
  cursor: pointer;
  /* Eased like every other row in this console (.tab, .rail__item, .search
     all run .18s); these were the only ones that snapped. */
  transition: background .16s var(--ease), color .16s var(--ease);
}
.palette__item svg { width: 16px; height: 16px; flex: none; opacity: .8; }
/* Hover and the keyboard cursor were ONE declaration, so with the pointer
   resting anywhere over the list two rows looked equally chosen and nothing
   said which one Enter would run. The keyboard cursor is the stronger of the
   two now, and carries a rule down its left edge that hover does not. */
.palette__item:hover { background: rgba(255, 255, 255, .06); }
.palette__item[aria-selected="true"] {
  background: rgba(255, 255, 255, .12);
  box-shadow: inset 2px 0 0 var(--green);
}
.palette__item[aria-selected="true"] .palette__kind { color: var(--muted); }
.palette__kind { margin-left: auto; font-family: var(--mono); font-size: 11px; color: var(--faint); }
/* The soft edge above the rule. With a hard border alone, the half-row the
   list deliberately leaves showing was sliced clean through its text and read
   as damage rather than as "there is more below". The shadow makes the list
   visibly pass UNDER the hint bar, which is the same thing said as a
   material. Nothing is hidden by it, so it stays correct when the list is
   scrolled to its end - which a fade mask would not. */
.palette__hint {
  padding: 8px 14px 10px; margin: 0;
  border-top: 1px solid var(--glass-edge);
  box-shadow: 0 -10px 14px -10px rgba(4, 6, 14, .55);
  position: relative; z-index: 1;
  display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
}
.palette__hint-sep { margin-inline: 4px; }
.palette__empty { padding: 18px 12px; color: var(--muted); font-size: 14px; }

/* ── who you are: the account block (bottom-left of the rail since 2026-09-02 evening) */

.whoami {
  display: flex; align-items: center; gap: 9px;
  padding: 4px 6px; min-width: 0;
  border-radius: var(--r-pill);
}
.whoami img {
  width: 32px; height: 32px; border-radius: 50%;
  object-fit: cover; background: var(--surface-3); flex: none;
  border: 1px solid var(--control-border);
}
/**
 * Every level of this needs min-width:0 or the name cannot ellipsise.
 * A grid/flex item defaults to min-width:auto — it refuses to shrink below
 * its content — so .whoami__body was being squeezed to 9px by the siblings
 * beside it while the name overflowed straight across them. Measured, not
 * guessed: the flank columns here are 257px, not the ~500px they look.
 */
.whoami__body { min-width: 0; display: grid; }
.whoami__name,
.whoami__meta { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.whoami__name { font-size: 13.5px; font-weight: 600; }
.whoami__meta { font-size: 12px; color: var(--faint); display: flex; align-items: center; gap: 5px; }
.whoami__meta span:last-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--faint); flex: none; }
.dot--good { background: var(--green); box-shadow: 0 0 0 3px rgba(255,255,255,.18); animation: pulse 2.6s var(--ease) infinite; }
.dot--warn { background: var(--warn); box-shadow: 0 0 0 3px var(--warn-soft); }
.dot--bad  { background: var(--bad);  box-shadow: 0 0 0 3px var(--bad-soft); }

/* A live session is the one thing worth a heartbeat — it is a state, not decoration. */
@keyframes pulse {
  0%, 100% { box-shadow: 0 0 0 3px rgba(255,255,255,.18); }
  50%      { box-shadow: 0 0 0 5px rgba(255,255,255,.06); }
}

/* ── content */

/* The page breathes between every box: the gap is the rhythm, clamped so
   desktop opens right up while a phone stays usefully dense.

   Both numbers came down on 2026-09-08. They were 28-36 over 64-88 of padding
   and a 26-38 gap, which is 220px of fixed chrome on a page that now has a
   fixed budget to spend. The bottom padding in particular was sized for a
   document you scroll to the end of; there is no end to scroll to any more.
   Measured saving: 116px at 1440x900, which is a sixth of the budget.

   FLEX, not grid, and the reason is structural rather than stylistic. This
   column holds a head, a tab strip and a body, and the body is the only one
   that should absorb the leftover height. A grid would need a
   `grid-template-rows` naming exactly how many children are visible, and that
   number changes: a gate screen shows instead of `#view-signedin`, and the
   tab strip is hidden on a single-view page. Flex lets each child say for
   itself whether it grows, so nothing has to be counted. */
/*
 * 2026-09-08: a grid that starts at the top, rather than a flex column
 * stretched to a fixed height.
 *
 * `height: 100%` was the third leg of the one-screen model and it has gone with
 * the other two. Everything in the column now keeps its own height and the
 * column is as tall as what is in it.
 */
.content {
  padding-block: clamp(18px, 1.8vw, 24px) clamp(16px, 1.6vw, 22px);
  display: grid;
  align-content: start;
  gap: clamp(18px, 1.8vw, 26px);
  min-height: 0;
}
/*
 * Nothing stretches any more, so the flex overrides that made the body take
 * "what is left" are gone too: there is no leftover to take. The `min-width: 0`
 * stays, because a grid item still refuses to shrink below its content on the
 * horizontal axis and that is what keeps wide tables inside the column.
 */
.content > * { min-width: 0; }

/**
 * A single unbreakable string sets a floor under everything above it, and this
 * console is full of them: email addresses, box names, LinkedIn identifiers,
 * headline text with no spaces near the break. On the Storage tab one of them
 * pushed the page 59px sideways at 320px; on Email, 26px.
 *
 * `anywhere` rather than `break-word` deliberately. Only `anywhere` also
 * reduces the element's MIN-CONTENT width, which is the measurement the
 * layout above is sized from — `break-word` wraps the visible text and leaves
 * the floor exactly where it was, so it looks like a fix and changes nothing.
 * Measured: this took Storage from 59px to 2px and Email from 26px to zero.
 */
.content :where(p, h1, h2, h3, h4, span, li, td, th, dd, dt, label, small, b, strong, em, code) {
  overflow-wrap: anywhere;
}

/**
 * THE MEASURE, now that the column no longer provides one (2026-09-08).
 *
 * With `--page` fluid, a paragraph in a card would run the full width of the
 * window, and a 2000px line is unreadable however good the type is. So the cap
 * moves from the LAYOUT to the TEXT: the card still fills its column, and the
 * sentences inside it stop at a readable length.
 *
 * Deliberately narrow in scope. It applies to explanatory paragraphs, which are
 * the only thing here that is really prose. It does NOT apply to lists, tables,
 * rows, headings or anything inside a pane, because those are data and want
 * every pixel of the width this change just won back.
 */
.card > p,
.card > .muted,
.card > .note,
.card > .lead {
  max-width: var(--measure);
}

/**
 * The same min-width:auto trap as on the shell above, one level further in.
 * A card is a grid, so its children are grid ITEMS, and the track sizes to the
 * widest child's min-content — so a card 288px wide was laying its rows out at
 * 283px INSIDE a 244px content box and pushing the page 2px sideways.
 *
 * Containers only, deliberately. Adding buttons and inputs to this list would
 * let flexbox squash a control until its own label spilled out of it, which
 * trades a visible defect for a subtler one. Verified: with this rule no button
 * on any of the twelve tabs clips its label at 320px.
 */
.content :where(div, section, article, ul, ol, li, form, fieldset, figure, table) {
  min-width: 0;
}

/* Keep the last card clear of the home indicator. `max()` so the existing
   breathing room still wins on every device that has no indicator, where
   `env()` is 0. Must sit AFTER the `padding-block` shorthand above, or the
   shorthand resets it. */
.content { padding-bottom: max(clamp(64px, 6vw, 88px), env(safe-area-inset-bottom)); }

/**
 * THE FIT CONTRACT (2026-09-09).
 *
 * The shell is exactly the window and the DOCUMENT never scrolls. The rail and
 * the page head stay where they are; a page longer than the screen scrolls
 * inside its own panel instead of taking the whole frame with it.
 *
 * Read the 2026-09-08 note above this file's two-column block before touching
 * it, because this is the same idea that was removed that day and it is allowed
 * back only because of what changed either side of it. That version made every
 * panel a set of equal rows and gave any card that overflowed its cell its own
 * scrollbar, so three Settings cards concealed 404px between them. NOTHING here
 * caps a card. The panel scrolls as one piece, whole cards and all, which is a
 * thing a person can see and use; the card-level clipping that made the old
 * rule dishonest is not reintroduced.
 *
 * Most pages never reach it. After the work above, eleven of thirteen fit at
 * 1440x900 with room to spare, so this is the floor under the two that do not
 * rather than the mechanism the console runs on.
 *
 * ABOVE 1000px ONLY, tied to the same breakpoint as the two-column grid. Below
 * it there is one column, everything is taller, and a fixed frame would mean
 * scrolling a small region inside a small screen. A narrow window scrolls the
 * document, exactly as it does today.
 */
@media (min-width: 1000px) {
  html, body { height: 100%; }
  /* The clip is what makes it a frame. `.shell` keeps its own overflow visible
     so the rail's account menu can still open past its edge, which is the
     reason the old clip was placed on the body rather than the shell. */
  body { overflow: hidden; }
  .shell { height: 100dvh; min-height: 0; }
  .maincol { grid-template-rows: minmax(0, 1fr); min-height: 0; }
  .content {
    min-height: 0;
    /* The head, then everything else. The head is row 1 and stays put; the
       panel is row 2 and is the only thing that moves. */
    grid-template-rows: auto minmax(0, 1fr);
    align-content: stretch;
    /* 64 to 88px of bottom padding was breathing room under the LAST card of a
       scrolling document. In a fixed frame it is permanent dead space below a
       panel that already ends where the window does. */
    padding-bottom: max(20px, env(safe-area-inset-bottom));
  }
  /*
   * `#view-signedin` sits between `.content` and the panel and carries
   * `align-content: start` of its own, so without this the frame's height
   * stopped here and the panel went back to being as tall as its cards. One
   * panel is visible at a time, so one row, and it takes the lot.
   */
  #view-signedin {
    min-height: 0;
    grid-template-rows: minmax(0, 1fr);
    align-content: stretch;
  }

  /* The panel is the one scrolling region on a page that needs one. */
  #view-signedin > .panel {
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    /*
     * NO `scrollbar-gutter: stable` here, and it was tried. It reserves 15px on
     * every panel including the eleven that do not scroll, which put a 15px
     * inset down the right of every page and made the content column
     * asymmetric against its own left gutter. Alignment is the complaint this
     * whole change answers, so a permanent misalignment to avoid a one-off 15px
     * shift is the wrong way round. Panes keep their gutter; they scroll always.
     */
  }
  /*
   * Except on a work page, where the two panes are meant to reach the bottom of
   * the screen together and each list scrolls inside its own card. A scrollbar
   * on the panel there would be a second one outside the two that already
   * belong to the lists, moving the whole thing including both headers.
   */
  #view-signedin > .panel[data-shape="work"] { overflow: hidden; }
}

/**
 * Page heading. The old layout put the current view's name in the top bar,
 * which meant the h1 was chrome — it scrolled away, it competed with the
 * status pills, and it left the content area with no title at all.
 */
/**
 * ONE HEAD HEIGHT, ON EVERY PAGE (2026-09-09).
 *
 * The switcher used to sit on its own row UNDER the title, and
 * `.views--single { display: none }` means a one-view page has no switcher at
 * all. So the head was 60px on Inbox and 114px on Leads, and the body jumped
 * 54px as you moved between two pages that look like siblings. Measured, both
 * at 1440x900.
 *
 * The switcher moves onto the title row, pushed right. Two things follow, and
 * both are the point: every head is the same height whether a page has one view
 * or two, so content starts at the same Y everywhere; and the 44px the
 * switcher used to cost in vertical space is given back to the content, which
 * is 44px of the fit problem solved for free on six of the thirteen pages.
 *
 * `align-items: end` rather than centre: the switcher lines up with the
 * BASELINE of the title block, not with the middle of a two-line block, so it
 * does not drift when a page has a subtitle and its neighbour does not.
 */
.pagehead {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: 10px 16px;
  margin: 0;
  /* The floor is what actually equalises the two shapes. A title with no
     subtitle and no switcher would otherwise be shorter than one with both,
     and the whole point is that a page does not move when you leave it. */
  min-height: 62px;
}
.pagehead .section-head { gap: 4px; grid-column: 1; }
.pagehead .views { grid-column: 2; grid-row: 1; margin-bottom: 2px; }
/* Below the two-column breakpoint the switcher drops under the title, where
   there is room for it. A 320px phone cannot put a 220px pill beside an h1. */
@media (max-width: 700px) {
  .pagehead { grid-template-columns: minmax(0, 1fr); }
  .pagehead .views { grid-column: 1; grid-row: 2; justify-self: start; }
}
.pagehead h1 { font-size: clamp(26px, 2.4vw, 30px); line-height: 1.12; letter-spacing: -.018em; text-wrap: balance; }
.pagehead__sub { color: var(--muted); font-size: 15px; line-height: 1.5; max-width: 58ch; text-wrap: pretty; margin-top: 4px; }

/**
 * A gate screen: unlock, sign in, challenge, reconnect. One decision on
 * screen, so it is narrow and centred rather than stretched across 72rem.
 * This is the single biggest readability win in the restructure.
 */
/**
 * The class sits on the card ITSELF, not on a wrapper. A wrapper div stays
 * in the grid when its only child is [hidden] — five of them contributed
 * 100px of dead space above the first real content, which looked like a
 * broken layout rather than an empty container. On the card, [hidden]
 * collapses the element outright.
 */
.card.gate { max-width: var(--page-narrow); margin-inline: auto; width: 100%; padding: 28px; }
/* Both selectors require .card deliberately: landing.html has its own
   unrelated `<ol class="gate">` for the numbered start steps, and a bare
   `.gate h2` here reaches straight into it across pages. */
.card.gate h2 { font-size: 22px; line-height: 28px; }

/* =========================================================================
   Components
   ========================================================================= */

.card {
  position: relative;
  background: linear-gradient(160deg, var(--glass-strong), var(--glass));
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--glass-edge);
  border-radius: 14px;
  box-shadow: inset 0 1px 0 var(--glass-spec), var(--lift-glass);
  /* Widened from Base44's 24/16 by request (2026-08-17): every box gets
     more air inside as well as between. Clamped so phones stay dense. */
  padding: clamp(22px, 2.6vw, 30px);
  display: grid; gap: clamp(16px, 2vw, 20px);
}
.card--warn { border-color: var(--warn); background: var(--warn-soft); }
.card--warn h2 { color: var(--warn); }

@supports not (backdrop-filter: blur(1px)) { .card { background: var(--surface); } }

.section-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.counter { font-weight: 400; font-size: 12px; color: var(--faint); font-family: var(--mono); }

.stack { display: grid; gap: 16px; }
.row { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; }
.row--tight { margin-top: 4px; }
.rule { border: none; border-top: 1px solid var(--glass-edge); margin: 0; }

/* ── inputs */

.field { display: grid; gap: 6px; }
/* A row of fields: each field takes a fair share and wraps below a floor,
   and the row's non-field controls sit on the fields' baseline. Without this
   the booking form's fields were min-content wide with no rhythm between
   the rows (2026-09-02 night). */
.row--fields { align-items: flex-end; row-gap: 14px; }
.row--fields > .field { flex: 1 1 12rem; }
#cal-body { display: grid; gap: 14px; }
#cal-body h3 { margin-top: 4px; }
#morning-card .row--fields > .field { flex: 0 1 11rem; }
.field > span { font-size: 13.5px; font-weight: 600; color: var(--text); }

/**
 * The hint under a field, and it has to out-specify the label rule above it.
 *
 * `.field > span` is a child combinator (0-2-0) and `.field__hint` was a bare
 * class (0-1-0), so for every hint written as a DIRECT child of a `.field` —
 * which is how all of them are written, because that is where a hint goes —
 * the label rule won and the hint rendered at 13.5px, weight 600, full-strength
 * ink. Identical to the label above it.
 *
 * So this class has never worked in its own intended position. It looked
 * correct in the stylesheet and did nothing on the page, which is why the
 * Discover card ended up with help text set heavier than its own headings.
 * Measured 2026-09-07 before the fix: 600 weight, rgb(10,16,36).
 *
 * `.field > .field__hint` ties `.field > span` on specificity and wins on
 * order. The weight is stated here for the first time: a hint is not bold, and
 * relying on inheritance meant it took the label's.
 */
.field__hint,
.field > .field__hint { font-size: 12.5px; font-weight: 400; color: var(--muted); }

/**
 * 16px, and it is not a taste decision. Safari on iOS zooms the entire page
 * whenever a form control under 16px takes focus, and it does NOT zoom back out
 * when the field is blurred — so one tap on a search box leaves the reader
 * stranded at 1.3x with the layout hanging off both edges, for the rest of the
 * session. It was 15px, which is under the threshold by one pixel and cost the
 * whole behaviour.
 *
 * `select` is in the list now too. It never was, so every dropdown in the
 * console fell back to the browser default of 13.33px in a 19px-tall box: the
 * smallest and least tappable controls in the product, on the tab that sends
 * email.
 */
input[type=email], input[type=password], input[type=text], input[type=search],
input[type=number], input[type=datetime-local], input[type=file], textarea, select {
  width: 100%; font: inherit; font-size: 16px; color: var(--text);
  background: rgba(0, 0, 0, .32);
  border: 1px solid var(--control-border);        /* 3.06:1 — WCAG 1.4.11 */
  border-radius: var(--r-sm);
  min-height: var(--tap);
  padding: 9px 12px;
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease), background .18s var(--ease);
}
input::placeholder, textarea::placeholder { color: var(--faint); opacity: 1; }

/* The ring is an outline so forced-colors keeps it; the green border and
   fill are the redundant, decorative half of the same signal. */
input:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 1px;
  border-color: var(--green);
  background: rgba(0, 0, 0, .45);
}
textarea { resize: vertical; min-height: 140px; line-height: 1.55; padding: 12px; }

/* The auto-connect controls stack: target, note mode, then the run row. */
.autoconnect { display: grid; gap: 16px; margin-top: 8px; }
.autoconnect__controls { align-items: center; }
/* The daily-limit box is a two-digit number; full width would look like a
   search field and read as something it is not. It lives inside the
   Options fold now (2026-08-15), where the .field grid would otherwise
   stretch it across the whole card. */
.autoconnect #ac-limit { width: 5.5rem; }
.autoconnect textarea { min-height: 64px; }

/* Live log of each invitation attempt, failures included. */
.ac-log { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 6px; max-height: 320px; overflow-y: auto; }
.ac-log li { display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: baseline; font-size: 13.5px; padding: 6px 0; border-top: 1px solid var(--hairline, rgba(255,255,255,.08)); }
.ac-log .ac-log__name { font-weight: 600; color: var(--text); }
.ac-log .ac-log__state { margin-left: auto; font-size: 12px; font-family: var(--mono); }
.ac-log .ok { color: var(--green); }
/* A failure is a warning, in the system's warning colour — it was --faint,
   which dressed the entries that matter most as timestamps. `quiet` is for
   skips that sent nothing and mean nothing (already pending). */
.ac-log .warn { color: var(--warn); }
.ac-log .quiet { color: var(--faint); }
.ac-log .ac-log__note { flex-basis: 100%; color: var(--muted); font-size: 12.5px; }

/*
 * A log entry is a PERSON, not a line of text.
 *
 * The face is 28px — big enough to recognise, small enough that sixty of
 * them still read as a log. `align-items: center` rather than the baseline
 * the text-only version used, because a baseline-aligned avatar sits wrong
 * against two lines of text.
 */
.ac-log li { align-items: center; }
.ac-log .ac-log__face {
  width: 28px; height: 28px; flex: none;
  border-radius: 50%; object-fit: cover;
  background: var(--surface-2);
}
.ac-log .ac-log__body { min-width: 0; flex: 1 1 0; }
.ac-log .ac-log__name { display: block; }
.ac-log a.ac-log__name { color: inherit; text-decoration: none; }
.ac-log a.ac-log__name:hover { text-decoration: underline; }
.ac-log .ac-log__headline {
  color: var(--muted); font-size: 12.5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ac-log .ac-log__state { flex: none; }

/*
 * The person the run is on right now.
 *
 * Deliberately the loudest thing in the card while a run is live: this is
 * the last moment before an invitation goes to someone the user has not
 * looked at, and it is the surface that makes a wrong target obvious after
 * ONE send rather than sixty.
 */
.ac-now {
  display: flex; align-items: center; gap: 12px;
  margin-top: 12px; padding: 12px 14px;
  border: 1px solid var(--hairline, rgba(255,255,255,.08));
  border-radius: 12px;
  background: var(--surface-2);
}
.ac-now img {
  width: 44px; height: 44px; flex: none;
  border-radius: 50%; object-fit: cover;
  background: var(--surface);
}
.ac-now__body { min-width: 0; flex: 1 1 0; }
.ac-now__name {
  font-weight: 600; font-size: 14.5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ac-now__name a { color: var(--text); text-decoration: none; }
.ac-now__name a:hover { text-decoration: underline; }
.ac-now__headline {
  color: var(--muted); font-size: 13px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ac-now__state {
  flex: none; font-family: var(--mono); font-size: 12px; color: var(--green);
}

/* The same face on Home, where the live strip already reports the run. */
.livebar__face {
  width: 24px; height: 24px; flex: none;
  border-radius: 50%; object-fit: cover;
  background: var(--surface-2);
}

@media (max-width: 560px) {
  /* Two lines of person plus a status word will not fit one row on a
     phone; the state drops under the name rather than crushing it. */
  .ac-now { flex-wrap: wrap; row-gap: 8px; }
  .ac-now__state { margin-left: 56px; }
}

/*
 * The step number on the Campaigns page's four cards.
 *
 * Ordered work should look ordered. Those cards were previously
 * indistinguishable from any other stack, so nothing on screen said that
 * connecting an account comes before choosing people, or that creating a
 * draft is not the same act as sending it.
 */
.step-no {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; flex: none;
  margin-right: 10px;
  border-radius: 50%;
  background: var(--surface-2);
  border: 1px solid var(--hairline, rgba(255,255,255,.08));
  font-family: var(--mono); font-size: 12px; font-weight: 600;
  color: var(--muted);
}

/**
 * The campaign form.
 *
 * It used to be `display: contents`, which lifted its four step cards into the
 * `.panel` grid so they picked up the panel's gap. (Before THAT it was neither:
 * the rule named `#form-campaign` and the form is `#form-msgcampaign`, so it
 * matched nothing and the four steps were welded into one slab with 0px
 * between them at every width from 380 to 1600. Fixed 2026-09-07.)
 *
 * Since 2026-09-08 the form is ONE box holding one step at a time, so it is a
 * real card again rather than a pass-through. The four steps together were
 * 1,398px and the panel could not hold them beside the three cards below on a
 * 1366x768 screen; showing one costs the tallest, 712px. The gap the
 * `display: contents` rule existed to provide is now internal spacing, which
 * is what it should always have been for steps of one form.
 *
 * The active step carries `.pane`, so the card is bounded and the step scrolls
 * inside it by the same machinery every list uses. Nothing here is special.
 */
#form-msgcampaign {
  gap: 12px;
}

.mc-nav {
  display: flex; flex-wrap: wrap; gap: 10px;
  padding-bottom: 2px;
}
.mc-nav__step {
  font: inherit; font-size: 13px; font-weight: 600;
  display: inline-flex; align-items: center; gap: 8px;
  min-height: var(--tap-sm);
  padding: 6px 12px;
  border-radius: var(--r-pill);
  background: transparent;
  border: 1px solid transparent;
  color: var(--muted);
  cursor: pointer;
}
.mc-nav__step:hover { color: var(--text); background: rgba(255, 255, 255, .06); }
.mc-nav__step--on {
  background: var(--surface-2);
  border-color: var(--glass-edge);
  color: var(--text);
}
.mc-nav__no {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; flex: none;
  border-radius: 50%;
  background: var(--surface-2);
  border: 1px solid var(--hairline, rgba(255,255,255,.08));
  font-family: var(--mono); font-size: 11px;
}
.mc-nav__step--on .mc-nav__no { background: var(--green); color: var(--bg); border-color: transparent; }

/* The step itself no longer draws a box: the form around it is the box. Its
   own head keeps the number it always had, which is what makes the nav above
   read as a position rather than a second set of buttons. */
.mc-step { display: grid; gap: 14px; align-content: start; }
.mc-step:focus-visible { outline: none; }

.mc-foot { display: flex; gap: 10px; justify-content: flex-end; }

/* Step 4's preview is a <pre>, and nothing here had ever given `pre` a
   white-space, so it kept the UA's `pre`: one long line, no wrapping,
   overflow visible. A single-sentence message ran 101px past the card at
   1600 and 361px at 900 — the review text quite literally outside its box.
   Same pair of properties .suggest__text uses, for the same reason. */
#mc-preview { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }

/* The .check label (a tick box beside its words) went with the morning
   batch's checkbox on 2026-09-04; the console's only switch of that kind is
   now a chip pair, and the Approval page carries no checkbox by rule. */
.search { margin: 0; }
.searchbar { display: flex; gap: 10px; flex-wrap: wrap; }
.searchbar input { flex: 1 1 18rem; }

/* ── buttons: Base44's pill, "bold but not aggressive" */

.btn {
  font: inherit; font-size: 14px; font-weight: 600;
  border-radius: var(--r-pill);
  min-height: var(--tap);
  padding: 8px 18px;
  border: 1px solid var(--control-border);
  background: rgba(255, 255, 255, .06);
  color: var(--text);
  cursor: pointer; text-decoration: none;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  white-space: nowrap;
  transition: transform .16s var(--ease), background .16s var(--ease),
              border-color .16s var(--ease), color .16s var(--ease), filter .16s var(--ease);
}
/* Base44: buttons "gently grow and brighten on hover". Gently. */
.btn:hover:not(:disabled) { transform: translateY(-1px); filter: brightness(1.14); border-color: var(--green); }
.btn:active:not(:disabled) { transform: translateY(0) scale(.995); }
.btn:disabled { opacity: .45; cursor: not-allowed; }

.btn--primary { background: var(--green); border-color: var(--green); color: var(--green-ink); }
.btn--primary:hover:not(:disabled) { background: var(--green-bright); border-color: var(--green-bright); }

.btn--ghost { background: rgba(255,255,255,.04); border-color: var(--control-border); color: var(--text); }
.btn--ghost:hover:not(:disabled) { background: rgba(255,255,255,.09); }

.btn--sm { min-height: var(--tap-sm); padding: 5px 14px; font-size: 13px; }

.icon-btn {
  border: 1px solid transparent; background: transparent; color: var(--muted);
  font-size: 24px; line-height: 1; cursor: pointer;
  width: var(--tap); height: var(--tap); border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  transition: color .16s var(--ease), background .16s var(--ease);
}
.icon-btn:hover { color: var(--text); background: rgba(255,255,255,.08); }

.chip {
  font: inherit; font-size: 13px; font-weight: 600;
  min-height: var(--tap-sm);
  padding: 4px 14px; border-radius: var(--r-pill);
  border: 1px solid var(--control-border); background: rgba(255,255,255,.04);
  color: var(--text); cursor: pointer;
  display: inline-flex; align-items: center;
  transition: background .16s var(--ease), border-color .16s var(--ease), color .16s var(--ease);
}
.chip--on { background: rgba(255,255,255,.14); border-color: var(--green); color: var(--green-bright); }

/* ── pills */

.pill {
  font-size: 12px; font-weight: 600; font-family: var(--mono);
  padding: 4px 10px; border-radius: var(--r-pill);
  border: 1px solid transparent; white-space: nowrap;
  display: inline-flex; align-items: center; gap: 6px;
}
.pill--idle { background: rgba(255,255,255,.06); color: var(--muted); border-color: var(--control-border); }
.pill--good { background: rgba(255,255,255,.12); color: var(--green); border-color: rgba(255,255,255,.5); }
.pill--warn { background: var(--warn-soft); color: var(--warn); border-color: var(--warn); }
.pill--bad  { background: var(--bad-soft);  color: var(--bad);  border-color: var(--bad); }

/* ── notes */

.note {
  margin: 0; font-size: 13.5px;
  padding: 12px 14px; border-radius: var(--r-sm);
  background: rgba(255,255,255,.05);
  border: 1px solid var(--control-border);
  border-left-width: 3px;
  white-space: pre-wrap; word-break: break-word;
  max-width: var(--page-prose);
  animation: slip .28s var(--ease);
}
.note--bad  { border-left-color: var(--bad);  color: var(--bad); }
.note--good { border-left-color: var(--green); color: var(--green); }
.note--warn { border-left-color: var(--warn); color: var(--warn); }

/* A note appearing is a state change, so it arrives from somewhere. */
@keyframes slip { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }

/* ── identity */

.identity__badge { display: inline-flex; align-items: center; gap: 5px; color: var(--green); }

/**
 * Stats. Was a flex row of baseline-aligned pairs that collapsed into an
 * unreadable run of numbers on a narrow window; now a real grid of tiles
 * that reflows, with the number given room to be the thing you see.
 */
/**
 * `minmax(min(11rem, 100%), 1fr)`, not `minmax(11rem, 1fr)`.
 *
 * A bare floor of 11rem is 176px that the track will not go below, so on a
 * 320px phone the grid laid out TWO 177px columns and pushed the page 39px
 * sideways. Wrapping the floor in `min(FLOOR, 100%)` lets the track collapse
 * when the column is narrower than the floor, which is the only case anyone
 * wanted the floor to stop. Measured: this alone took Home from 39px of
 * overflow at 320px to zero.
 */
.stats {
  display: grid; gap: clamp(12px, 1.6vw, 18px);
  grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
}
/**
 * The design element the redesign turns on: an icon-led tile. A tinted chip
 * on the left gives every statistic a shape to find it by, so a row of
 * numbers becomes scannable instead of a wall of digits.
 */
.stat {
  display: flex; align-items: center; gap: 13px;
  padding: 14px 16px;
  position: relative; overflow: hidden;
  background: linear-gradient(150deg, rgba(255, 255, 255, .05), rgba(255, 255, 255, .02));
  border: 1px solid var(--glass-edge);
  border-radius: 12px;
  transition: border-color .18s var(--ease), transform .18s var(--ease), box-shadow .18s var(--ease);
}
.stat:hover {
  border-color: rgba(255, 255, 255, .4);
  transform: translateY(-2px);
  box-shadow: 0 8px 22px -14px rgba(190, 205, 255, .5);
}
.stat__icon {
  flex: none; width: 38px; height: 38px; border-radius: 10px;
  display: grid; place-items: center;
  color: var(--green);
  background: rgba(255, 255, 255, .12);
  border: 1px solid rgba(255, 255, 255, .22);
}
.stat__icon svg { width: 20px; height: 20px; }
.stat__body { display: grid; gap: 1px; min-width: 0; }
.stat__n { font-family: var(--mono); font-size: 23px; font-weight: 700; letter-spacing: -.02em; color: var(--white); line-height: 1.12; }
/* The label keeps the shared eyebrow typography; only its colour and a
   tighter tracking (the tile is narrower than before) are set here. */
.stat__l { color: var(--faint); letter-spacing: .1em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/**
 * The headline tile of a row: a green accent rail, a solid-green chip and a
 * bright number, so the one number that matters most is found first.
 */
.stat--hero { border-color: rgba(255, 255, 255, .34); background: linear-gradient(150deg, rgba(255, 255, 255, .12), rgba(255, 255, 255, .02)); }
.stat--hero::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--green); }
.stat--hero .stat__icon { background: var(--green); border-color: var(--green); color: var(--green-ink); }
.stat--hero .stat__n { color: var(--green-bright); }


/* Section-heading icon chip: the same green square as the stat chips, smaller. */
.section-head h2, .figure__head h3 { display: flex; align-items: center; gap: 9px; }
.head-ico {
  display: inline-grid; place-items: center; flex: none;
  width: 26px; height: 26px; border-radius: 7px;
  color: var(--green);
  background: rgba(255, 255, 255, .12);
  border: 1px solid rgba(255, 255, 255, .2);
}
.head-ico svg { width: 15px; height: 15px; }

/* ── figures: the house dataviz rules in CSS ──
   Single hue (green carries the data), thin marks, hairline solid grid,
   text in text tokens never the series colour, and judgement in words. */

.figure { display: grid; gap: 12px; }

/* A line of figures: a caption under a card head, where a second row of
   tiles would be. Mono number, sans label, one row that wraps. */
.figures { display: flex; flex-wrap: wrap; gap: 8px 26px; margin: 0; font-size: 13.5px; color: var(--muted); }
.figures__item { display: inline-flex; align-items: baseline; gap: 7px; white-space: nowrap; }
.figures__n { font-family: var(--mono); font-size: 19px; font-weight: 700; letter-spacing: -.02em; color: var(--white); line-height: 1.1; }
.figure__head { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
.figure__head h3 { font-size: 14.5px; }
/* Side-by-side figures keep a wide channel between the columns, and the
   20rem floor makes each column collapse to its own row BEFORE the two can
   crowd into each other. */
.figure-row { display: grid; gap: clamp(24px, 3.2vw, 40px); grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr)); align-items: start; }

/* ── minis: the three tiny diagrams in Home's Outreach card (2026-09-09) ──
   The card's third row takes whatever height the Home row leaves, and each
   mini's drawing takes whatever height its box leaves, so the row of three
   fills the space the owner asked to have filled and shrinks on a phone to
   a 48px glance. Bars stretch (preserveAspectRatio none, no text inside);
   the ring keeps its circle. Colours come from the accent slots the measured
   charts use, so the skin re-inks them like everything else.

   THE ROW IS CAPPED. Home's card row is about 450px tall at 1600x1000, and
   three boxes that tall with a 140px drawing at the foot of each read as
   three empty columns, which is the look this exists to remove. A track of at
   most 250px (less on a short window: it shrinks to what the card has, and to
   what a phone has) gives a drawing about 180px tall, which a 14-bar run and
   a four-step funnel carry without turning into needles. */
/* Four content-sized rows (head, stages, stages note, figures) and then the
   minis, which take the rest. The count is the card's markup order in
   index.html: the minis are the fifth child. The unmessaged figure after them
   is hidden until it has somebody to show, and takes an implicit row when it
   does. */
#outreach-card { grid-template-rows: repeat(4, auto) minmax(0, 1fr); }
.minis {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px;
  grid-template-rows: minmax(0, min(250px, 32vh)); align-content: start;
  min-height: 0; margin: 0;
}
.mini {
  display: grid; grid-template-rows: minmax(0, 1fr) auto; gap: 8px;
  margin: 0; min-width: 0; min-height: 0;
  padding: 12px 10px 10px;
  border-radius: 10px;
  background: rgba(255, 255, 255, .04);
  border: 1px solid var(--border);
}
.mini__draw { display: grid; align-items: end; justify-items: center; min-width: 0; min-height: 48px; }
.mini__draw svg { display: block; width: 100%; height: 100%; max-height: 190px; }
#mini-today .mini__draw { align-items: center; }
/* The ring keeps its circle: as wide as the box allows, never wider than the
   drawing area, and its height follows from that rather than from the row. */
#mini-today .mini__draw svg { width: min(100%, 120px); height: auto; aspect-ratio: 1; }
.mini__cap {
  font-size: 11.5px; line-height: 1.3; color: var(--faint);
  text-align: center; text-wrap: balance;
}
/*
 * A TALL WINDOW STACKS THEM. At 1000px and up Home's card row leaves the
 * Outreach card 400px or more below its figures, and three 250px boxes
 * across leave a band of nothing under them, which is the complaint. Stacked,
 * each mini is a wide row of about 130px: the 14 days become a broad run of
 * bars, the funnel four wide steps, and the ring sits in its row with the
 * caption beside it, so the drawings use the width the column has rather than
 * the height it does not. The 900px line is where the row has room to stack
 * without the rows going short; below it the three-across layout above holds.
 */
@media (min-height: 900px) and (min-width: 1100px) {
  .minis { grid-template-columns: minmax(0, 1fr); grid-template-rows: repeat(3, minmax(0, 1fr)); align-content: stretch; }
  .mini__draw svg { max-height: none; }
  #mini-today { grid-template-columns: auto minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); align-items: center; }
  #mini-today .mini__draw { min-height: 0; height: 100%; }
  #mini-today .mini__draw svg { height: 100%; width: auto; max-height: 100%; }
  #mini-today .mini__cap { text-align: left; }
}
.mini__bar { fill: var(--green-dim); }
.mini__bar--today { fill: var(--green); }
.mini__bar--zero { fill: var(--green-deep); }
.mini__track { stroke: var(--green-deep); }
.mini__ring { stroke: var(--green); transition: stroke-dasharray .6s var(--ease); }
.mini__ring--full { stroke: var(--green-bright); }
.mini__step-track { fill: var(--green-deep); }
.mini__step { fill: var(--green-dim); }
.mini__step--sent { fill: var(--green); }
.mini__step--replied { fill: var(--green-bright); }

/* ── Home ──
   Home owns the account summary; its cards are entry points. The cursor,
   the hover lift and the explicit "Tab →" affordance all say so — a
   clickable card must never be a secret (hover-only disclosure is a known
   discoverability trap, so the affordance is always visible). */
.home-grid { display: grid; gap: clamp(24px, 3.2vw, 40px); grid-template-columns: repeat(auto-fit, minmax(min(21rem, 100%), 1fr)); align-items: start; }
.card[data-goto] { cursor: pointer; }
.card[data-goto]:hover { border-color: var(--border-strong); }
.card__go {
  border: none; background: none; padding: 4px 0; cursor: pointer;
  font: inherit; font-size: 13px; font-family: var(--mono);
  color: var(--faint); flex: none;
}
.card__go:hover, .card[data-goto]:hover .card__go { color: var(--text); }

/* A live auto-connect run, surfaced on Home. Never decorative: this bar
   exists so that a run sending real invitations is visible from the
   default tab, not only from Discover. */
.livebar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 12px 16px;
  border: 1px solid rgba(255, 255, 255, .34); border-radius: var(--r);
  background: linear-gradient(150deg, rgba(255, 255, 255, .12), rgba(255, 255, 255, .02));
}
.livebar__dot {
  width: 9px; height: 9px; border-radius: 50%; flex: none;
  background: var(--green); box-shadow: 0 0 8px var(--green);
  animation: livebar-pulse 1.6s ease-in-out infinite;
}
.livebar button { margin-left: auto; }
@keyframes livebar-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .35; }
}

/* ── Right now ──
   What wireagent is doing for you, across every tab at once (2026-09-07,
   docs/home-activity-plan-2026-09-07.md). One row per work stream, each a
   button that lands on the tab that owns it.

   A ROW IS A BUTTON AND LOOKS LIKE A ROW. It carries no chevron of its own
   until hover: seven arrows stacked down a card is a toolbar, and the thing
   being read here is the numbers.

   THE BAR IS DRAWN ONLY WHERE THERE IS A CEILING TO DRAW AGAINST, and only
   while the stream is running. A full bar on a finished thing reads as a
   warning; a bar on a backlog with no total reads as a target that does not
   exist. The renderer omits the element entirely in both cases rather than
   painting an empty track. */
.activity { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.act {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 6px 12px;
  width: 100%;
  padding: 11px 14px;
  text-align: left;
  font: inherit;
  color: inherit;
  border: 1px solid var(--glass-edge); border-radius: var(--r);
  background: rgba(255, 255, 255, .04);
  cursor: pointer;
  transition: border-color .16s var(--ease), background .16s var(--ease);
}
.act:hover, .act:focus-visible { border-color: var(--rule-strong, var(--glass-edge)); background: rgba(255, 255, 255, .07); }
.act__dot {
  width: 8px; height: 8px; border-radius: 50%; flex: none;
  background: var(--faint);
  grid-row: 1;
}
/* Live: the same pulse the auto-connect bar uses, so one visual word means
   "this is happening now" everywhere on the page. */
.act--live .act__dot {
  background: var(--green); box-shadow: 0 0 8px var(--green);
  animation: livebar-pulse 1.6s ease-in-out infinite;
}
/*
 * `break-word`, NOT the blanket `anywhere` from line 948 (2026-09-09).
 *
 * `overflow-wrap: anywhere` is the one value that also shrinks an element's
 * MIN-CONTENT width, so the grid was free to give this label a single character
 * and hand the rest to the nowrap figure beside it. On Home's three-track
 * layout that rendered "Invitations" as "Invitation" over "s".
 *
 * `break-word` still breaks a word that genuinely cannot fit, which is what the
 * blanket rule was protecting against, but it leaves min-content alone so the
 * column is sized for the whole word first.
 */
.act__label { min-width: 0; font-weight: 600; overflow-wrap: break-word; }
.act__figure { font-family: var(--mono); font-size: 13px; color: var(--muted); white-space: nowrap; }
.act__figure b { color: var(--text); font-weight: 700; }
/* The second line spans the full width under the label, so a long "inviting
   Someone With A Long Name" wraps rather than squeezing the figure. */
.act__detail { grid-column: 1 / -1; font-size: 12px; color: var(--muted); min-width: 0; }
.act__bad { color: var(--bad); }
/* DELIBERATELY NOT .meter. Home draws no charts, donuts, funnels or meters —
   those went to Analytics on 2026-09-04 and test/ui-probe.mjs enforces it by
   CLASS NAME. Borrowing .meter here would have made that rule unassertable:
   the probe could no longer tell a progress hair on a live run from an
   analytics meter creeping back onto the summary. So this is its own thing,
   4px, no track colour of its own beyond the rule, and it exists only while
   something is actually running. */
.act__bar {
  grid-column: 1 / -1; height: 4px; margin-top: 2px;
  border-radius: var(--r-pill); background: var(--glass-edge); overflow: hidden;
}
.act__fill {
  height: 100%; width: 0; border-radius: var(--r-pill);
  background: var(--green);
  transition: width .5s var(--ease);
}
.act__go { color: var(--faint); opacity: 0; transition: opacity .16s var(--ease); }
.act:hover .act__go, .act:focus-visible .act__go { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .act--live .act__dot { animation: none; }
}

/* Where an activity row landed you. It fades on its own after two seconds:
   a permanent highlight would still be sitting there tomorrow claiming to
   mean something. Not a focus ring — the keyboard focus stays on the panel,
   because dropping the caret into the middle of a list for somebody who
   pressed a summary row is not where they asked to be. */
.is-pointed {
  animation: pointed 2s var(--ease);
}
@keyframes pointed {
  0%, 60% { box-shadow: 0 0 0 2px var(--green); }
  100% { box-shadow: 0 0 0 2px transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .is-pointed { animation: none; box-shadow: 0 0 0 2px var(--green); }
}

/* ── Messages ──
   The inbox rows follow the people-list grammar (overlay opener, ellipsised
   body); the thread is bubbles — mine on the right in the brighter surface,
   theirs on the left. Colour alone never carries "who": alignment and the
   sender line do. */
.convs { list-style: none; margin: 0; padding: 0; display: grid; }
.conv {
  position: relative; display: flex; gap: 12px; align-items: center;
  padding: 16px 10px; min-width: 0;
  border-top: 1px solid var(--hairline, rgba(255, 255, 255, .08));
}
.conv:first-child { border-top: none; }
.conv img {
  width: 40px; height: 40px; border-radius: 50%; object-fit: cover;
  background: var(--surface-3); flex: none; border: 1px solid var(--control-border);
}
.conv__body { min-width: 0; flex: 1; display: grid; gap: 3px; }
.conv__top { display: flex; gap: 8px; align-items: baseline; min-width: 0; }
.conv__who { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.conv__when { margin-left: auto; font-family: var(--mono); font-size: 11.5px; color: var(--faint); flex: none; }
.conv__sub { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--faint); font-size: 12.5px; }
.conv__last { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--muted); font-size: 13.5px; }
/* Unread: the name and the preview carry weight, and a dot sits before the
   time. Weight and the dot together, so colour never carries it alone. */
.conv--unread .conv__who, .conv--unread .conv__last { font-weight: 600; color: var(--ink, inherit); }
.conv--unread .conv__when::before { content: '●'; margin-right: 6px; font-size: 8px; vertical-align: middle; color: var(--green, currentColor); }
.conv__updated { display: inline-block; margin-left: 6px; font-family: var(--mono); font-size: 11.5px; color: var(--faint); }
.conv__updated:empty { display: none; }
.conv .tag { flex: none; }
.conv__open { position: absolute; inset: 0; background: none; border: none; cursor: pointer; border-radius: var(--r); }
.conv__open:hover, .conv__open:focus-visible { background: rgba(255, 255, 255, .04); }

.thread {
  list-style: none; margin: 0; padding: 4px 2px 0;
  display: grid; gap: 10px; max-height: 480px; overflow-y: auto;
}
/* As a pane it takes the height the card has left instead of a flat 480px, and
   `.pane` already supplies the scrolling. The literal cap has to go or it wins
   over the flex basis and the messages stop at 480px inside a taller card. */
.thread.pane { max-height: none; align-content: start; }
/* Their bubbles carry the sender's picture in a first column; mine have no
   picture, so the grid collapses to the one column. */
.msg { max-width: 46rem; justify-self: start; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 3px 8px; align-items: end; }
.msg--me { justify-self: end; grid-template-columns: minmax(0, 1fr); }
.msg__avatar {
  grid-column: 1; grid-row: 1 / span 3; align-self: end;
  width: 28px; height: 28px; border-radius: 50%; object-fit: cover;
  background: var(--surface-3); border: 1px solid var(--control-border);
}
.msg__who, .msg__text, .msg__when { grid-column: 2; }
.msg--me .msg__who, .msg--me .msg__text, .msg--me .msg__when { grid-column: 1; }
.msg__text {
  margin: 0; padding: 9px 13px;
  border-radius: 12px 12px 12px 4px;
  background: rgba(255, 255, 255, .06);
  border: 1px solid var(--glass-edge);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.msg--me .msg__text {
  border-radius: 12px 12px 4px 12px;
  background: rgba(255, 255, 255, .14);
  border-color: rgba(255, 255, 255, .28);
}
.msg__who { font-size: 11.5px; color: var(--faint); }
.msg__when { font-size: 11px; font-family: var(--mono); color: var(--faint); }
.msg--me .msg__when { justify-self: end; }

/* The Composer's read of the open thread: a quiet block under the messages,
   never a dark object, with the replies as rows that each end in one
   button. */
.suggest {
  margin-top: 12px; padding: 12px 14px;
  border: 1px solid var(--hairline, rgba(255, 255, 255, .08)); border-radius: var(--r);
  background: rgba(255, 255, 255, .03);
  display: grid; gap: 8px;
}
.suggest__head { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; }
.suggest__about { margin: 0; }
.suggest__next { margin: 0; color: var(--muted); font-size: 13.5px; }
.suggest__next:empty, .suggest__about:empty { display: none; }
.suggest__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.suggest__item {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 10px; align-items: start;
  padding: 10px 12px; border-radius: 10px;
  background: rgba(255, 255, 255, .04); border: 1px solid var(--glass-edge);
}
.suggest__item .tag { margin-top: 2px; }
.suggest__text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
@media (max-width: 640px) {
  .suggest__item { grid-template-columns: auto minmax(0, 1fr); }
  .suggest__item .btn { grid-column: 2; justify-self: start; }
}

/* The inline reply under a thread. One box, one Send; no drawer. */
.reply { display: grid; gap: 8px; margin-top: 12px; }
.reply__box { width: 100%; min-height: 64px; resize: vertical; font: inherit; line-height: 1.45; }
.reply__row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
/* A bubble that is on its way, and one that did not get there. Opacity plus
   the time label carry the state; colour never carries it alone. */
.msg--pending { opacity: .55; }
.msg--failed .msg__text { border-color: var(--bad, #e5484d); border-style: dashed; }
.msg--failed .msg__when { color: var(--bad, #e5484d); }

/* ── Analysis ──
   A table, because "what requests went where" is tabular fact — and the
   wrap scrolls sideways so the page never does. */
.tablewrap { overflow-x: auto; }
.ana { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.ana th {
  text-align: left; font-size: 11px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--faint); font-weight: 600;
  padding: 6px 10px; border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.ana td { padding: 8px 10px; border-top: 1px solid var(--hairline, rgba(255, 255, 255, .08)); }
.ana tr:first-child td { border-top: none; }
.ana .num, .ana th.num { text-align: right; font-family: var(--mono); }
.ana .ana__when { color: var(--faint); font-size: 12.5px; white-space: nowrap; }
.ana .ana__empty { color: var(--muted); }

/*
 * The one-sentence read that sits above a figure and says what it means.
 *
 * Deliberately NOT a `.note`: a note is a status message, it appears and
 * disappears, and it carries a tone colour that says whether something is
 * wrong. This is a finding. It is always true when it is shown, it is nobody's
 * fault, and it earns the accent rail rather than a warning one.
 */
.ana__verdict {
  margin: 0 0 4px;
  padding: 10px 14px;
  border-left: 2px solid var(--green-dim);
  background: var(--glass);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  color: var(--text);
  font-size: 13.5px;
  line-height: 1.55;
}

/*
 * The opener list. A message is prose, so it gets prose treatment: full width,
 * readable line length, its numbers underneath rather than in a column beside
 * it. Squeezing the text into a table cell was the alternative, and it makes
 * every opener unreadable at exactly the moment the user is trying to compare
 * two of them.
 */
.openers { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 2px; }

.opener {
  padding: 12px 0;
  border-top: 1px solid var(--hairline, rgba(255, 255, 255, .08));
}
.opener:first-child { border-top: none; }

.opener__text {
  margin: 0;
  color: var(--text);
  font-size: 13.5px;
  line-height: 1.55;
  max-width: var(--page-prose);
  /* A pasted opener carries its own newlines and they are part of what is
     being compared: an opener that opens with a line break reads differently
     from one that does not. */
  white-space: pre-wrap;
}

.opener__meta { margin: 6px 0 0; font-family: var(--mono); font-size: 12px; }

.openers .quiet { padding: 10px 0 0; color: var(--faint); font-size: 12.5px; }

.linklike {
  background: none; border: none; padding: 0; cursor: pointer;
  font: inherit; color: var(--text); text-decoration: underline;
}
.linklike:disabled { opacity: .6; cursor: default; }

.chart { position: relative; }
.chart svg { display: block; width: 100%; height: auto; }
.chart .grid { stroke: var(--border); stroke-width: 1; }
/* Bars grow from the baseline (y/height transition) and light up on hover. */
.chart .bar,
.chart .bar--quiet {
  fill: var(--green);
  transition: y .55s var(--ease), height .55s var(--ease), fill .15s var(--ease);
}
.chart .bar--quiet { fill: var(--green-dim); }
.chart .is-hot { fill: var(--green-bright); }
.chart .tick { fill: var(--faint); font-size: 11px; font-family: var(--mono); }
.chart .val { fill: var(--grey-3); font-size: 11px; font-family: var(--mono); }
/* The growth line: 2px round-joined, drawn on via dashoffset; its area is
   the hue at a whisper; the end dot wears a surface ring; the crosshair is
   a recessive hairline parked off-canvas until hover. */
.chart .line {
  fill: none;
  stroke: var(--green);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
  transition: stroke-dashoffset 1.1s var(--ease);
}
.chart .area { fill: var(--green); opacity: .1; }
.chart .dot { fill: var(--green-bright); stroke: var(--bg); stroke-width: 2; }
.chart .cross { stroke: var(--border-strong); stroke-width: 1; }

/* ── donuts ──
   Segments are circle strokes swept in by a stroke-dasharray transition;
   the hole carries the hero figure. Hover thickens the segment — the same
   acknowledgement the buttons give a pointer. */
.donut { display: grid; justify-items: center; gap: 10px; }
.donut svg { overflow: visible; }
.donut__track { fill: none; stroke: rgba(255, 255, 255, .07); }
.donut__seg {
  fill: none;
  transform: rotate(0.0001deg); /* promotes smooth stroke animation */
  transition: stroke-dasharray .9s var(--ease), stroke-width .18s var(--ease);
}
.donut__seg:hover { stroke-width: 24; cursor: default; }
.donut--gauge .donut__seg:hover { stroke-width: 9; }
/* With a white accent the old bright/base pair would be two whites, so the
   ramp is white → --green-dim → --grey: three steps anyone can tell apart. */
.donut__seg--accepted { stroke: var(--green); }
.donut__seg--pending { stroke: var(--green-dim); }
.donut__seg--other { stroke: var(--grey); }
.donut__seg--spent { stroke: var(--green); }
.donut__seg--email { stroke: var(--green); }
.donut__seg--phone { stroke: var(--green-dim); }
.donut__seg--none { stroke: var(--grey); }
.donut__seg--sent { stroke: var(--green); }
.donut__seg--waiting { stroke: var(--green-dim); }
.donut__seg--missed { stroke: var(--grey); }
.donut--gauge .donut__track { stroke: var(--green-deep); }
.donut__num { fill: var(--white); font-family: var(--mono); font-weight: 700; font-size: 26px; }
.donut__sub { fill: var(--faint); font-size: 11px; letter-spacing: .12em; text-transform: uppercase; }
.donut--gauge .donut__num { font-size: 15px; }
.donut--gauge { flex: none; }

/* max-width: the legend sits in .donut, whose justify-items: center sizes
   each item to its content, so without a cap the row is as wide as its three
   items and never wraps; at 320px that was 191px of sideways scroll
   (probe:widths, 2026-09-04). */
.legend { display: flex; gap: 14px; flex-wrap: wrap; justify-content: center; max-width: 100%; font-size: 12.5px; color: var(--muted); }
.legend__item { display: inline-flex; align-items: center; gap: 6px; }
.legend__item strong { color: var(--grey-3); font-family: var(--mono); font-weight: 600; }
.legend__swatch { width: 9px; height: 9px; border-radius: 3px; flex: none; }
.legend__swatch--accepted { background: var(--green); }
.legend__swatch--pending { background: var(--green-dim); }
.legend__swatch--other { background: var(--grey); }
.legend__swatch--email { background: var(--green); }
.legend__swatch--phone { background: var(--green-dim); }
.legend__swatch--none { background: var(--grey); }
.legend__swatch--sent { background: var(--green); }
.legend__swatch--waiting { background: var(--green-dim); }
.legend__swatch--missed { background: var(--grey); }

/* Shared hover tooltip; positioned by JS, never intercepts the pointer. */
.chart-tip {
  position: absolute;
  pointer-events: none;
  background: var(--surface-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  padding: 5px 10px;
  font-size: 12.5px;
  white-space: nowrap;
  transform: translate(-50%, -110%);
  box-shadow: var(--lift);
  z-index: 3;
}
.chart-tip strong { font-family: var(--mono); font-weight: 700; }

/* Meter: severity fill on a lighter step of the same green ramp. The words
   beside it carry the judgement — amber vs red alone is not a signal. */
.meter {
  height: 12px;
  border-radius: var(--r-pill);
  background: var(--green-deep);
  overflow: hidden;
}
.meter__fill {
  height: 100%;
  width: 0;
  border-radius: var(--r-pill);
  background: var(--green);
  transition: width .5s var(--ease), background .3s var(--ease);
}
.meter__fill.is-warn { background: var(--warn); }
.meter__fill.is-over { background: var(--bad); }

/* ── The outreach funnel ──
   Ordered stages of ONE process, which is why this is a rail of bars sharing
   a left edge and not a row of tiles: every step can only be smaller than the
   one above it, and the DROP between two of them is what the figure is for.
   Tiles would let the eye read them as five unrelated counts.

   The bars step down one ramp of the single accent rather than taking five
   hues. Five colours would imply five kinds of thing; there is one kind of
   thing at five depths. The label and the value carry identity regardless,
   per the house rule that colour is never the only channel — which matters
   more here than usual, because a stage can legitimately be an em dash. */
.funnel { display: grid; gap: 12px; margin: 0; padding: 0; list-style: none; }
.funnel__step {
  display: grid;
  grid-template-columns: 5.5rem 1fr auto;
  gap: 12px;
  align-items: center;
}
.funnel__label { font-size: 13px; color: var(--muted); }
.funnel__bar {
  height: 18px;
  min-width: 2px;
  border-radius: 0 4px 4px 0;
  background: var(--green);
  transition: width .55s var(--ease);
}
/* One ramp, deepest at the top of the funnel. A stage further down is a
   smaller number AND a quieter bar, so the shape reads before the digits. */
.funnel__step--accepted .funnel__bar { background: var(--green-bright); }
.funnel__step--messaged .funnel__bar { background: var(--green-dim); }
.funnel__step--replied  .funnel__bar { background: var(--green-deep); border: 1px solid var(--green-dim); }
.funnel__value {
  font-family: var(--mono);
  font-size: 12.5px;
  font-weight: 600;
  color: var(--white);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

@media (max-width: 30rem) {
  .funnel__step { grid-template-columns: 4.5rem 1fr auto; gap: 8px; }
  .funnel__label { font-size: 12px; }
}

/* Categorical bars: horizontal, ≤24px thick, rounded data-end, square
   baseline. (The pipeline that first used them was cut 2026-08-15; the
   tag and source breakdowns keep the form.) */
.pipes { display: grid; gap: 8px; }
.pipe { display: grid; grid-template-columns: 8.5rem 1fr 3rem; gap: 12px; align-items: center; }
.pipe__label { font-size: 13px; color: var(--muted); text-align: right; }
.pipe__track { background: rgba(255, 255, 255, .05); border-radius: 0 4px 4px 0; }
.pipe__bar {
  height: 16px;
  border-radius: 0 4px 4px 0;
  background: var(--green);
  min-width: 2px;
  transition: width .5s var(--ease);
}
/* The quiet variant is a step dimmer — categorical position carries the
   identity, so the accent hue is not needed at full strength. */
.pipe__bar--quiet { background: var(--green-dim); }
.pipe__value { font-family: var(--mono); font-size: 12.5px; color: var(--grey-3); }

.hero { display: grid; gap: 16px; }
.hero__banner { display: none; }
.hero__main { display: flex; gap: 18px; align-items: center; flex-wrap: wrap; }
.hero__avatar {
  width: 72px; height: 72px; border-radius: 50%; object-fit: cover;
  background: var(--surface-3); flex: none;
  border: 1px solid var(--control-border);
  box-shadow: 0 0 0 3px rgba(255,255,255,.14);
}
.hero__body { flex: 1 1 16rem; min-width: 0; display: grid; gap: 3px; }
.hero__actions { display: flex; gap: 10px; flex-wrap: wrap; }
.hero__checked { color: var(--faint); font-family: var(--mono); font-size: 12px; }

/* How you sign in: one row per provider inside the account card, since
   2026-09-04 (docs/console-ia-2026-09-04.md). A row is a name with its
   badge, a line of explanation, and the one button, on the card's own rule.
 *
 * The BLOCK is addressed by its id, not by a bare `.signin`, and that is the
 * whole point of this rule rather than a preference. `styles.css` is loaded by
 * the sign-in page too, and login.html's entire column has been
 * `<main class="signin">` since 2026-09-02. A bare `.signin` therefore painted
 * this divider straight across the top of /login, above the wireagent mark: a
 * short blue-tinted rule with nothing above it to divide, on a page that has
 * no header at all. `site.css` overrides the width, display and gap there but
 * declares neither `border-top` nor `padding-top`, so those two were the only
 * properties that leaked, which is exactly why it read as a stray line rather
 * than a broken layout. The `signin__*` children below stay class-based: they
 * exist only in index.html and collide with nothing. */
#signin-methods { display: grid; gap: 12px; padding-top: 14px; border-top: 1px solid var(--glass-edge); }
.signin__lead { margin: 0; }
.signin__row { display: flex; gap: 14px; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; }
.signin__body { display: grid; gap: 4px; flex: 1 1 18rem; min-width: 0; }
.signin__body p { margin: 0; }
.signin__name { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; font-size: 14px; }

/* ── About you (2026-09-08): the two scopes of the survey, on Settings.
 *
 * Shaped on the sign-in block above because it is the same thing: a labelled
 * group of rows inside a card, each row a name, a value and one press. The
 * classes are its own rather than reused, because `signin__*` belongs to the
 * block above and to /login, and sharing them would tie two unrelated surfaces
 * together the next time either moves.
 *
 * Addressed by class rather than by id, unlike `#signin-methods`: that rule is
 * id-scoped because `styles.css` is loaded by login.html, whose whole column is
 * `<main class="signin">`. Nothing on any other page is `.about`, so there is
 * no such collision to avoid here. */
.about { display: grid; gap: 12px; padding-top: 14px; border-top: 1px solid var(--glass-edge); }
.about__lead { margin: 0; }
.about__row { display: flex; gap: 14px; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; }
.about__body { display: grid; gap: 4px; flex: 1 1 18rem; min-width: 0; }
.about__body p { margin: 0; }
.about__name { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; font-size: 14px; }
/* `.about__edit`, `.about__choices` and `.about__list` dressed the per-field
   editors and came out with them on 2026-09-08. The card is a read-back and one
   press now, so the rules above are all it needs; dead selectors left behind are
   how a stylesheet stops describing its own page. */

/* ── people: opaque rows. Glass behind 40 rows costs legibility. */

.people {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: 1px;
  background: var(--border);
  border: 1px solid var(--glass-edge);
  border-radius: var(--r);
  /* No inner scroll (2026-09-02 night): a 34rem box with its own scrollbar
     put a scrollbar over the rows' right edge and a scroll inside the page's
     scroll. The lists are paged by Load more, so the page scrolls instead. */
  overflow: hidden;
}

/* A company heading inside the people grid: the same 1px gutter, the page
   ground rather than a row's, the name and a count on one line. Not a row,
   so no overlay button and no tab stop. */
.people__group {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 10px 14px 6px;
  background: var(--bg);
}
.people__group-name { font-weight: 700; font-size: 13px; letter-spacing: .01em; }
.people__group-meta { font-size: 12px; color: var(--faint); white-space: nowrap; }

/**
 * A row is a <li> whose NAME is a real <button> (.person__open). That button
 * carries an ::after overlay stretched across the whole row, so the entire
 * row stays clickable while the row contributes exactly one tab stop.
 *
 * The row used to be a bare <li> with a click handler and cursor:pointer —
 * completely unreachable by keyboard, and announced as plain text.
 *
 * Why the button wraps only the name, rather than the whole row: the row
 * also holds `mailto:` and `tel:` links and action buttons. Interactive
 * content cannot nest, so anything wrapping them would be invalid markup
 * and would swallow them. They opt back above the overlay with z-index
 * instead — see .person__email a and .person__actions.
 */
.person {
  position: relative;
  display: flex; align-items: center; gap: 14px;
  /**
   * Wrapping is the base behaviour, not a phone exception.
   *
   * It used to be applied only below 560px, with a comment explaining that
   * the name is what the row is ABOUT and must not be the thing that gets
   * squeezed. That reasoning was right and the breakpoint was wrong: on the
   * Saved tab, whose rows carry the most buttons, the row was 545px of content
   * in a 482px list at a 576px viewport — one pixel above the rule that would
   * have saved it. `.person__body` measured ZERO pixels wide. The row about a
   * person was showing the person's name at no width at all, and the list's
   * `overflow-x: hidden` quietly ate the 63px difference so nothing scrolled
   * and nothing looked wrong.
   *
   * The basis on the body is what makes wrapping useful rather than merely
   * legal: without it the name still collapses first and the buttons still win.
   */
  flex-wrap: wrap; row-gap: 10px;
  padding: 16px;
  background: var(--surface);
  min-width: 0;
  transition: background .14s var(--ease);
}
.person:hover { background: var(--surface-2); }
.person:has(.person__open:focus-visible) { background: var(--surface-2); }

.person__open {
  position: static;             /* so ::after resolves against .person, not itself */
  font: inherit; font-weight: 600; text-align: left;
  background: none; border: none; padding: 0; margin: 0;
  color: var(--text); cursor: pointer;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The overlay: the whole row is clickable, but only one focusable thing. */
.person__open::after { content: ""; position: absolute; inset: 0; }
.person__open:focus-visible { outline: none; }
.person__open:focus-visible::after {
  outline: 2px solid var(--green); outline-offset: -3px; border-radius: var(--r-sm);
}

.person img {
  width: 40px; height: 40px; border-radius: 50%;
  object-fit: cover; background: var(--surface-3); flex: none;
}
/* `1 1 12rem`, not `1 1 0`. A zero basis means the name column is the first
   thing flexbox takes width from, so on the Saved tab — the row with the most
   buttons — it was squeezed to 15px while every button beside it kept its full
   size. A basis gives it something to hold, and the row wraps rather than
   crushing it. The 560px rule below already did this with 10rem; it just never
   applied above 560, which is where the Saved rows actually broke. */
/*
 * A ROW'S LINES NEED AIR BETWEEN THEM (2026-09-09).
 *
 * The owner, looking at a real account: "I find that all of them are touching."
 * He was right, and it measured as zero. This was a plain block, so its four
 * children - the name, the headline, the company and place, and the line saying
 * which of your words matched - stacked with NOTHING between them and only their
 * own line-height to separate them. test/discover-ui.mjs measured 16 gaps across
 * the visible rows and every one of them was 0px.
 *
 * The Inbox has done this correctly since it was built: .conv__body two hundred
 * lines above is a grid with a 3px gap. So this is that pattern applied here
 * rather than a new one. 5px rather than 3px because a person row carries up to
 * four lines where a conversation carries two, and the same gap reads tighter
 * the more lines it has to separate.
 *
 * It was invisible to every existing check for a reason worth remembering: none
 * of these lines paints a background, and the gap probes deliberately compare
 * only siblings that paint, because that is the set which can cut through one
 * another. Text rhythm is now its own assertion.
 */
.person__body { min-width: 0; flex: 1 1 12rem; overflow: hidden; display: grid; gap: 5px; }
.person__name { font-weight: 600; font-size: 14.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.person__headline { color: var(--muted); font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.person__email { font-size: 12.5px; font-family: var(--mono); color: var(--green); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.person__email a { color: inherit; text-decoration: none; position: relative; }
/* The why-this-person line: which of your search words hit which field.
   Accent mid-step so it reads as the tool's annotation, never as something
   LinkedIn said; wraps rather than truncating because the reason IS the
   content. 6.43:1 on the worst surface, per the ramp notes at the top of
   this file. Was .person__reason, when it printed an AI opinion. */
.person__why { color: var(--green-dim); font-size: 12.5px; line-height: 1.45; }

/* Employer and place, read straight off the search card. Deliberately
   quieter than the headline and NOT in the accent: this is what LinkedIn
   said, not what the matcher thinks, and --faint keeps it separable from
   .person__why at a glance. Truncates rather than wraps, because a row
   growing a second line changes the rhythm of the whole list. */
.person__meta { color: var(--faint); font-size: 12.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* -- the campaign picker: a chosen row

   Added 2026-08-19 for the Campaigns audience step, where a row TOGGLES
   rather than opening a profile. The same .person shell is reused so the list
   reads as the Connections tab it was asked to look like.

   THE STATE IS SAID THREE WAYS, deliberately rather than as belt and braces:
   a tinted surface for the eye scanning the list, a solid left bar, and the
   word "Selected" in the row itself. a11y.mjs pins the focus ring as an
   outline precisely because box-shadow is discarded in forced-colors; a
   highlight carried by background alone would disappear under exactly the
   conditions that rule was written for. */
.person--picked {
  background: color-mix(in srgb, var(--green) 9%, var(--surface));
  box-shadow: inset 3px 0 0 0 var(--green);
}
.person--picked:hover { background: color-mix(in srgb, var(--green) 14%, var(--surface)); }

/* A browser without color-mix gets the flat surface rather than no cue at
   all; the bar and the word still carry the state. */
@supports not (background: color-mix(in srgb, red 50%, blue)) {
  .person--picked { background: var(--surface-2); }
}

/* The word, so the state is never carried by colour alone. */
.person__picked {
  margin-left: auto; flex: none;
  font-size: 11.5px; font-family: var(--mono); letter-spacing: .02em;
  color: var(--green);
}
.person__picked--off { color: var(--faint); }

/* The row opener is a toggle here, not a link to a profile, so a chosen row
   states its name in full ink rather than in the muted tone. */
.person--picked .person__open { color: var(--text); }

@media (forced-colors: active) {
  /* color-mix and box-shadow are both discarded in this mode, so the chosen
     row needs a border it actually keeps. */
  .person--picked { border-inline-start: 3px solid Highlight; }
  .person__picked { color: Highlight; }
}


/* Discover's narrowing controls. A select rather than a row of chips
   because the place and company lists come from the results and can run
   to a dozen each — chips would wrap the filter bar onto three lines. */
.mini-select {
  background: var(--surface-2); color: var(--text); border: 1px solid var(--control-border);
  border-radius: 8px; padding: 5px 8px; font: inherit; font-size: 12.5px; max-width: 190px;
}
.mini-select:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
/* A hairline divider inside a control row: the degree chips and the facet
   selects are different KINDS of narrowing, and the gap alone did not say so. */
.sep { width: 1px; align-self: stretch; background: var(--border); margin: 0 2px; }

/**
 * Row actions sit above the overlay. They are NOT hidden until hover any
 * more: a control that only exists on hover cannot be found by touch, by
 * keyboard, or by anyone scanning the page — it just looks like it is not
 * there. They dim instead, and come to full strength on hover or focus.
 */
.person > .btn, .person__actions { position: relative; z-index: 1; flex: none; }
.person__actions { display: flex; gap: 8px; }

/* The Hook and draft panel (docs/discover-prompt-2026-09-04.md): opens under
   its row, full width, and opts above the row's click overlay like the
   actions do, or its chips and boxes would not take a press. Each step is
   its own block so Data, Prompt, Hook and the Draft press read in order. */
.person__prepare {
  position: relative; z-index: 1; flex-basis: 100%;
  display: grid; gap: 10px; padding: 10px 0 4px;
  border-top: 1px solid var(--border);
}
.person__prepare .prepare__step { display: grid; gap: 6px; }
.person__prepare .prepare__step > b { font-size: 12.5px; letter-spacing: .02em; text-transform: uppercase; color: var(--muted); }
.person__prepare .prepare__hooks { flex-wrap: wrap; }
.person__prepare .prepare__hooks .chip { white-space: normal; text-align: left; max-width: 100%; }
.person__prepare textarea.input { width: 100%; }
.person > .btn { opacity: .75; transition: opacity .16s var(--ease); }
.person:hover > .btn, .person:focus-within > .btn { opacity: 1; }

.empty { padding: 40px 20px; text-align: center; color: var(--muted); background: var(--surface); }

.tagrow { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
.tag { font-size: 11.5px; font-weight: 600; padding: 2px 8px; border-radius: var(--r-sm); background: rgba(255,255,255,.12); color: var(--green); }
/* A warm lead's badge: same accent family, just unmistakably stronger
   than an ordinary tag. */
.tag--warm { background: rgba(255,255,255,.22); color: var(--white); border: 1px solid rgba(255,255,255,.5); }

/* ── queue */

.queue { list-style: none; margin: 0; padding: 0; display: grid; gap: 14px; }
.queued {
  display: flex; gap: 14px; align-items: flex-start;
  padding: 14px 16px;
  border: 1px solid var(--glass-edge); border-radius: var(--r);
  background: rgba(255,255,255,.04);
}
.queued img { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; flex: none; background: var(--surface-3); }
/* Same defect, same fix: a queued row stacks a name, a time and a preview with
   nothing between them. */
.queued__body { flex: 1; min-width: 0; display: grid; gap: 5px; }
/* The inline "why?" under a draft on Approval (2026-09-04): field, then row. */
.reject { display: grid; gap: 10px; margin-top: 10px; }
/* The picker's filter takes the row; the pick buttons sit at the row's end. */
#pick-search { flex: 1 1 14rem; min-width: 0; }
#pick-list .queued { align-items: center; }
/* The Approval measures: a label on the left, its value on the right. They
   were two inline spans with nothing between them ("Median time to decide0"). */
#ap-stats .queued__head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; flex-wrap: wrap; width: 100%; }
#ap-stats .queued__head b { font-family: var(--mono); font-size: 15px; }
.queued__when { font-size: 12px; font-family: var(--mono); color: var(--muted); }
.queued__text { color: var(--muted); font-size: 13px; margin: 5px 0 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

/* ── browsers */

.browsers { display: grid; gap: 10px; }
.browser {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 14px 16px;
  border: 1px solid var(--glass-edge); border-radius: var(--r);
  background: rgba(255,255,255,.04);
  transition: border-color .16s var(--ease), background .16s var(--ease);
}
.browser--on { border-color: var(--green); background: rgba(255,255,255,.08); }
.browser__name { font-weight: 600; font-size: 14.5px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* ── disclosures */

.importer, .fold {
  border: 1px solid var(--glass-edge); border-radius: var(--r);
  padding: 14px 16px; background: rgba(255,255,255,.04);
}
.importer summary, .fold summary {
  cursor: pointer; font-weight: 600; font-size: 14.5px;
  min-height: var(--tap-sm); display: flex; align-items: center; gap: 10px;
}
.importer > *:not(summary), .fold > *:not(summary) { margin-top: 12px; }
.importer input[type=text] { flex: 1 1 12rem; }
.steps { margin: 10px 0 0; padding-left: 20px; color: var(--muted); }
.steps li { margin-bottom: 5px; }

.sso {
  display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
  padding: 18px;
  background: linear-gradient(140deg, rgba(255,255,255,.14), rgba(255,255,255,.04));
  border: 1px solid rgba(255,255,255,.4);
  border-radius: var(--r);
}
.sso > div { flex: 1 1 15rem; }
.sso p { margin: 5px 0 0; }

/* ── drawer */

.backdrop { position: fixed; inset: 0; background: rgba(4, 6, 14, .7); backdrop-filter: blur(4px); z-index: 20; animation: fade .24s var(--ease); }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }

.drawer {
  position: fixed; right: 0; top: 0; bottom: 0;
  width: min(460px, 100vw);
  background: linear-gradient(160deg, rgba(255,255,255,.08), rgba(255,255,255,.03));
  backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  border-left: 1px solid var(--glass-edge);
  box-shadow: inset 1px 0 0 var(--glass-spec), var(--lift-glass);
  padding: 20px 22px 28px; z-index: 21;
  display: flex; flex-direction: column; gap: 16px; overflow-y: auto;
  /* Companion to the fixed shell (2026-09-08). `app.js` freezes what is behind
     an overlay by setting `body` overflow, which works while the DOCUMENT is
     the thing that scrolls — still true below the floor, so that lock stays.
     Above the floor the scrollers are the bounded panes inside the page, and a
     `body` lock says nothing about them: reaching the end of this drawer would
     chain the wheel straight into the list underneath. `contain` stops the
     chain at this element's own edge, which is the part the body lock cannot
     express. */
  overscroll-behavior: contain;
  animation: slide .32s var(--ease);
}
@supports not (backdrop-filter: blur(1px)) { .drawer { background: var(--surface); } }
/* It comes from the right edge, which is where it lives. */
@keyframes slide { from { transform: translateX(24px); opacity: 0; } to { transform: none; opacity: 1; } }

#pane-profile, #pane-compose { display: grid; gap: 16px; }
#pane-compose[hidden], #pane-profile[hidden] { display: none; }

.drawer__head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  position: sticky; top: -20px; z-index: 2;
  margin: -20px -22px 0; padding: 16px 18px 12px 22px;
  background: rgba(12, 17, 34, .8);
  backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  border-bottom: 1px solid var(--glass-edge);
}
.drawer__to { display: flex; align-items: center; gap: 14px; min-width: 0; }
.drawer__to img { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; background: var(--surface-3); }
.drawer__to div { display: grid; min-width: 0; }
.drawer__to span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.drawer__from { display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: rgba(255,255,255,.05); border-radius: var(--r-sm); }
.drawer__from img { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; }
.drawer__meta { display: flex; justify-content: space-between; gap: 16px; font-family: var(--mono); font-size: 12.5px; }

.profile { display: grid; gap: 6px; }
.profile h2 { margin-top: 6px; }
.profile .row { margin-top: 14px; }
.profile__banner {
  height: 92px; border-radius: var(--r);
  background: linear-gradient(140deg, rgba(140,162,230,.3), rgba(255,255,255,.05));
  background-size: cover; background-position: center;
  margin-bottom: -38px;
  border: 1px solid var(--glass-edge);
}
.profile__avatar {
  width: 76px; height: 76px; border-radius: 50%; object-fit: cover;
  border: 2px solid rgba(255,255,255,.18);
  background: var(--surface-3); margin-left: 10px;
  box-shadow: var(--lift-glass);
}

.contact {
  margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 8px 16px;
  padding: 14px 16px;
  border: 1px solid var(--glass-edge); border-radius: var(--r);
  background: rgba(0,0,0,.25);
  font-size: 13px;
}
.contact dt { font-weight: 600; color: var(--muted); }
.contact dd { margin: 0; min-width: 0; overflow-wrap: anywhere; white-space: pre-wrap; font-family: var(--mono); font-size: 12.5px; }

/* ── logo */

.logo { display: inline-flex; align-items: center; gap: 10px; text-decoration: none; color: inherit; }
.logo__word { font-weight: 600; letter-spacing: -.03em; font-size: 17px; color: var(--text); }

/**
 * The mark is a solid squircle, so it needs no stroke colour — `color`
 * fills the tile and the glyph is knocked out in the fixed green-ink so it
 * stays legible on any surface the mark is placed on.
 */
.logo__mark {
  flex: none;
  color: var(--logo-tile);             /* tile; the glyph is knocked out in --logo-ink */
  border-radius: 7px;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .12), 0 6px 18px -6px rgba(255, 255, 255, .35);
  transition: transform .18s var(--ease), box-shadow .18s var(--ease);
}
a:hover > .logo__mark, .logo:hover .logo__mark {
  transform: translateY(-1px);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .2), 0 8px 22px -6px rgba(255, 255, 255, .5);
}

/* =========================================================================
   Motion layer

   Still under Base44's rule — "motion should support clarity, not hide weak
   structure" — so each of these is tied to something real: an arrival, a
   state change, or a pointer. Nothing loops for decoration except the two
   ambient pieces (aurora, live dot), and both are switched off entirely
   under prefers-reduced-motion.
   ========================================================================= */

/* THE GAP INSIDE EVERY TAB (2026-08-17). This rule was always claimed by
   the [hidden] comment near the top of the file and never actually
   existed, so every panel's boxes (the stats row, the live bar, every
   card) stacked flush against each other on every single tab. The panel
   is the grid that spaces its boxes; .content only spaces what sits
   OUTSIDE the panels. Same clamped rhythm as the page, so desktop
   breathes and phones stay dense. [hidden]'s !important still collapses
   inactive panels over this display. */
.panel, #view-signedin { display: grid; gap: clamp(14px, 1.4vw, 20px); align-content: start; }

/*
 * 2026-09-08: THE HEIGHT-SHARING ARRANGEMENT IS GONE.
 *
 * `#view-signedin` and `.panel` were flex columns whose whole job was to divide
 * a fixed screen height between cards: the panel took what was left of the
 * screen, then handed its own leftover to whichever cards held a list. Every
 * rule below was in service of that, including a `min-content` floor that
 * existed to stop the squeezing from cutting off a card's own footer.
 *
 * There is no leftover to divide now. A panel is as tall as its cards, a card
 * is as tall as its content, and the page scrolls. So these are plain grids
 * that start at the top, and the floor and the shares are deleted rather than
 * carried forward, because a rule that guards against a constraint you removed
 * is a rule nobody can reason about later.
 */
#view-signedin {
  display: grid;
  align-content: start;
  gap: clamp(14px, 1.4vw, 20px);
  min-width: 0;
}

.panel {
  display: grid;
  align-content: start;
  gap: clamp(14px, 1.4vw, 20px);
  min-width: 0;
}
.panel > * { min-width: 0; }

/**
 * BOUNDED PANES — the half of one-screen that makes it survive real data.
 *
 * A list grows with the account, so no layout can ever make it fit: measured
 * with 40 rows, the Follow-ups day list alone reached 66,824px. The answer is
 * not to shrink the page, it is to stop the list being the thing that sets the
 * page's height. The list gets a bounded region and scrolls inside it, and the
 * page does not move.
 *
 * This is not a new pattern here, which is the argument for it: `.rail`,
 * `.ac-log`, the thread list, the drawer and `.discover-work` all already bound
 * themselves this way. What changes is that it becomes the rule rather than
 * five separate exceptions.
 *
 * There are eighteen, and they were found by measuring every block in every
 * panel at one row and again at forty and keeping the ones whose height
 * changed. Reading the markup and reasoning about it had already missed ten of
 * them.
 *
 * FLEX again, and for the same reason as the two rules above: the head, the
 * filter row and the footer keep their own heights and exactly one child
 * absorbs the rest. A grid would have to name which row that is, and it is a
 * different row in almost every card.
 */
.card:has(> .pane) {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}
/* `:not(.pane)` is load-bearing: `.card:has(> .pane) > *` is the more specific
   selector, so without the exclusion it would win over `.pane` below and pin
   the list to `flex: 0 0 auto` — the card would still be told to bound itself
   while nothing inside it was allowed to shrink, and the content would clip
   instead of scrolling. */
.card:has(> .pane) > *:not(.pane) { flex: 0 0 auto; }

.pane {
  flex: 1 1 0;
  /* NOT 0, and this is the subtle one. With `min-height: 0` the card's own
     `min-content` floor resolves to just its head, filters and footer, so on a
     crowded page the pane was squeezed to 2px: the list still "scrolled" and
     nothing reported as clipped, but a 115px row was rendering inside a 2px
     box and mid-row clicks fell through to the panel behind. A page that fits
     because its lists collapsed to nothing has not been made to fit.
     7rem is the least that still reads as a list and shows that it continues.
     It is deliberately close to the probe's starvation threshold rather than
     comfortably above it: a card can hold two panes (Home's plan holds two),
     and every floor is paid twice there, so a generous floor on one list is
     taken straight out of the card beside it. */
  min-height: 7rem;
  /*
   * ROWS DO NOT SHARE THE SPARE HEIGHT (2026-09-09).
   *
   * Most panes are `display: grid`, so the moment a pane is allowed to fill a
   * taller card its ROWS divide the extra between them: the Inbox's two
   * conversations became 250px tall each, one row of text floating in the
   * middle of a quarter of the screen. This is the same failure recorded
   * against `align-items: stretch` on 2026-09-08, one level further in, and it
   * has the same answer. A list that does not fill its pane should leave the
   * space at the bottom, where it reads as a list that has ended.
   */
  align-content: start;
  overflow-y: auto;
  /* Stops at its own edge rather than chaining the wheel on into the page, or
     into whatever overlay is sitting on top of it. */
  overscroll-behavior: contain;
  /* The recorded objection to an inner scrollbar was that it "sits over the
     rows" (test/ui-probe.mjs). That is true of an overlay scrollbar; a stable
     gutter reserves the space instead, so the rows keep their full width and
     nothing sits on top of them. The objection is answered, not overruled. */
  scrollbar-gutter: stable;
  scrollbar-width: thin;
}

/**
 * TWO COLUMNS — where the pages actually start fitting.
 *
 * One column could never have got there on its own. With every list bounded,
 * the panels were still over by 257 to 1392px at 1440x900, because the height
 * is set by the number of boxes and not by any one of them. Standing them side
 * by side is the only move that halves that, and it is what makes the page read
 * as a set of boxes rather than a column you run down.
 *
 * Uniform rows, deliberately. `minmax(0, 1fr)` gives every box the same cell,
 * so the page is a grid and not a ragged stack, and — the part that matters —
 * NO box can be taller than its cell. Combined with the rule below it, that
 * makes clipping impossible by construction rather than by inspection: a box
 * either holds a pane and scrolls its list, or it scrolls itself.
 *
 * `align-items: start` is NOT set: a box fills its cell. A half-empty box
 * looks deliberate; a box that stops halfway up its own outline looks broken.
 *
 * Two columns and not `auto-fit`. Auto-fit would give a 27-inch monitor three
 * or four columns and turn a working page into a wall, and the reading column
 * is capped by `--page` anyway, so the extra track would be narrow rather than
 * useful.
 */
/**
 * THE BAND BETWEEN THE RULE AND THE COLUMNS (found and fixed 2026-09-08).
 *
 * The shell stops scrolling above 900px wide, but the two-column block below
 * only begins at 1000px — and the card scroll fallback lived inside it. So
 * between 901 and 999px the page was fixed, single-column, and nothing could
 * scroll: measured at 950x900, NINE of thirteen panels clipped, Settings by
 * 1,653px. Content silently gone on any window that width.
 *
 * `test/one-screen.mjs` never saw it: its viewports are 1440, 1920 and 1366,
 * all past the band. `width-sweep.mjs` did, intermittently, because its coarse
 * pass only sometimes lands inside a 99px window — which is the exact defect
 * class its own header says it exists to find, and the reason it sweeps rather
 * than spot-checks. It read as a flaky test and was not one.
 *
 * So the fallback is tied to the same breakpoint as the RULE rather than to the
 * columns. A card may shrink and scroll itself in one column; whether there is
 * a second column is a separate question and stays at 1000px.
 */
/*
 * 2026-09-08: A CARD WITHOUT A LIST NO LONGER SCROLLS ITSELF.
 *
 * This block used to give `.panel > .card:not(:has(> .pane))` its own
 * `overflow-y: auto`, and its own comment called that "a worse experience and
 * it is meant to be", a fallback waiting on phases that were never done. It was
 * only ever needed because the panel could not be taller than the screen. It
 * can now, so the fallback is deleted rather than tuned: content that does not
 * fit makes the PAGE longer, which is the behaviour a person can see and scroll.
 *
 * A card holding a LIST keeps its inner scroll, and that one is right rather
 * than tolerated: `> .pane` is a list with a fixed head and footer above and
 * below it, so scrolling the list keeps the filters and the "load more" in
 * place. That is what every table in every CRM does. The distinction the old
 * code drew was correct; only the fallback half was wrong.
 */
@media (min-width: 900.02px) {
  .panel > .card:has(> .pane) {
    overflow-y: auto;
    overscroll-behavior: contain;
    min-height: 0;
  }
}

/*
 * TWO COLUMNS, AND THE ROWS SIZE TO WHAT IS IN THEM (2026-09-08).
 *
 * `grid-auto-rows: minmax(0, 1fr)` made every row exactly as tall as the
 * tallest card in it AND bound the lot to one screen, so a short card carried
 * dead space and a tall one hid its own content. On Settings that meant six
 * identical 344px boxes, three of them concealing 404px between them.
 *
 * `auto` rows and `start` alignment instead: a card is as tall as its content
 * and no taller, the page grows, and the page scrolls. `align-items: start`
 * matters as much as the rows do, because without it a grid item still
 * stretches to its row and the short cards get their dead space back.
 *
 * The `min-height: 0` overrides are gone with the constraint that needed them.
 * They existed to let cards shrink below their content inside a fixed cell;
 * there is no fixed cell now, and keeping them would only re-enable the
 * shrinking this change exists to stop.
 *
 * The height condition is gone too. A short screen no longer needs a different
 * layout, because a short screen just means more scrolling, which is what the
 * small-screen block below already relied on.
 */
@media (min-width: 1000px) {
  .panel {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-auto-rows: auto;
    align-content: start;
    align-items: start;
  }
  .panel > * { min-width: 0; }

  /*
   * A card holding a list is the one thing that still wants a bound, and it
   * wants a generous one rather than a cell: tall enough to be worth scrolling,
   * short enough that the card below it is still reachable without paging
   * through a thousand rows. The list scrolls; the head and footer stay put.
   */
  .panel > .card:has(> .pane) {
    max-height: min(78vh, 900px);
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  /**
   * A PAGE WITH ONE BOX GIVES THAT BOX THE WIDTH (2026-09-09).
   *
   * Two columns are right for a page with something to put in both. On a page
   * with one, track 2 was simply empty: measured at 1440x900, Inbox, Leads and
   * Boxes each rendered a 547px card in a 1113px column and left 566px of
   * nothing beside it, under a page head that spanned the full width. The
   * title stretched across and the content stopped halfway, which is what
   * "it isn't aligned and there's a lot of white space" was describing.
   *
   * `:only-child` DOES NOT WORK HERE, and that is the whole reason this
   * selector looks the way it does. The Inbox has two boxes in the markup and
   * swaps between them: app.js hides `#msg-listcard` to show `#msg-threadcard`
   * and back. A hidden element is still a child, so `:only-child` matches
   * neither of them, and the rule would silently do nothing on the one page it
   * was written for. Boxes has the same shape with `#storage-open`.
   *
   * So count what is VISIBLE. `> *:not([hidden]) ~ *:not([hidden])` matches a
   * shown box that has a shown box before it, which can only exist when there
   * are two or more; a panel containing no such thing has at most one.
   * `:has()` is already load-bearing in this file (`.card:has(> .pane)`), so
   * this adds no new support floor.
   *
   * It reads the `hidden` ATTRIBUTE, which is how this codebase toggles a box.
   * One hidden by a CSS `display: none` would slip past it. test/space-probe.mjs
   * measures the rendered result rather than trusting the selector, which is
   * what would catch that.
   */
  .panel:not(:has(> *:not([hidden]) ~ *:not([hidden]))) > * {
    grid-column: 1 / -1;
  }
  /*
   * And it fills the page, not only the width (2026-09-09, second pass).
   *
   * Leads is one card in a 774px panel and was rendering at 684px with its list
   * showing 112px of 434 inside it: a page with ONE thing on it, showing a
   * quarter of that thing, above 90px of nothing. Giving the row the panel's
   * height hands the difference to the list, which is the only child that
   * wants it.
   */
  .panel:not(:has(> *:not([hidden]) ~ *:not([hidden]))) {
    align-content: stretch;
    /* `align-items` too, and it is the half that was missing first time: the
       ROW was already 774px, but `.panel`'s own `align-items: start` left the
       card sitting at 684px inside it. */
    align-items: stretch;
    grid-auto-rows: minmax(0, 1fr);
  }
  /*
   * And the 78vh cap comes off. It exists so a long list cannot push the card
   * BELOW it out of reach, which is a real problem on a page with several
   * boxes and no problem at all on a page with one. On Leads it was the last
   * 72px standing between a 702px card and the 774px page it is alone on.
   */
  .panel:not(:has(> *:not([hidden]) ~ *:not([hidden]))) > .card:has(> .pane) {
    max-height: none;
  }

  /**
   * THE WORK SHAPE: a list, and the thing you are working on (2026-09-09).
   *
   * `data-shape="work"` and not a modifier class, because test/markup.mjs:65
   * and test/a11y.mjs:213 match `class="panel"` EXACTLY and a second class
   * would make the element stop being a panel to both of them.
   *
   * Two tracks that mean something rather than two tracks that happen to be
   * there. The list keeps a real minimum so it never becomes a column of
   * wrapped names; the work column is capped because a conversation reads
   * badly across a very wide measure. Same reasoning and the same numbers as
   * `.discover-panes` (console.css:605), which is the pattern this generalises
   * rather than a new one.
   *
   * The rows are `1fr` and NOT `auto`, and alignment is `stretch` and not
   * `start`: on a work page the two panes are meant to be the same height and
   * to reach the bottom of the screen together. This is the one shape where a
   * box filling its cell is right, because both boxes hold a scrolling list.
   */
  .panel[data-shape="work"] {
    grid-template-columns: minmax(22rem, 1fr) minmax(0, 1.15fr);
    grid-auto-rows: minmax(0, 1fr);
    align-items: stretch;
    /*
     * This overrides `.panel`'s `align-content: start`, so the row fills the
     * frame and both panes reach the bottom of the window. Without it the Inbox
     * stopped 448px short of the bottom on a 900px screen, which is the same
     * "a lot of white space" complaint turned on its side.
     *
     * It is only safe because the panel now has a real height to fill, and it
     * is the exact move that went wrong on 2026-09-08: a stretched card
     * answered its spare height by centring its heading and inflating one
     * control into a 350px slab. The answer then and now is that a stretched
     * card must still start its content at the top, which is the rule below.
     */
    align-content: stretch;
  }
  /* A filled card, with its content still at the top. */
  .panel[data-shape="work"] > .card { align-content: start; }
  /* The lone-box rule above must not apply here. A work page with its detail
     pane empty still wants two columns; collapsing to one the moment nothing
     is selected would make the page jump every time you clear a selection. */
  .panel[data-shape="work"] > * { grid-column: auto; }

  /**
   * THE READ SHAPE: a set of boxes, sized to what is in them (2026-09-09).
   *
   * Two tracks is a good default and a bad rule. Integrations holds four boxes
   * of 61, 255, 192 and 574px: in two tracks its rows resolve to 255 and 574
   * and the page is 139px over, while in three they resolve to 574 alone and
   * it fits with room to spare. Nothing about the CONTENT changed, only how
   * many boxes stand beside each other.
   *
   * So the track count is a per-page number rather than a constant, which is
   * the whole of "anything that's larger should be accountable for in size".
   * The default stays 2, so a page that says nothing behaves exactly as before.
   */
  .panel[data-shape="read"] {
    grid-template-columns: repeat(var(--tracks, 2), minmax(0, 1fr));
    /*
     * Boxes in a row are the same height as each other. Without it Home showed
     * a 220px card beside two 480px ones and the short one clipped its own
     * list at the 7rem floor while its neighbours had room to spare, which
     * reads as three things that were laid out separately.
     *
     * `align-content: start` on the card is what makes this safe rather than
     * a repeat of 2026-09-08: the box fills its row, its CONTENT stays at the
     * top, and a pane inside it takes the difference. A box that stops halfway
     * up its own outline looks broken; a box that centres its heading in the
     * spare space looks broken differently.
     */
    align-items: stretch;
  }
  .panel[data-shape="read"] > .card { align-content: start; }

  /**
   * A box that wants the whole width.
   *
   * Two kinds qualify and no others. A STAT ROW, because four figures squeezed
   * into half a column wrap into a block and stop being a row. And a box that
   * already contains its own responsive grid — `.figure-row` and `.home-grid`
   * are both `auto-fit minmax(...)` — because giving it the width lets its own
   * grid do the splitting, and it splits better than an outer track can: the
   * Analytics Outreach card is 976px tall in a 547px column and its figure row
   * gets one column there, against three at full width.
   *
   * Marked in the markup rather than inferred, so it is visible where the box
   * is written and cannot fire on a box that merely looks similar.
   */
  .panel > .stats,
  .panel > [data-span="full"] { grid-column: 1 / -1; }

  /**
   * The per-page track counts. Three numbers, each with a measured reason.
   *
   * Integrations: four boxes of 61, 255, 192 and 574px. Two tracks resolve to
   * rows of 255 and 574 and the page is 139px over; three put the three real
   * boxes side by side, so the page is the height of the tallest one.
   */
  #panel-integrations {
    --tracks: 3;
    /*
     * And it opts OUT of the equal-height row above. That rule is right where
     * boxes are comparable, which is Home: three cards within 260px of each
     * other, and evening them up removed a 220px-beside-480px raggedness.
     * Here the Calendar card is 748px against 255 and 275, because it holds a
     * whole booking form, so stretching turned its two neighbours into tall
     * mostly-empty boxes. That is the white space this change exists to remove,
     * reintroduced by the rule meant to remove it.
     */
    align-items: start;
  }

  /*
   * Home: four boxes of 167, 221, 439 and 257px. In two tracks the stat row
   * takes a row of its own and the three cards need two more, so the page was
   * 249px over at 1366x768. In three the cards stand side by side and the page
   * is the stat row plus the tallest card. Home is the page the owner named,
   * so it is the one that must fit rather than merely improve.
   */
  /*
   * Analytics is a board like Home, not an index: it is two dashboards and its
   * growth chart measures its own container, so a section that starts closed
   * draws that chart at zero width. Its row fills the page for the same reason
   * Home's does.
   */
  #panel-analytics { grid-template-rows: minmax(0, 1fr); align-content: stretch; }

  #panel-home {
    --tracks: 3;
    /*
     * Home keeps its board rather than gaining an index: it is a summary of
     * several things at once, and an index would defeat the one job it has.
     * What it needed was the ROW filling the page. Its three cards were 480px
     * in a 774px panel, so their lists sat at the 7rem floor - "Today's plan"
     * showed 112px of 353 - above 200px of nothing. Row 1 is the stat strip,
     * row 2 is the cards and takes what is left, which is about 200px more each.
     */
    grid-template-rows: auto minmax(0, 1fr);
    align-content: stretch;
  }
  /*
   * THE FOURTH CARD (2026-09-10). The two rows above are sized for exactly
   * one row of cards, and Home has three tracks. "Right now" (activity-card,
   * shown whenever /api/activity answers) made three cards with Today's plan
   * and Outreach; "About you" (about-card, shown to anyone with no account
   * attached yet, and to anyone who pressed Not now) made four. The fourth
   * wrapped into an IMPLICIT auto row that took Outreach's full height, the
   * explicit 1fr row was left 0px, and the three cards in it stretched to a
   * zero track: Today's plan showed only its heading and About you's text
   * spilled out under its own outline. Measured at 1440x900 and 1280x720:
   * rows 74 / 0 / 756. This is the first thing a new install sees.
   *
   * So About you is not a fourth box in the board. It is a strip across the
   * top of it, in a row of its own that exists only while the card is shown:
   * the heading and its button on the left, the one sentence beside them.
   * The board keeps its three cards and the row that fills the page.
   */
  #panel-home:has(> #about-card:not([hidden])) { grid-template-rows: auto auto minmax(0, 1fr); }
  #panel-home > #about-card {
    grid-row: 2; grid-column: 1 / -1;
    grid-template-columns: max-content minmax(0, 1fr);
    align-items: center; column-gap: clamp(16px, 2.4vw, 32px);
  }

  /**
   * THE SETUP SHAPE: an index, and one section at a time (2026-09-09).
   *
   * The index on the left, the chosen section on the right. The same two-pane
   * grammar as `data-shape="work"` with different content: there the left
   * column lists people, here it lists sections.
   *
   * Settings is the page that forced it. Six forms, 1,556px, against 794px of
   * room, and no arrangement of six boxes gets under that: three tracks still
   * leaves rows of 606 and 481. The ways left are to hide part of a form, which
   * is what 2026-09-08 did and had to be undone, or to let somebody choose
   * which form they are looking at. This is the second one.
   */
  .panel[data-shape="setup"] {
    grid-template-columns: minmax(0, 13rem) minmax(0, 1fr);
    /*
     * THE SECTION FILLS THE PAGE (2026-09-09, second pass).
     *
     * The shape started life on Settings, whose sections are short forms, so it
     * left them at their natural height. It is now the shape for seven more
     * pages whose sections are LISTS, and a list in a card that stops at its
     * content height gets no free space, so `.pane`'s `flex: 1 1 0` resolves to
     * its 7rem floor. That is the letterbox, and it is the whole reason those
     * pages read as cut off: Discover's Outreach showed 308px of a 672px list.
     *
     * A section that fills the page has ~650px for its list instead of 112px.
     * `align-content: start` on the card keeps its content at the top, which is
     * the rule that stops a filled box centring its heading in the spare space.
     */
    align-items: stretch;
    align-content: stretch;
    /*
     * Two rows, named rather than flowed. Row 1 is for a page's lead line, the
     * sentence that belongs to the whole page rather than to any one section;
     * Integrations has one and Settings does not, and on a panel without one
     * the row is simply 0px. Row 2 is the index and the section.
     *
     * Explicit, because auto-flow puts a stray paragraph in the first cell and
     * then the index starts halfway down the page.
     */
    grid-template-rows: auto minmax(0, 1fr);
  }
  .panel[data-shape="setup"] > * { grid-column: 2; grid-row: 2; }
  .panel[data-shape="setup"] > p { grid-column: 1 / -1; grid-row: 1; align-self: start; }
  .panel[data-shape="setup"] > .secindex { grid-column: 1; grid-row: 2; }
  .panel[data-shape="setup"] > .card { align-content: start; }
  /*
   * Settings opts OUT, by name and deliberately. It is the page the owner
   * approved as it stands ("perfect, exactly what I envisioned"), and what he
   * approved is short forms sitting at their natural height. Stretching them
   * would change the one page nobody asked to change.
   */
  #panel-settings { align-items: start; align-content: start; grid-auto-rows: auto; }
}

/**
 * The section index itself. A quiet list of names rather than a set of boxes:
 * it is navigation INSIDE a page, so it has to read as lighter than the thing
 * it navigates. The current item is marked with a left edge, the same marker
 * the rail and the selected list row use; a filled item on the light ground
 * would read as a dark object, which the one-ground rule forbids.
 */
/* The index's word for "not the section you are looking at". Deliberately NOT
   the `hidden` attribute: that one belongs to the app and means "there is
   nothing in here", and the two must not be able to overwrite each other. */
.panel[data-shape="setup"] > .is-off { display: none; }

/*
 * 8px, NOT 2px (2026-09-09). The owner: "some of the tabs are still merging
 * with each other or very closely touching, make sure it's spread out."
 *
 * 2px is a fine gap between two lines of TEXT and the wrong one here, because
 * every item in this list paints its own background on hover and another when
 * it is the current section. Two painted blocks two pixels apart do not read
 * as two items; they read as one smudged block with a seam in it. Measured by
 * test/discover-ui.mjs at 1600, 1440 and 1280: three pairs, 2px each.
 *
 * The cost is bounded and was checked rather than assumed. The longest index
 * in the console has six items (Requests and Settings), so this adds 5 x 6px =
 * 30px to the tallest case, in a 13rem column that is nowhere near full. All
 * seven indexes get it, which is the point: the complaint is about the
 * component, not about Discover.
 */
.secindex { display: grid; gap: 10px; align-content: start; min-width: 0; }
.secindex__item {
  font: inherit; font-size: 14px; font-weight: 500; text-align: left;
  background: none; border: none; cursor: pointer;
  color: var(--muted);
  min-height: var(--tap-sm);
  padding: 8px 12px;
  border-radius: 8px;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.secindex__item:hover { color: var(--text); background: rgba(255, 255, 255, .04); }
.secindex__item[aria-current="true"] {
  color: var(--text); font-weight: 600;
  background: rgba(255, 255, 255, .06);
  box-shadow: inset 3px 0 0 0 var(--accent, #3446a8);
}
.content .secindex__item { color: var(--lx-ink-2); }
.content .secindex__item:hover { color: var(--lx-ink); background: rgba(10, 16, 36, .05); }
.content .secindex__item[aria-current="true"] {
  color: var(--lx-ink); background: rgba(10, 16, 36, .05);
  box-shadow: inset 3px 0 0 0 var(--lx-accent);
}

/* Below the two-column breakpoint there is no index and nothing is put away:
   every section shows, stacked, exactly as it did before. A narrow window is
   already a scrolling window, so splitting it buys nothing. */
@media (max-width: 999.98px) {
  .secindex { display: none; }
  .panel[data-shape="setup"] > .is-section.is-off { display: block; }
}

/**
 * The diary list bounds itself (2026-09-09).
 *
 * `#cal-body` measured 633px of the Integrations Calendar card's 748px, and it
 * is the only thing on that page whose height depends on how much is IN an
 * account rather than on what the page is. It cannot use `.pane`, because that
 * mechanism needs the list to be a direct child of the card and this one is two
 * levels down; so it bounds itself the way `.rail` and `.ac-log` already do.
 *
 * 18rem is about six meetings, which is enough to see the shape of a week.
 */
#cal-list {
  max-height: 18rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-gutter: stable;
  scrollbar-width: thin;
}

/**
 * A control row that grows. Not every growing thing is a list: the Leads tag
 * filter is a row of chips, one per tag the account has ever used, and it went
 * from 32px to 242px across the same measurement that found the lists. Left
 * alone it pushed the card it shares with `#saved-list` past the screen, which
 * is why Saved leads was the last panel still over at 1440x900.
 *
 * It is a pane, but not an equal one. `flex: 0 1 auto` keeps it out of the
 * share-what-is-left arrangement, because a filter row should never take half
 * a card away from the list it filters, and the cap lets it show about three
 * rows of chips before it starts scrolling on its own.
 */
.pane--tight {
  flex: 0 1 auto;
  min-height: 0;
  max-height: 6.5rem;
}

/**
 * The Leads note is capped for the same reason the filter row above it is
 * (2026-09-09). It is explanatory prose about what retrieving contact details
 * does, and it wraps to 156px of the card on a 1366x768 screen, which took the
 * saved list down to its 112px floor with 434px inside it. A note explaining a
 * list must not be bigger than the list.
 *
 * It scrolls rather than truncating, so nothing it says is lost.
 */
.note--enrich {
  max-height: 5.5rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

/**
 * The Leads list gets a higher floor than the generic 7rem (2026-09-09).
 *
 * Its card carries more around the list than any other: a heading, an
 * explanatory note, an import row, a tag filter row, an action row and a
 * composition summary. On a 1366x768 screen with a busy account that is about
 * 430px of the card, and the list was left at exactly its 7rem floor with
 * 10,707px inside it.
 *
 * 12rem rather than 7. The card already scrolls itself, so the summary line
 * below simply moves into that scroll instead of squeezing the thing the page
 * is named after. A list of leads that shows two rows is not a list of leads.
 */
#saved-list { min-height: 12rem; }

/* Panels fade up rather than sliding sideways: the nav is above them now,
   so a horizontal slide would point at nothing. */
.panel:not([hidden]) { animation: panel-in .3s var(--ease) both; }
@keyframes panel-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

/* List rows deal in, briefly, so a 40-row refresh reads as filling rather
   than snapping. Capped at 12 so a long list is never slow to become useful. */
.person { animation: row-in .3s var(--ease) both; animation-delay: calc(var(--i, 0) * 22ms); }
@keyframes row-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

/* Cards settle in on first paint. */
.card { animation: card-in .42s var(--ease) both; }
@keyframes card-in {
  from { opacity: 0; transform: translateY(10px) scale(.995); }
  to   { opacity: 1; transform: none; }
}

/* A sweep crosses a primary button on hover — the glass catching the
   pointer. The button is white now, so the sheen is a soft indigo shadow
   rather than light. Contained, so it cannot bleed past the pill. */
.btn--primary { position: relative; overflow: hidden; }
.btn--primary::after {
  content: "";
  position: absolute; inset: 0;
  transform: translateX(-120%);
  background: linear-gradient(100deg, transparent 20%, rgba(10,16,36,.14) 50%, transparent 80%);
  transition: transform .6s var(--ease);
  pointer-events: none;
}
.btn--primary:hover::after { transform: translateX(120%); }

/* Stat numbers tick up rather than appearing, so a change is noticed. */
.stat__n { transition: color .2s var(--ease); }
.stat__n.counting { color: var(--green); }

/* The drawer's contents follow the panel in, a beat later. */
#pane-profile > *, #pane-compose > * { animation: row-in .34s var(--ease) both; }
#pane-profile > *:nth-child(2), #pane-compose > *:nth-child(2) { animation-delay: .04s; }
#pane-profile > *:nth-child(3), #pane-compose > *:nth-child(3) { animation-delay: .08s; }

/* Chips and tabs acknowledge a press. */
.chip:active, .tab:active { transform: scale(.97); }

/* =========================================================================
   Responsive

   Two steps. At 900px the bar stops being three columns and stacks into
   two rows — brand + account, then the nav on its own centred row. At
   560px the nav scrolls horizontally rather than wrapping into a block
   that pushes the content off the first screen.
   ========================================================================= */

/* Below 900px the rail becomes a scrolling row across the top: labels stay
   visible (no hamburger, per NN/g and this console's own rejection of
   hover-only disclosure), group titles go, and the content takes the full
   width. The 420px probe measures no horizontal scroll against this form. */
/**
 * THE FLOOR — where one-screen stops applying, and why it has to.
 *
 * Below 720px of viewport height there is no packing that fits. That number is
 * measured, not chosen: `test/one-screen.mjs` sweeps the panels, and at 700px
 * six of them still run over even with every list bounded and the wizard
 * stepped. Holding the rule there would not make pages fit, it would clip them,
 * which is strictly worse than the scrolling it replaced.
 *
 * The same applies under 900px wide, where the rail is already a horizontal row
 * above the column and the column has a phone's width to work with.
 *
 * So the shell hands scrolling back to the document, exactly as it was. The
 * probe asserts this branch rather than assuming it: it sweeps two short
 * viewports and requires that nothing is clipped there either.
 */
/*
 * 2026-09-08: MOST OF THIS BLOCK IS NOW THE DEFAULT, so it has been deleted
 * rather than left to shadow rules that already agree with it.
 *
 * It used to hand scrolling back to the document below 720px tall or 900px
 * wide, because one-screen could not fit there. One-screen no longer applies
 * anywhere, so `.shell`, `.maincol` and `.content` need no small-screen form:
 * the rules further up are already what this block was overriding them to.
 *
 * What survives is the part that was never about the shell. A pane bounds its
 * own list so the head and footer stay put, and on a narrow screen that bound
 * is unwanted: there is no second column to protect, the page scrolls anyway,
 * and a list inside a scroller inside a page is two scrollbars where one will
 * do.
 */
@media (max-height: 719.98px), (max-width: 900px) {
  .pane__body { max-height: none; overflow: visible; }
  .panel > .card:has(> .pane) { max-height: none; overflow: visible; }
}

@media (max-width: 900px) {
  .shell, .shell.side-collapsed { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto 1fr; }
  .side {
    position: sticky; top: 0; height: auto;
    grid-template-rows: none; grid-template-columns: auto auto minmax(0, 1fr) auto;
    align-items: center; gap: 8px;
    padding: 8px 10px 8px max(10px, env(safe-area-inset-left));
    border-right: 0; border-bottom: 1px solid var(--glass-edge);
    overflow: visible;
  }
  .side__head { padding: 0; }
  .side__collapse, .rail__group, .rail__rule,
  .side__acct .whoami__body, .side__acct .whoami__chev { display: none; }
  /* The account block is the row's last column (head, search, nav, then
     it): the avatar, and a menu
     that drops DOWN from it, since there is nothing above the row. */
  .side__acct { border-top: 0; padding-top: 0; }
  .side__acct .whoami { width: auto; padding: 2px; }
  .side__acct .acctmenu__panel { top: calc(100% + 8px); bottom: auto; right: 0; left: auto; }
  .side__acct .acctmenu__panel:not([hidden]) { transform-origin: top right; animation-name: acctmenu-in; }
  .logo__word { display: none; }
  .rail {
    display: flex; gap: 4px; align-items: center;
    overflow-x: auto; scrollbar-width: none;
    mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent);
  }
  .rail::-webkit-scrollbar { display: none; }
  /* flex: none, or every item shrinks and the labels collapse to nothing;
     the row scrolls instead, which is what the mask is for. */
  .rail__item { flex: none; padding: 6px 10px; min-height: var(--tap-sm); font-size: 13px; }
  .rail__label { overflow: visible; }
  .rail__item--on::before { display: none; }
  .side .side__search { width: auto; margin: 0; padding: 6px 10px; }
  .search__label, .search__kbd { display: none; }
  .search { padding: 6px 10px; }
  /* max() again, for the same reason as the base rule: this shorthand would
     otherwise wipe out the home-indicator clearance below 900px, which is
     every phone. */
  .content { padding-block: 22px max(48px, env(safe-area-inset-bottom)); }
}

@media (max-width: 560px) {
  .whoami__body { display: none; }          /* the avatar and dot still say who and what state */
  .tab { padding: 6px 12px; }
  .hero__actions .btn { flex: 1 1 auto; }
  .drawer { width: 100vw; }

  /**
   * Give the name its own line. At 420px the avatar plus two action buttons
   * left about 90px for the person, so every name truncated to "Ada Lovel…"
   * — the row was showing everything except the thing it is a row about.
   * The actions wrap to a second line and split it between them.
   */
  .person { flex-wrap: wrap; row-gap: 10px; padding: 14px; }
  .person__body { flex: 1 1 10rem; }
  .person > .btn { flex: 1 1 0; min-width: 7rem; }
}

/* =========================================================================
   Forced colours (Windows High Contrast)

   Every custom colour is discarded by the OS here, including box-shadow.
   Anything whose meaning was carried by a shadow, a tint or a border
   opacity has to restate it with a system colour or it disappears.
   ========================================================================= */

@media (forced-colors: active) {
  .card, .note, .stat, .browser, .queued, .people, .side, .drawer {
    border: 1px solid CanvasText;
  }
  .tab--on { border: 2px solid Highlight; }
  .rail__item--on { border: 2px solid Highlight; }
  .dot { forced-color-adjust: none; }
  :where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
    outline: 3px solid Highlight;
    outline-offset: 2px;
  }
  .person__open:focus-visible::after { outline: 3px solid Highlight; }
  .btn--primary { border: 2px solid CanvasText; }
  .pill { border: 1px solid CanvasText; }
}

/* More contrast, if the OS asks for it: drop the glass tints for solids. */
@media (prefers-contrast: more) {
  :root {
    --muted: #c4d0ec;
    --faint: #aebadf;
    --control-border: #8b9ac0;
    --glass-edge: rgba(255, 255, 255, .28);
  }
  .card, .side, .drawer, .acctmenu__panel { backdrop-filter: none; -webkit-backdrop-filter: none; background: var(--surface); }
  body::before { display: none; }
}

/**
 * Base44's own guidance, honoured completely: motion supports clarity, and
 * anyone who has asked for less of it gets none of it — including the
 * aurora, which is the thing most likely to bother someone sensitive to
 * movement.
 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  body::before { animation: none; }
  .btn--primary::after { display: none; }
  .btn:hover:not(:disabled) { transform: none; }
  .panel:not([hidden]), .person, .card,
  #pane-profile > *, #pane-compose > * { animation: none !important; }
}

/* The email campaigns rules (#camp-preview, #camp-review) were removed with
 * the Email tab on 2026-09-07. The LinkedIn campaign preview and picker carry
 * their own, further up, under the mc- ids. */


/* ------------------------------------------------------ the account menu */
/*
 * The corner name is now a menu trigger. It keeps the .whoami look exactly,
 * because it is the same thing in the same place: restyling it as a new
 * control would cost the recognition it already has.
 */
.acctmenu { position: relative; }

button.whoami {
  background: none; border: 1px solid transparent; cursor: pointer;
  font: inherit; color: inherit; text-align: left;
}
button.whoami:hover,
button.whoami[aria-expanded='true'] {
  background: var(--surface-2); border-color: var(--control-border);
}
.whoami__chev { flex: none; opacity: .55; transition: transform .16s var(--ease); }
button.whoami[aria-expanded='true'] .whoami__chev { transform: rotate(180deg); }

/*
 * The panel floats over CONTENT, which is why it cannot be dressed like a card.
 *
 * It was, and it was 92% see-through. Two mistakes compounding:
 *
 *   1. --glass-strong is rgba(130,150,225,.08). It is a TINT, meant to sit on
 *      top of an opaque surface the way .card does over the page background.
 *      Used as an entire background it is 8% of a colour and nothing else.
 *   2. backdrop-filter: blur(var(--blur)) was invalid and silently dropped.
 *      --blur is already a filter LIST (blur(22px) saturate(150%)), so that
 *      expanded to blur(blur(22px) saturate(150%)). So there was not even a
 *      blur behind the 8%: it was plain transparency over live text.
 *
 * A card can be 8% because what is behind it is the page background, which is
 * quiet by design. A dropdown is over paragraphs, tables and other people's
 * names, and text reading through text is the one thing a menu must not do.
 *
 * So the base is near-opaque and the blur does the depth work rather than the
 * transparency. The 6% that is left is enough to feel like glass and far too
 * little to read anything through.
 */
.acctmenu__panel {
  position: absolute; top: calc(100% + 10px); right: 0; z-index: 60;
  width: min(340px, calc(100vw - 24px));
  padding: 6px; border-radius: 16px;
  /* Tint over a near-opaque base, not instead of one. rgba of --surface-2. */
  background:
    linear-gradient(160deg, var(--glass-strong), var(--glass)),
    rgba(21, 32, 60, .94);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--glass-edge);
  box-shadow: inset 0 1px 0 var(--glass-spec), var(--lift-glass);
}
.acctmenu__panel[hidden] { display: none; }

/* Without backdrop-filter the remaining 6% would be a washed-out smear, the
   same reason .card and .drawer carry this. */
@supports not (backdrop-filter: blur(1px)) {
  .acctmenu__panel { background: var(--surface-2); }
}

/*
 * The seat line is the quietest thing in the menu and the most re-read, so it
 * gets its own footing rather than being a paragraph squeezed above the list.
 */
.acctmenu__seats {
  margin: 0; padding: 10px 12px 9px;
  font-size: .74rem; letter-spacing: .01em;
  color: var(--muted);
  border-bottom: 1px solid var(--glass-edge);
}

.acctmenu__list { list-style: none; margin: 0; padding: 4px; }

/*
 * A row is an avatar, a name, and what that account actually is underneath.
 *
 * The two lines are the point. A label like "Client B" says nothing about
 * which LinkedIn identity it is, and the identity alone says nothing about why
 * you named it that. Someone picking between five accounts needs both, and
 * needs to tell at a glance which one is live.
 */
/*
 * A container holding two controls, not one control.
 *
 * The pick fills the row and the bin sits at the end. The hover lives on the
 * container so the whole row lights up as one thing, which is what it looks
 * like, while the two children stay separately clickable and focusable.
 */
.acctmenu__row {
  display: flex; align-items: center; gap: 4px; width: 100%;
  padding: 3px; border: 1px solid transparent; border-radius: 12px;
  transition: background .14s var(--ease), border-color .14s var(--ease);
}
.acctmenu__row:hover,
.acctmenu__row:focus-within {
  background: var(--surface-3);
  border-color: var(--control-border);
}

.acctmenu__pick {
  display: flex; align-items: center; gap: 11px; flex: 1; min-width: 0;
  padding: 6px 7px; border: 0; border-radius: 9px;
  background: none; color: inherit; font: inherit; text-align: left;
  cursor: pointer;
}
/* The active row is a state, so its pick is a div and must not look pressable. */
div.acctmenu__pick { cursor: default; }
.acctmenu__pick:focus-visible { outline: 2px solid var(--green); outline-offset: -2px; }

/*
 * The bin is quiet until wanted.
 *
 * A menu whose every row carries a bright delete icon reads as dangerous at
 * rest, and people click carefully in a place that looks dangerous. It stays
 * faint, and only takes the warning colour on hover or focus, which is the
 * moment the intent exists.
 */
.acctmenu__bin {
  flex: none; display: grid; place-items: center;
  width: 30px; height: 30px; padding: 0;
  border: 1px solid transparent; border-radius: 9px;
  background: none; color: var(--faint); cursor: pointer;
  opacity: .55;
  transition: opacity .14s var(--ease), color .14s var(--ease),
              background .14s var(--ease), border-color .14s var(--ease);
}
.acctmenu__row:hover .acctmenu__bin { opacity: 1; }
.acctmenu__bin:hover,
.acctmenu__bin:focus-visible {
  opacity: 1;
  color: var(--bad);
  background: var(--bad-soft);
  border-color: var(--bad);
}

/* ---------------------------------------------------- the confirmation */

.acctmenu__row--confirm {
  background: var(--bad-soft);
  border-color: var(--bad);
  align-items: stretch;
}
.acctmenu__confirm {
  display: grid; gap: 7px; width: 100%;
  padding: 9px 10px;
}
.acctmenu__confirm-q {
  font-size: .85rem; font-weight: 600; color: var(--text);
}
.acctmenu__confirm-detail {
  font-size: .73rem; line-height: 1.45; color: var(--muted);
}
.acctmenu__confirm-row { display: flex; gap: 7px; }
.acctmenu__confirm-row .btn { flex: 1; }

/*
 * The destructive button carries the warning colour, and Cancel does not.
 * Two identical buttons side by side make the reader do the work of telling
 * them apart, at exactly the moment they are least likely to.
 */
.acctmenu__confirm-yes {
  background: var(--bad); border-color: var(--bad); color: #fff;
}
.acctmenu__confirm-yes:hover:not(:disabled) { filter: brightness(1.08); }
.acctmenu__confirm-yes:disabled { opacity: .7; }
/* The active row is a state, not a hover. It is never clickable, so it must
   not light up under the pointer as though it were. */
.acctmenu__row[aria-current='true'] {
  background: var(--surface-3);
  border-color: var(--glass-edge);
  cursor: default;
}

/*
 * The initial stands in for a photo we do not have.
 *
 * LinkedIn avatars are behind the session, so the menu cannot fetch one for an
 * account that is not currently open. A letter in the product's own colour is
 * honest about that and still gives the eye something to aim at, which a bare
 * list of similar names does not.
 */
.acctmenu__avatar {
  flex: none; width: 32px; height: 32px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: .82rem; font-weight: 600; line-height: 1;
  color: var(--green-ink);
  background: linear-gradient(150deg, var(--green), var(--green-dim));
  border: 1px solid var(--glass-edge);
}
.acctmenu__row[aria-current='true'] .acctmenu__avatar {
  box-shadow: 0 0 0 2px var(--surface-3), 0 0 0 3px var(--green);
}

.acctmenu__who { min-width: 0; flex: 1; }
.acctmenu__label {
  display: block; font-size: .88rem; font-weight: 500; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.acctmenu__ident {
  display: block; margin-top: 1px;
  font-size: .72rem; color: var(--faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* "In use" is a label, not a button, so it reads as a pill rather than an
   action somebody might try to press. */
.acctmenu__tick {
  flex: none; font-size: .64rem; font-weight: 600;
  letter-spacing: .04em; text-transform: uppercase;
  color: var(--green);
  padding: 3px 7px; border-radius: 999px;
  background: color-mix(in srgb, var(--green) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--green) 30%, transparent);
}
@supports not (color: color-mix(in srgb, red 50%, blue)) {
  .acctmenu__tick { background: var(--green-deep); border-color: var(--green-dim); }
}

.acctmenu__item {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 11px 12px; margin: 0;
  border: 0; border-top: 1px solid var(--glass-edge); border-radius: 0;
  background: none; color: var(--text); font: inherit;
  font-size: .84rem; cursor: pointer;
  transition: background .14s var(--ease), color .14s var(--ease);
}
.acctmenu__item:first-of-type { border-radius: 0 0 2px 2px; }
.acctmenu__item:last-child { border-radius: 0 0 12px 12px; }
.acctmenu__item:hover,
.acctmenu__item:focus-visible { background: var(--surface-3); }
.acctmenu__item--quiet { color: var(--muted); }
.acctmenu__item--quiet:hover { color: var(--text); }
.acctmenu__plus {
  display: grid; place-items: center;
  width: 20px; height: 20px; border-radius: 6px;
  font-size: .95rem; line-height: 1;
  background: var(--surface-3); border: 1px solid var(--control-border);
  color: var(--muted);
}
.acctmenu__item:hover .acctmenu__plus { color: var(--text); border-color: var(--green); }

.acctmenu__form {
  display: grid; gap: 7px; padding: 12px;
  border-top: 1px solid var(--glass-edge);
  background: rgba(11, 18, 36, .5);
  border-radius: 0 0 12px 12px;
}
.acctmenu__form[hidden] { display: none; }
.acctmenu__form input[type='text'],
.acctmenu__form input[type='email'],
.acctmenu__form input[type='password'] { width: 100%; font-size: .84rem; }
.acctmenu__formrow { display: flex; gap: 7px; margin-top: 3px; }
.acctmenu__formrow .btn { flex: 1; }

/*
 * Open with a short rise, because a menu that simply appears reads as a
 * rendering glitch at this size. prefers-reduced-motion already zeroes every
 * animation globally, so this needs no guard of its own.
 */
@keyframes acctmenu-in {
  from { opacity: 0; transform: translateY(-6px) scale(.985); }
  to   { opacity: 1; transform: none; }
}
.acctmenu__panel:not([hidden]) {
  animation: acctmenu-in .16s var(--ease);
  transform-origin: top right;
}

/*
 * The box picker: the popup that opens off a Store button on a person's row.
 *
 * Same material and the same behaviours as the account menu above (Escape,
 * click-outside, focus returned), because it is the same kind of object. It is
 * positioned against the document rather than a parent, since the button it
 * hangs off lives inside a list row that scrolls and re-renders.
 *
 * It lists boxes and stores into ONE of them. There is no multi-select here and
 * there must not be: the console's standing line is that no people list offers
 * bulk selection, and one press per person is what keeps that true.
 */
.boxpick {
  position: absolute; z-index: 70;
  width: min(250px, calc(100vw - 24px));
  padding: 4px; border-radius: 14px;
  background:
    linear-gradient(160deg, var(--glass-strong), var(--glass)),
    rgba(21, 32, 60, .94);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--glass-edge);
  box-shadow: inset 0 1px 0 var(--glass-spec), var(--lift-glass);
  animation: acctmenu-in .16s var(--ease);
  transform-origin: top left;
}

@supports not (backdrop-filter: blur(1px)) {
  .boxpick { background: var(--surface-2); }
}

.boxpick__head {
  margin: 0; padding: 9px 11px 8px;
  font-size: .74rem; color: var(--muted);
  border-bottom: 1px solid var(--glass-edge);
}

.boxpick__note {
  margin: 0; padding: 9px 11px;
  font-size: .78rem; color: var(--muted);
}

.boxpick__item {
  display: block; width: 100%; text-align: left;
  padding: 10px 11px; margin: 0;
  border: 0; border-top: 1px solid var(--glass-edge); border-radius: 0;
  background: none; color: var(--text); font: inherit;
  font-size: .82rem; cursor: pointer;
  transition: background .14s var(--ease), color .14s var(--ease);
}
.boxpick__item:first-of-type { border-top: 0; }
.boxpick__item:last-child { border-radius: 0 0 11px 11px; }
.boxpick__item:hover,
.boxpick__item:focus-visible { background: var(--surface-3); }
.boxpick__item:disabled { color: var(--muted); cursor: default; background: none; }


/* =========================================================================
   Input and orientation, not width
   -------------------------------------------------------------------------
   Added 2026-08-21. Two things a width breakpoint structurally cannot see,
   both found by sweeping every viewport rather than sampling a few.
   ========================================================================= */

/**
 * A blunt finger, at any width.
 *
 * The undersized-target count barely moved across the whole 320-1600 range:
 * 30 on the landing page at 320px and 37 at 1600px. So this was never a
 * breakpoint problem — it is a question about the INPUT, and it gets asked
 * with (pointer: coarse) instead. Desktop density is untouched.
 *
 * --tap-sm exists so a console can be dense. On a touch screen there is no
 * such thing as a dense control that also works, so it resolves to --tap.
 */
@media (pointer: coarse) {
  :root { --tap-sm: var(--tap); }
  .tab { min-height: var(--tap); padding-inline: 14px; }
  .btn, .btn--sm, .mini-select, summary { min-height: var(--tap); }
  /* A link laid out as a control gets a control's target. Links sitting
     inline in a sentence are deliberately left alone: WCAG 2.5.8 exempts
     them, and boxing them out would wreck the line they live in. */
  .card__go, .lp-more, .cs-back { min-height: var(--tap); display: inline-flex; align-items: center; }
}

/**
 * A phone on its side.
 *
 * The two-row app bar is right in portrait and catastrophic here: measured at
 * 271px of a 320px-tall SE screen, which is 85% of everything the reader has.
 * No max-width rule can see this, because the width in landscape is 568px and
 * looks roomy. The query has to ask about the HEIGHT.
 *
 * Since 2026-09-02 the rail is already a single scrolling row at these widths
 * (the 900px rule above), so the only thing left to condense is the top bar
 * and the page padding.
 */
@media (orientation: landscape) and (max-height: 30rem) {
  .side { padding-block: 4px; }
  .whoami__body { display: none; }
  .pagehead { padding-block: 0; }
  .content { padding-block: 14px max(28px, env(safe-area-inset-bottom)); }
}

/* ── the day's plan (Home, 2026-09-04): one plain line per lever, no
   controls; the guided window (plan.html) does the asking. Rows not tiles,
   so Home keeps its single stat row. */
.plan-rows { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.plan-row {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 10px 12px;
  border: 1px solid var(--glass-edge); border-radius: var(--r);
  background: rgba(255,255,255,.04);
}
.plan-row__body { flex: 1 1 14rem; min-width: 0; display: grid; gap: 2px; }
.plan-row__title { font-size: 14px; }
.plan-row--done .plan-row__title { text-decoration: line-through; opacity: .7; }
