/* ==========================================================================
   RITE Email Triage — "Morning Edition"
   Editorial, paper-and-ink aesthetic. Display: Fraunces. Body: IBM Plex Sans.
   ========================================================================== */

:root {
  --paper: #f6f1e7;
  --paper-2: #ece8de;
  --paper-3: #e6dcc9;
  /* The two raised stocks the sheet prints on, spelled as tokens so a theme can move them:
     --paper-raised is the near-white a card/editor sits on (--paper a touch whitened and
     warmed), --paper-bright the brighter still of a focused field or hovered tool button.
     Morning Edition keeps the literals every one of these rules has always carried; Pressroom
     Grey re-points all of them below, which is why no surface rule hard-codes white anymore. */
  --paper-raised: #fffdf8;
  --paper-bright: #ffffff;
  /* Shared control/field stock, spelled as tokens so BOTH editions drive them from one set of
     surface rules (the base .btn/.btn-ghost/.segmented/input/.settings-group/.check rules below
     spend these, not a literal). Morning prints white on cream - the same crisp chip Pressroom
     does on grey, so a control or field always reads as its own raised box off the paper behind
     it. Pressroom re-points the same tokens below. */
  --field-bg: #ffffff;        /* inputs, selects, textareas, the date picker, admin fields */
  --segmented-bg: #ffffff;    /* the pill groups: Pending/Sent/Rejected, Today/Tomorrow, Day/Week/Month */
  --btn-neutral-bg: #ffffff;  /* the default .btn and the quiet .btn-ghost, as raised white chips */
  --check-bg: #ffffff;        /* the mark-done circle's rest stock (hover/checked stay moss) */
  --btn-neutral-ink: var(--ink);
  --btn-neutral-border: var(--rule);
  --ink: #1f1d1a;
  --ink-2: #4a453e;
  --ink-3: #857d72;
  --rule: rgba(31, 29, 26, 0.16);
  --vermilion: #b4331e;
  --vermilion-soft: rgba(180, 51, 30, 0.1);
  --vermilion-strong: #9c2b18; /* btn-primary hover - a deepened pull of the same red */
  --amber: #d98e2b;
  --amber-soft: rgba(217, 142, 43, 0.14);
  --amber-ink: #9a5f12; /* readable ink on an --amber-soft wash, spelled once (was a fallback) */
  --slate: #3f5e7a;
  --slate-soft: rgba(63, 94, 122, 0.12);
  --moss: #4f6f4a;
  --moss-soft: rgba(79, 111, 74, 0.14);
  --on-accent: #ffffff; /* text sitting on a filled --vermilion / --moss chip */
  /* Ink-family washes the rules used to hard-code as rgba(31, 29, 26, a): the scrim behind
     modals and the dropdown sheet, the quiet hovers, the stronger of the drawer's
     quiet/surf ledger chips, the idle thumb outline, and the dropdown's drop. Tokens so the
     pressroom theme can darken them with the ink instead of chasing every rule. */
  --scrim: rgba(31, 29, 26, 0.35);
  --hover-ink-soft: rgba(31, 29, 26, 0.12);
  --hover-ink-faint: rgba(31, 29, 26, 0.1);
  --hover-ink-strong: rgba(31, 29, 26, 0.18);
  --outline-ink-strong: rgba(31, 29, 26, 0.34);
  --row-hover: rgba(255, 253, 248, 0.7);
  --card-hover: rgba(255, 253, 248, 0.6);
  --hover-moss-faint: rgba(79, 111, 74, 0.18);
  --hover-moss-soft: rgba(79, 111, 74, 0.24);
  --hover-moss-strong: rgba(79, 111, 74, 0.3);
  --outline-moss-mid: rgba(79, 111, 74, 0.4);
  --outline-moss-strong: rgba(79, 111, 74, 0.55);
  --outline-amber-mid: rgba(217, 142, 43, 0.4);
  --spin-track: rgba(255, 255, 255, 0.35);
  --spin-head: #ffffff;
  /* The two corner glows the body gradient washes over the paper, and the paper's own
     underlay - variables so a theme re-tints the ambience without touching the body rule. */
  --glow-amber: rgba(217, 142, 43, 0.12);
  --glow-slate: rgba(63, 94, 122, 0.12);
  /* The image-loading shimmer's three stops, and the grain sheet's ink (an SVG data-URI
     filter matrix can read var() - the turbulence noise underneath is theme-neutral). */
  --shimmer-a: #f0ece4;
  --shimmer-b: #faf7f1;
  --grain-ink: #000000;
  /* --edge-glow is whichever corner glow sits nearer - warm amber over the cream sheet,
     cool slate over the pressroom grey - so the masthead bar and the drawer cast the
     ambience of the paper they stand on. */
  --edge-glow: var(--glow-amber);

  --font-display: "Fraunces", Georgia, "Times New Roman", serif;
  --font-body: "IBM Plex Sans", "Segoe UI", sans-serif;
  --font-mono: "IBM Plex Mono", Consolas, monospace;

  --radius: 4px;
  --shadow: 0 1px 0 rgba(31, 29, 26, 0.06), 0 18px 40px -24px rgba(31, 29, 26, 0.45);
  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  /* Schedule grid geometry - one place so the month's gutter, its cells and the week's cells
     cannot drift apart, and so the phone breakpoint can resize all three at once. The gutter is
     a real track and the gap beside it is one too, so the seven day columns are never narrowed
     by the week number. */
  --month-gutter: 34px;   /* fits a two-digit week number at the 10px mono size */
  --month-gutter-gap: 6px;
  --month-cell-h: 96px;
  --week-cell-h: 190px;
}

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scrollbar-gutter: stable; }
body {
  margin: 0;
  min-height: 100vh;
  color: var(--ink);
  background:
    radial-gradient(1200px 600px at 10% -10%, var(--glow-amber), transparent 60%),
    radial-gradient(900px 500px at 110% 10%, var(--glow-slate), transparent 55%),
    var(--paper-2);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

/* ==========================================================================
   Pressroom Grey - the second edition (html[data-theme="afterdark"])

   The same newspaper pressed on cooler stock: recycled pressroom grey where Morning
   Edition is cream, graphite where it is warm ink, and Playfair Display for the masthead
   instead of Fraunces. The four accent inks - vermilion, amber, slate, moss - are the
   paper's colour language and do not move; their translucent washes are re-mixed at the
   same opacities over the new paper, because a cream-tinted or graphite-tinted wash reads
   wrong on grey stock.

   Every rule in this sheet draws from the variables above (surface whites, scrim/hover
   washes, the body glows and the grain sheet's ink are all tokens since this theme
   landed), so re-pointing them here restocks the whole interface - cards, chips, pills,
   the drawer, the modal, the schedule grid - without touching a single rule below. The
   block sits after :root and after body, so it wins both their cascades on equal
   specificity and the theme only ever changes what a variable resolves to - any literal
   a rule still hard-codes keeps its value in both editions, by the same law that makes
   the login page (auth.css, morning's literals) immune to this block.

   --font-display carries Fraunces as its second family: the webfont link in app.html
   loads both faces, so the swap is instant, and the fallback is what a Pressroom user
   sees for the first frame if the network is slow - Fraunces then, Playfair a moment
   later, never the cream edition's paper.
   ========================================================================== */
html[data-theme="afterdark"] {
  /* Recycled newsprint: greys with the faintest warm lean, so it still reads as paper. */
  --paper: #E5E5E2;
  --paper-2: #ECECE8;
  --paper-3: #d3d0c9;
  /* Cards read ABOVE the body here, which is the reverse of the stock order the rest of the
     sheet assumes: a --paper-raised surface is meant to sit a step above its page, and on this
     pressroom grey the page (--paper-2, #ECECE8) is already the lightest thing on screen -
     every "raised" surface (the settings cards, the editor, the day chips, the email panel)
     was printing BELOW it, which is the "everything is the same grey" complaint. So the ramp
     flips: the body keeps --paper-2 and the raised stock lifts to a clear bright step (the
     L* gap is +6.4). --paper stays the darker top bar / sheet of menus, so top bar / body /
     card remain three distinct steps, just in the other direction. */
  --paper-raised: #f5f5f3;
  /* Hover/focus stock - one step below the raised card, brighter than the page. */
  --paper-bright: #ededea;
  /* Graphite ink - deepened from the washed-out originals so secondary/muted text reads clearly
     on the cool grey stock (the contrast complaint). */
  --ink: #16181a;
  --ink-2: #3a3d41;
  --ink-3: #5f6368;
  --rule: rgba(22, 24, 26, 0.28);
  --shadow: 0 1px 0 rgba(22, 24, 26, 0.1), 0 20px 46px -24px rgba(22, 24, 26, 0.55);
  /* The accents stay; their washes are re-mixed over grey paper at the same strengths. */
  --vermilion-soft: rgba(178, 49, 29, 0.1);
  --vermilion-strong: #992a19;
  --amber: #cf862a;
  --amber-soft: rgba(207, 134, 42, 0.16);
  --amber-ink: #8a6114;
  --slate: #3f5d78;
  --slate-soft: rgba(63, 93, 120, 0.13);
  --moss: #4e6d49;
  --moss-soft: rgba(78, 109, 73, 0.14);
  /* The ink washes follow the graphite ink; the raised hovers follow the new stocks. */
  --scrim: rgba(24, 26, 28, 0.42);
  --hover-ink-soft: rgba(30, 32, 34, 0.12);
  --hover-ink-faint: rgba(30, 32, 34, 0.1);
  --hover-ink-strong: rgba(30, 32, 34, 0.18);
  --outline-ink-strong: rgba(30, 32, 34, 0.34);
  --row-hover: rgba(255, 255, 255, 0.5);
  --card-hover: rgba(255, 255, 255, 0.4);
  --hover-moss-faint: rgba(78, 109, 73, 0.18);
  --hover-moss-soft: rgba(78, 109, 73, 0.24);
  --hover-moss-strong: rgba(78, 109, 73, 0.3);
  --outline-moss-mid: rgba(78, 109, 73, 0.42);
  --outline-moss-strong: rgba(78, 109, 73, 0.55);
  --outline-amber-mid: rgba(207, 134, 42, 0.42);
  /* The corner glows cool to match the stock; the grain sheet and the spinner keep their
     job, only printing in the new ink. */
  --glow-amber: rgba(207, 134, 42, 0.1);
  --glow-slate: rgba(63, 93, 120, 0.13);
  --shimmer-a: #dbdad6;
  --shimmer-b: #efefec;
  --grain-ink: #14161a;
  --edge-glow: var(--glow-slate);
  /* The chart panel. Pressroom used to print the charts as a dark graphite panel with light text,
     the one dark surface on a near-white page. It now matches the cards: the panel takes the card
     stock (--paper-raised, #f5f5f3) so a chart reads as another lifted card, and the ink/tick flip
     to the dark sheet ink so titles and ticks stay legible on that light fill. The grid drops to a
     faint dark ruling. charts.py reads these four literally so the PNG can't drift from the sheet. */
  --panel-chart: #f5f5f3;
  --panel-chart-ink: #000000;
  --panel-chart-tick: #000000;
  --panel-chart-grid: none;
  /* Buttons print on WHITE stock here. Morning's neutral button is a transparent ghost (it sits
     on cream) and its default .btn is graphite-filled; on the grey Pressroom sheet both nearly
     vanish into the page - the "everything is the same colour" complaint. Here every neutral
     control (the default .btn - e.g. Download Brief - plus the btn-ghost behind Dismiss, the row
     tick, Move and Close) becomes a solid white chip with graphite text and a crisp --rule border,
     so each control reads as its own raised chip off the grey. The chip is pure WHITE, the one
     value that stands a clear margin (10+ luminance points) off EVERY surface it can land on -
     the body (--paper-2), the topbar (--paper), and now the lifted cards/editor (--paper-raised),
     all of which the "lighter than the background" request pushed up close to white. --paper-bright
     and even a near-white like #fbfbfa fail that margin against the lifted card, re-creating the
     "everything is the same colour" complaint this block exists to answer. Done / +Add
     (btn-primary) stay vermilion and Reject / Decline (btn-critical) keep the danger-red wash, so
     a row reads as white chips + two reds. */
  --btn-neutral-bg: #ffffff;
  /* The mark-done circle (.check) is its own house control, not a .btn, so the white-chip rule
     below never reaches it: it rests transparent (base .check) and nearly vanishes into the grey
     stock exactly like the ghost buttons did. It inherits the chip stock here; its hover and the
     is-checked fill stay moss - only the rest state lifts. */
  --check-bg: var(--btn-neutral-bg);
  /* Segmented pills (Drafts Pending/Sent/Rejected, Calendar Today/Tomorrow + Day/Week/Month,
     the Settings switches) rest on --paper-2 - a grey a step off the grey page, so they sink
     into it exactly like the ghost buttons did. On Pressroom they print as white chips with
     graphite labels; the chosen segment keeps the graphite fill and reversed ink (base
     .segmented button.is-active is later and higher specificity, untouched by this). */
  --segmented-bg: var(--btn-neutral-bg);
  /* Bare form fields (search, priority/sort/kind selects, the date picker, admin inputs, the
     timezone combobox) print on the same white as the controls - a field the user types into is
     the single surface they most need to read as an active well, and Pressroom's whole complaint
     was that these sat on the grey and vanished. A hair under pure white would tie the card it
     sits on (the cards lifted too), so the field is white and the card stays --paper-raised just
     beneath it - field, card, page read as three separate steps. */
  --field-bg: #ffffff;
  --btn-neutral-ink: var(--ink);
  --btn-neutral-border: var(--rule);
  --font-display: "Playfair Display", "Fraunces", Georgia, "Times New Roman", serif;
  /* Both editions are light-paper; telling the browser so keeps native scrollbars, form
     controls and autofill painted for a light sheet whatever the OS preference is. */
  color-scheme: light;
}

/* Pressroom buttons print on white. The default .btn (Download Brief) is graphite-filled in the
   base sheet; btn-ghost (Dismiss, the row tick, Move, Close) is transparent there. On the grey
   Pressroom page both sink into the stock, so here they resolve to one crisp white chip - solid
   --btn-neutral-bg (a near-white literal, brighter than every grey stock), graphite ink, a hairline
   --rule - and hover only firms that border. The :not()
   guards are load-bearing: a bare ".btn" clause would tie on specificity with ".btn-primary"/
   ".btn-critical" below it and, sitting later in the sheet, paint every button white - flattening
   the very row this is meant to differentiate. So the semantic reds fall straight through to their
   base rules: Done / +Add stay vermilion (--btn-add), Reject / Decline stay danger-red
   (--btn-reject). btn-ms is deliberately excluded too: it is already a bright chip in both
   editions (below the button family at 0,1,0, and it pays --paper-bright), so letting the
   guard-less clause reach it would silently re-point the Microsoft button's stock. */
html[data-theme="afterdark"] .btn:not(.btn-primary):not(.btn-critical):not(.btn-ms),
html[data-theme="afterdark"] .btn-ghost:not(:hover) {
  background: var(--btn-neutral-bg);
  color: var(--btn-neutral-ink);
  border-color: var(--btn-neutral-border);
}
/* Hover stays Morning's behaviour plus this sheet's: the neutral chip only firms its border,
   and the ghost keeps resting transparent (0,3,0 here vs the base .btn-ghost's 0,1,0), so the
   neutral chip and the quiet ghost stay two different controls rather than one flat look. */
html[data-theme="afterdark"] .btn:not(.btn-primary):not(.btn-critical):not(.btn-ms):hover,
html[data-theme="afterdark"] .btn-ghost:hover { border-color: var(--ink); }
/* The two control groups the base sheet leaves flat on this stock get the chip treatment too:
   the segmented pills and the bare form fields. Both own a source-LATER rule (the base
   .segmented shorthand, the base input/select/textarea rule) that restates the property, so a
   re-pointed token alone would not reach them and this has to be rules of its own. Specificity
   does the rest: each clause below out-weighs the base rule it answers, so the outcome does not
   depend on where in the sheet either one sits. Both keep their borders, so each still reads as
   its own box - chips at --segmented-bg, fields a step under at --field-bg. The chosen segment
   keeps the graphite fill and reversed ink (base .segmented button.is-active, 0,2,1). */
html[data-theme="afterdark"] .segmented { background: var(--segmented-bg); }
html[data-theme="afterdark"] input:not([type="checkbox"]):not([type="radio"]),
html[data-theme="afterdark"] select,
html[data-theme="afterdark"] textarea {
  background: var(--field-bg);
}
/* The history "Show / All events" select restates the stock from its own class rule (0,1,1),
   which out-weighs the bare element rules above - so it needs its own clause. */
html[data-theme="afterdark"] .history-filter select { background: var(--field-bg); }
/* The mark-done circle joins the chip stock at rest (see --check-bg above), and only at rest.
   The catch the browser taught us: "html[data-theme=afterdark] .check" scores 0,2,0 (the
   attribute selector counts as a class), and the base sheet's moss ".check:hover,
   .check.is-checked" also scores 0,2,0 (two classes) - a TIE. A tie is broken by source order,
   and this block sits far earlier than the base .check rules, so on its own the rest lift would
   lose the tie and never paint... except it does not, because it must not win the checked state.
   So the checked/hover state gets stated again HERE, at 0,3,0 (.check.is-checked under the
   theme attribute), strictly above the 0,2,0 rest lift and above the base rule's 0,2,0 - the
   moss fill and reversed tick land no matter where the base rule sits or which way the tie
   falls. Only a circle that is neither done nor hovered prints the chip stock. */
html[data-theme="afterdark"] .check { background: var(--check-bg); }
html[data-theme="afterdark"] .check:hover,
html[data-theme="afterdark"] .check.is-checked {
  background: var(--moss);
  border-color: var(--moss);
  color: var(--on-accent);
}
/* The settings / AI Analysis cards. Their base rule (much later, and equally specific) paints
   --paper - the DARKER base stock the morning sheet gets away with because its cards carry a
   shadow. Pressroom's cards get the same treatment as every other raised surface (the --paper-
   raised flip above), which is the "lighter than the background" ask; the hairline and shadow
   stay, so the card reads as a card on both counts. */
html[data-theme="afterdark"] .settings-group { background: var(--paper-raised); }
/* .day-chip (the calendar rail) spends --paper-raised but owns a source-later rule restating
   the property, so it needs an explicit clause to take the flip. */
html[data-theme="afterdark"] .day-chip { background: var(--paper-raised); }
/* The action-group tiles in Pressroom: match the chart panel (--panel-chart, #f5f5f3 == this
   edition's --paper-raised), the surface the user prefers. The base rule spends --paper (#E5E5E2),
   which reads darker than the charts; here the tiles lift to the same lighter card stock so desk
   cards and charts share one colour. Morning is untouched (its tiles stay on --paper). */
html[data-theme="afterdark"] .action-group { background: var(--paper-raised); }
/* The draft / signature rich-text editor (.rte). Its editable surface (.rte-editable) is
   transparent, so whatever the .rte box paints IS the page you type onto - and the user asked
   for that page to be WHITE, the one surface they most read and write. The base .rte rule
   (source-later, equally specific) spends --paper-raised, the card grey, which reads a touch
   dim; here the editor takes the field white instead, matching every other input. The
   .rte-toolbar band stays --paper-2 (a grey strip over the white body), which is the point. */
html[data-theme="afterdark"] .rte { background: var(--field-bg); }

/* The blanket field lift must not reach the recipient entry, which is chrome rather than a
   field: it sits inside a chip row with a dashed border that only appears when it is focused,
   and a white box there would break the row apart. The clause is deliberately 0,3,1 because the
   lift above ranks 0,3,0 for an input and the cascade is ordered by (origin, importance,
   specificity, order) - a plain ".rcpt-input" guard (0,2,0) loses to it, and so does a tied
   0,3,0 written earlier; only a strictly higher weight wins. The entry's own focus rule is
   0,2,0 and does NOT outrank the lift, so the entry prints --field-bg once focused - one step
   under the --paper-bright Morning shows it, and the slate ring plus dashed border still say
   "active" unmistakably.
   (The legacy .draft-body textarea rule is NOT defended here: app.js now always renders a .rte
   inside .draft-body, so that base rule matches nothing and needs no afterdark guard.) */
html[data-theme="afterdark"] input.rcpt-input[type="text"] { background: transparent; }

/* "Add to queue" (Admin → People) rides at the end of a flex row beside the email input it
   submits. The control's base padding is generous for a -sm button, and with the input set to
   flex: 1 1 auto it would squeeze the label into two lines; a no-wrap button in a shrinkable
   flex row never wraps however narrow it gets (the box just overflows), so the label holds one
   line and the input gives the room up instead. */
.person-add .btn { white-space: nowrap; flex: 0 0 auto; }

html:not([data-theme]), html[data-theme="morning"] { color-scheme: light; }
a { color: inherit; }
button { font: inherit; color: inherit; }
img { max-width: 100%; display: block; }
[hidden] { display: none !important; }

.grain {
  display: none; /* No full-page blend overlay: its multiply/normal compositing renders a
                    dark-green cast on some GPUs/colour pipelines (it never tinted production).
                    The paper reads as flat --paper, exactly like production. */
  position: fixed; inset: 0; pointer-events: none; z-index: 0; opacity: 0.35; mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.22 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  /* The grain sheet's ink rides --grain-ink: the data-URI matrix above multiplies this
     flood into the turbulence's alpha, so a theme printing in graphite (Pressroom's
     #14161a) shifts the paper's noise a hair cooler without a second image. */
  background-color: var(--grain-ink);
}

/* ---------- Typography helpers ---------- */
.eyebrow {
  margin: 0 0 6px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.muted { color: var(--ink-3); }
.link {
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--slate); text-decoration: underline; text-underline-offset: 3px; font-size: 13px;
}
.link:hover { color: var(--ink); }
h1, h2, h3 { font-family: var(--font-display); font-weight: 500; letter-spacing: -0.01em; margin: 0; }
h1 { font-size: clamp(28px, 3.4vw, 42px); line-height: 1.05; }
h2 { font-size: 20px; }
h3 { font-size: 12px; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-3); font-weight: 500; }
code { font-family: var(--font-mono); font-size: 0.9em; background: var(--paper-2); padding: 1px 5px; border-radius: 3px; }

/* ---------- Reveal animation ---------- */
.reveal { animation: rise 0.7s var(--ease) both; }
.reveal-delay { animation-delay: 0.12s; }
@keyframes rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  padding: 11px 18px; border-radius: var(--radius); border: 1px solid var(--btn-neutral-border);
  background: var(--btn-neutral-bg); color: var(--btn-neutral-ink); font-weight: 500; font-size: 14px;
  cursor: pointer; text-decoration: none; position: relative;
  transition: transform 0.2s var(--ease), background 0.2s, color 0.2s, border-color 0.2s;
}
.btn:hover { transform: translateY(-1px); border-color: var(--ink); }
.btn:active { transform: translateY(0); }
.btn:disabled { opacity: 0.55; cursor: progress; transform: none; }
.btn-primary { background: var(--vermilion); border-color: var(--vermilion); color: var(--on-accent); }
.btn-primary:hover { background: var(--vermilion-strong); color: var(--on-accent); }
.btn-ghost { background: var(--btn-neutral-bg); color: var(--btn-neutral-ink); border-color: var(--btn-neutral-border); }
.btn-ghost:hover { border-color: var(--ink); }
/* btn-critical is NOT a solid red button: at rest it is a light danger wash (--vermilion-soft)
   under red ink, which only reads because the fill is pale - white text there would vanish. It
   turns solid vermilion only on hover, and that hover already flips its ink to --paper (white),
   so the white-on-red legibility is already handled. Only btn-primary is a solid fill at rest and
   needs its white ink spelled out here (it would otherwise inherit the graphite .btn colour). */
.btn-critical { background: var(--vermilion-soft); color: var(--vermilion); border-color: transparent; }
.btn-critical:hover { background: var(--vermilion); color: var(--on-accent); }
.btn-ms { background: var(--paper-bright); color: var(--ink); border-color: var(--rule); }
.btn-ms:hover { border-color: var(--ink); }
.btn-sm { padding: 7px 12px; font-size: 13px; }
#setup-prompt .btn, #setup-detail .btn-primary { width: 100%; }
#cancel-setup { margin-top: 22px; }
.btn-spinner {
  width: 14px; height: 14px; border-radius: 50%; display: none;
  border: 2px solid var(--spin-track); border-top-color: var(--spin-head); animation: spin 0.8s linear infinite;
}
.btn.is-busy .btn-spinner { display: inline-block; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- Forms ---------- */
.field { display: grid; gap: 6px; margin-bottom: 14px; }
.field span { font-size: 12px; font-weight: 500; color: var(--ink-2); letter-spacing: 0.02em; }
input, select, textarea {
  font: inherit; color: var(--ink); background: var(--field-bg);
  border: 1px solid var(--rule); border-radius: var(--radius); padding: 10px 12px; width: 100%;
  transition: border-color 0.2s, box-shadow 0.2s;
}
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--slate); box-shadow: 0 0 0 3px var(--slate-soft); }
textarea { min-height: 160px; resize: vertical; line-height: 1.6; }
.inline-form { display: grid; grid-template-columns: 1fr auto auto; gap: 8px; margin: 10px 0 16px; }
.inline-form select { width: auto; }

/* ---------- Alerts / banners / toast ---------- */
.alert { padding: 10px 12px; border-radius: var(--radius); font-size: 13px; margin-bottom: 14px; }
.alert-error { background: var(--vermilion-soft); color: var(--vermilion); border: 1px solid rgba(180, 51, 30, 0.3); }
.banner {
  display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  padding: 14px 18px; border-radius: var(--radius); margin-bottom: 22px; font-size: 14px;
}
.banner-warn { background: var(--amber-soft); border: 1px solid var(--outline-amber-mid); }
/* Centering is layout-based (a full-width fixed strip + centered pill) rather than the classic
   left:50%/translateX(-50%), so the pill shares app-main's centred column and can never sit at a
   different centre than the content when the vertical scrollbar appears or disappears (paired with
   scrollbar-gutter: stable on html). The strip is full width but click-through (pointer-events:
   none); only the pill takes pointer events. The pill keeps the "rise" entrance - its transform
   animation is on the pill, not the strip, so it never fights the horizontal centring. */
.toast {
  position: fixed; left: 0; right: 0; bottom: 28px; z-index: 60;
  display: flex; justify-content: center; pointer-events: none;
}
.toast-inner {
  pointer-events: auto;
  background: var(--ink); color: var(--paper); padding: 11px 16px; border-radius: var(--radius);
  font-size: 14px; box-shadow: var(--shadow);
  animation: rise 0.35s var(--ease);
}
.toast.is-error .toast-inner { background: var(--vermilion); }

/* ---------- Build stamp ---------- */
/* Fixed bottom-right chip: a small card (the same paper/shadow/rule language as every other
   surface), not text painted over the page. Below the drawer/toast layer, and pointer-events
   none so it is context that never steals clicks. The grain overlay sits at z-index 0 with
   multiply blending, so this needs a solid background to read cleanly above it. */
.app-version {
  position: fixed; right: 14px; bottom: 10px; z-index: 30; pointer-events: none;
  display: inline-flex; align-items: baseline; gap: 5px;
  padding: 5px 11px; border: 1px solid var(--rule); border-radius: 999px;
  background: var(--paper-raised); box-shadow: var(--shadow);
  font-size: 11.5px; color: var(--ink-3); white-space: nowrap;
}
.app-version-label {
  font-family: var(--font-mono); font-size: 9.5px; text-transform: uppercase;
  letter-spacing: 0.12em; color: var(--ink-3);
}
.app-version-value {
  font-family: var(--font-mono); font-size: 11px; color: var(--ink);
  font-variant-numeric: tabular-nums; letter-spacing: 0.02em;
}

/* ---------- Login page ---------- */
.login-layout {
  position: relative; z-index: 1;
  min-height: 100vh; display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(360px, 480px);
  gap: 48px; padding: clamp(24px, 5vw, 72px); align-items: center; max-width: 1280px; margin: 0 auto;
}
.masthead-kicker { font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-3); }
.masthead-title {
  font-family: var(--font-display); font-weight: 300; font-size: clamp(48px, 7vw, 96px);
  line-height: 0.95; letter-spacing: -0.03em; margin: 18px 0 22px;
  font-variation-settings: "opsz" 144;
}
.masthead-title em { font-style: italic; font-weight: 600; color: var(--vermilion); }
.masthead-standfirst { max-width: 52ch; font-size: 17px; color: var(--ink-2); margin: 0; }
.hero-features { list-style: none; padding: 0; margin: 40px 0 0; border-top: 1px solid var(--rule); }
.hero-features li { display: flex; gap: 18px; padding: 16px 0; border-bottom: 1px solid var(--rule); align-items: baseline; }
.hero-features .feature-num { font-family: var(--font-mono); color: var(--vermilion); font-size: 12px; min-width: 28px; }
.hero-features strong { display: block; font-family: var(--font-display); font-size: 19px; font-weight: 500; }
.hero-features span:not(.feature-num) { color: var(--ink-3); font-size: 14px; }

.login-card {
  background: var(--paper-raised); border: 1px solid var(--rule); border-radius: 6px; box-shadow: var(--shadow); position: relative;
}
.login-card::before {
  content: ""; position: absolute; inset: 0 auto 0 0; width: 4px; background: var(--vermilion); border-radius: 6px 0 0 6px;
}
.login-card-inner { padding: 36px 36px 28px; }
.card-title { font-size: 30px; margin-bottom: 20px; }
.divider { display: flex; align-items: center; gap: 12px; margin: 22px 0; color: var(--ink-3); font-size: 12px; }
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--rule); }
.btn-ms { width: 100%; }
.form .btn { width: 100%; margin-top: 4px; }
.hint { font-size: 12px; color: var(--ink-3); margin: 10px 0 0; }
.fineprint { margin: 22px 0 0; font-size: 12px; color: var(--ink-3); line-height: 1.5; }

/* ---------- Login page: instance-lock panel ---------- */
/* The lock replaces the sign-in FORM, not the page. index.html carries both panels in
   the right-hand card; the server (app/main.py's index route) adds page-login-locked to
   <body> while the license is locked, and these rules swap one for the other. The left
   hero stays: the login screen keeps its shape, and the lock reads as a state of it
   rather than the old full-screen error page.
   [hidden] is spelled out because several rules here set display on their class (a UA
   [hidden] rule has no specificity and loses to all of them). */
.login-lock { display: none; }
.login-lock .card-title { color: var(--vermilion); }
.login-lock-copy { color: var(--ink-2); line-height: 1.55; margin: 0 0 12px; font-size: 15px; }
.page-login-locked #login-signin, .page-login-locked #login-signin [hidden] { display: none; }
.page-login-locked .login-lock { display: block; }

/* ---------- App shell ---------- */
.page-app .btn-ms { width: auto; }
.topbar {
  position: sticky; top: 0; z-index: 20;
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 24px;
  padding: 14px clamp(16px, 3vw, 40px); background: var(--paper); backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--rule);
}
.topbar-brand { display: flex; align-items: center; gap: 10px; }
/* Envelope mark (app/static/img/logo-mark.svg) + the text lockup, restoring the full
   "RITE Email Triage" wordmark beside the mark. The spans reuse the lockup's typographic
   treatment (see app/static/img/logo.svg); the old pre-lockup block element stays retired. */
.brand-logo { display: block; height: 26px; width: auto; }
.brand-word {
  font-family: var(--font-display); font-size: 22px; line-height: 1;
  letter-spacing: -0.01em; white-space: nowrap;
}
.brand-word .w-rite { font-weight: 700; color: var(--ink); }
.brand-word .w-email { font-weight: 400; color: var(--ink-2); }
.brand-word .w-triage { font-weight: 500; font-style: italic; color: var(--vermilion); }
/* The brand cell shares the top bar grid with centred nav; below 600px the word is dropped
   (consistent with the nav-wrap breakpoint) so the mark alone never collides with the nav. */
@media (max-width: 600px) { .brand-word { display: none; } }
.topbar-nav { display: flex; gap: 4px; justify-content: center; }
.nav-link {
  background: none; border: 0; padding: 8px 14px; border-radius: 999px; cursor: pointer; font-size: 14px;
  color: var(--ink-2); display: inline-flex; align-items: center; gap: 8px; transition: background 0.2s, color 0.2s;
}
.nav-link:hover { background: var(--paper-2); color: var(--ink); }
.nav-link.is-active { background: var(--ink); color: var(--paper); }
.pill { font-family: var(--font-mono); font-size: 11px; padding: 1px 7px; border-radius: 999px; background: var(--paper-3); color: var(--ink); }
.pill-critical { background: var(--vermilion); color: var(--on-accent); }
.topbar-user { display: flex; align-items: center; gap: 12px; font-size: 13px; }
.user-graph { width: 9px; height: 9px; border-radius: 50%; background: var(--ink-3); }
.user-graph.is-connected { background: var(--moss); box-shadow: 0 0 0 3px var(--moss-soft); }

/* ---------- User menu (name -> profile settings / sign out) ---------- */
.user-menu-wrap { position: relative; }
.user-menu-btn {
  display: inline-flex; align-items: center; gap: 6px; background: none; border: none; cursor: pointer;
  font: inherit; color: var(--ink); padding: 4px 6px; border-radius: var(--radius);
}
.user-menu-btn:hover { background: var(--paper-3); }
/* The mailbox dot sits left of the name with a little extra air before it (6px gap + 6px). */
.user-graph { margin-right: 6px; }
.user-menu-caret { font-size: 10px; color: var(--ink-3); }
.user-menu {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 70; min-width: 210px;
  background: var(--paper); border: 1px solid var(--ink); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 6px; display: flex; flex-direction: column; gap: 2px;
}
.user-menu-email {
  padding: 6px 8px 8px; font-size: 12px; color: var(--ink-3); border-bottom: 1px solid var(--rule);
  margin-bottom: 4px; word-break: break-all;
}
.user-menu-item {
  display: block; width: 100%; text-align: left; background: none; border: none; cursor: pointer;
  font: inherit; color: var(--ink); padding: 8px; border-radius: calc(var(--radius) / 2);
}
.user-menu-item:hover { background: var(--paper-3); }

/* ---------- Edition picker (topbar -> choose the theme) ----------
   Lives just left of the name and borrows the user menu's whole vocabulary - ghost trigger,
   bordered paper card, ink-hover rows - so two dropdowns in one corner read as one family. The
   trigger carries the edition's own colour in the swatch, so the button shows what it does before
   it is opened. */
.edition-menu-wrap { position: relative; }
.edition-btn {
  display: inline-flex; align-items: center; gap: 7px; background: none; border: none; cursor: pointer;
  font: inherit; color: var(--ink); padding: 4px 8px; border-radius: var(--radius); font-size: 13px;
}
.edition-btn:hover { background: var(--paper-3); }
.edition-swatch {
  width: 12px; height: 12px; border-radius: 50%; flex: 0 0 auto;
  background: var(--ink); border: 1px solid var(--rule);
  box-shadow: inset 0 0 0 2px var(--paper);
}
.edition-caret { font-size: 10px; color: var(--ink-3); }
.edition-menu {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 70; min-width: 210px;
  background: var(--paper); border: 1px solid var(--ink); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 6px; display: flex; flex-direction: column; gap: 2px;
}
.edition-menu-head {
  padding: 6px 8px 8px; font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-3); border-bottom: 1px solid var(--rule); margin-bottom: 4px;
  font-family: var(--font-mono);
}
.edition-item {
  display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%;
  text-align: left; background: none; border: none; cursor: pointer; font: inherit; color: var(--ink);
  padding: 8px; border-radius: calc(var(--radius) / 2);
}
.edition-item:hover { background: var(--paper-3); }
.edition-item-check { visibility: hidden; color: var(--vermilion); }
.edition-item.is-active { font-weight: 600; }
.edition-item.is-active .edition-item-check { visibility: visible; }

/* ---------- Timezone combobox (searchable, server-backed zone list) ---------- */
.tz-combo { position: relative; }
.tz-combo-list {
  position: absolute; left: 0; right: 0; top: 100%; z-index: 5; max-height: 220px; overflow-y: auto;
  background: var(--paper); border: 1px solid var(--ink); border-radius: var(--radius);
  box-shadow: var(--shadow); margin-top: 4px;
}
.tz-combo-option {
  display: flex; justify-content: space-between; gap: 10px; width: 100%; text-align: left;
  background: none; border: none; cursor: pointer; font: inherit; color: var(--ink); padding: 7px 10px;
}
.tz-combo-option:hover, .tz-combo-option.is-active { background: var(--paper-3); }
.tz-combo-option-tz { font-variant-numeric: tabular-nums; }
.tz-combo-option-label { color: var(--ink-3); font-size: 12px; }
.tz-combo-empty { padding: 8px 10px; font-size: 13px; color: var(--ink-3); }
.tz-combo-hint { font-size: 12px; margin-top: 5px; }
/* A settings hint emptied of its copy (removed explanations leave an empty <p>) should take up no
   space - otherwise its margin-top reads as a stray gap. :empty also lets the timezone note vanish
   when a timezone is already set (app.js clears it), matching the profile table's behaviour. */
.settings-group .tz-combo-hint:empty { display: none; }
.tz-combo-hint.is-error { color: var(--vermilion); }

.app-main { position: relative; z-index: 1; max-width: 1240px; margin: 0 auto; padding: 36px clamp(16px, 3vw, 40px) 80px; }
.view { display: none; }
.view.is-active { display: block; }
.view-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 24px; flex-wrap: wrap; margin-bottom: 28px; padding-bottom: 18px; border-bottom: 2px solid var(--ink); }
.toolbar { display: flex; gap: 10px; align-items: center; }
.toolbar input { width: 260px; }
.toolbar select { width: auto; }
.segmented { display: inline-flex; border: 1px solid var(--rule); border-radius: 999px; padding: 3px; background: var(--segmented-bg); }
.segmented button { border: 0; background: none; padding: 6px 14px; border-radius: 999px; cursor: pointer; font-size: 13px; color: var(--ink-2); }
.segmented button.is-active { background: var(--ink); color: var(--paper); }

/* ---------- Today / brief ---------- */
.brief-header { display: grid; grid-template-columns: 1fr auto; gap: 32px; align-items: end; margin-bottom: 26px; }
.brief-title { font-size: clamp(34px, 4.5vw, 56px); font-weight: 400; letter-spacing: -0.025em; }
.brief-title span { font-style: italic; font-weight: 600; }
.brief-summary { max-width: 70ch; font-size: 17px; color: var(--ink-2); margin: 14px 0 0; font-family: var(--font-display); line-height: 1.5; }
.last-triaged { font-family: var(--font-mono); font-size: 11.5px; letter-spacing: 0.02em; color: var(--ink-3); margin: 10px 0 0; }

/* ---------- Profile modal: signature section ---------- */
.sig-preview { border: 1px solid var(--rule); border-radius: 6px; padding: 10px 12px; max-height: 220px; overflow: auto; background: var(--field-bg); font-size: 13px; }
.sig-toggle-row { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-2); cursor: pointer; }
.sig-toggle-row input[type="checkbox"] { width: auto; margin: 0; }
.sig-actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.sig-actions .tz-combo-hint { flex: 1; min-width: 0; }
.sig-btn-row { display: flex; gap: 8px; }

/* ---------- WYSIWYG rich-text editor (signature + draft reply body) ---------- */
.rte { border: 1px solid var(--rule); border-radius: var(--radius); background: var(--field-bg); overflow: hidden; }
.rte-toolbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px; padding: 6px 8px;
  border-bottom: 1px solid var(--rule); background: var(--paper-2, #f3ede2);
}
.rte-btn {
  width: 30px; height: 28px; display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid transparent; background: transparent; color: var(--ink); cursor: pointer;
  border-radius: 6px; font-size: 13px; font-family: var(--font-body);
}
.rte-btn:hover { background: var(--paper-bright); border-color: var(--rule); }
.rte-btn:disabled { opacity: 0.45; cursor: default; }
.rte-select { width: auto; padding: 3px 6px; font-size: 12px; border-radius: 6px; }
.rte-color { width: 30px; height: 28px; padding: 0; border: 1px solid transparent; border-radius: 6px; background: transparent; }
.rte-sep { width: 1px; height: 20px; background: var(--rule); margin: 0 3px; }
.rte-btn-row { display: inline-flex; gap: 2px; }
.rte-editable {
  padding: 10px 12px; font-size: 14px; line-height: 1.6; overflow-y: auto; position: relative;
}
.rte-editable:focus { outline: none; }
.rte-editable:empty::before { content: attr(data-placeholder); color: var(--ink-3); pointer-events: none; }
.rte-editable img { max-width: 100%; height: auto; }
.rte-editable[contenteditable="false"] { background: var(--paper-2, #f3ede2); color: var(--ink-2); }
.rte-source {
  width: 100%; min-height: 140px; border: none; border-radius: 0; resize: vertical;
  font-family: var(--font-mono); font-size: 12px;
}
.rte-source:focus { outline: none; box-shadow: none; border: none; }

/* ---------- Profile modal: reply font section ---------- */
.reply-font-row { display: flex; gap: 8px; }
.reply-font-row select { flex: 1; min-width: 0; }
/* The font-size input is a plain text field (type=text + inputMode=numeric), so it has
   no native spinner at all - nothing to fight over space with the "pt" suffix. */
.reply-font-row .size-field { position: relative; flex: none; width: 90px; }
.reply-font-row .size-field input { padding-right: 24px; }
/* Kept as a safety net in case a native number input is ever reintroduced here (e.g.
   a browser that ignores type=text's lack of a spinner). */
.reply-font-row .size-field input::-webkit-outer-spin-button,
.reply-font-row .size-field input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.reply-font-row .size-suffix {
  position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
  font-size: 12px; pointer-events: none;
}

.reply-font-preview {
  border: 1px solid var(--paper-3); border-radius: 8px; padding: 10px 12px; background: var(--paper-bright);
  color: var(--ink-2); margin-top: 2px;
}

.brief-actions { display: flex; flex-direction: column; gap: 10px; align-items: stretch; }

/* License seat usage (Admin → People). Deliberately not the triage bar's shape or
   colour: that one sweeps to mean "work is happening", and a seat count is a static
   fact. Uses the same tokens so it reads as the same app. */
.seat-bar {
  width: 100%; height: 6px; border-radius: 999px; background: var(--paper-3);
  overflow: hidden; margin: 0 0 8px;
}
.seat-bar-fill {
  height: 100%; min-width: 2%; border-radius: 999px; background: var(--moss);
  transition: width 0.4s var(--ease);
}
.seat-bar-fill.is-nearly-full { background: var(--amber); }
.seat-bar-fill.is-full { background: var(--vermilion); }

/* Queue an address before its owner signs in (Admin → People). Sits under the
   directory it feeds, reads as one row of the list rather than a floating form. */
.person-add {
  display: flex; gap: 8px; align-items: center; margin: 12px 0 0;
}
.person-add input[type="email"] { flex: 1 1 auto; min-width: 0; }

.triage-progress { margin: 0 0 26px; animation: rise 0.3s var(--ease) both; }
.triage-progress-bar {
  width: 100%; height: 6px; border-radius: 999px; background: var(--paper-3); overflow: hidden;
}
.triage-progress-fill {
  height: 100%; width: 6%; min-width: 6%; border-radius: 999px; background: var(--vermilion);
  transition: width 0.4s var(--ease);
}
.triage-progress.is-indeterminate .triage-progress-fill {
  width: 30% !important; animation: triage-indeterminate 1.3s ease-in-out infinite;
}
@keyframes triage-indeterminate {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(333%); }
}
.triage-progress-text {
  margin: 8px 0 0; font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.03em; color: var(--ink-3);
}

.stat-strip {
  display: grid; grid-template-columns: repeat(5, 1fr); border-top: 1px solid var(--ink); border-bottom: 1px solid var(--rule);
  margin-bottom: 36px;
}
.stat { padding: 18px 16px 16px; border-right: 1px solid var(--rule); }
.stat:last-child { border-right: 0; }
.stat-num { display: block; font-family: var(--font-display); font-size: 40px; font-weight: 300; line-height: 1; letter-spacing: -0.03em; }
.stat:first-child .stat-num { color: var(--vermilion); }
.stat-label { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3); }

/* ---------- Admin: setup cards inside the settings table ---------- */
/* The Microsoft 365 connect card and the AI assistant test card are full-width rows of
   #settings-table itself (see renderSettingsTable in app.js): the Microsoft 365 card sits
   directly under that band's caption as its sub-header, the LLM card closes the table as a
   footer. They carry the group-caption tint but NOT its uppercase-mono caption styling -
   each holds a paragraph-sized body, so it brings its own plain-caps title line. */
.setting-band-row td { padding: 12px 10px 14px; }
.setting-band-head { display: flex; align-items: baseline; gap: 10px; font-size: 13px; }
.setting-band-text { font-size: 13px; margin: 6px 0 0; }
.setting-band-row .btn { white-space: nowrap; }
/* Title + detail on the left, the card's action pinned to the right and centred against
   the text block: the action belongs to the card as a whole, not to the last line of it.
   The flex lives on an inner wrapper, never on the td itself: a flex CONTAINER is
   blockified (display:table-cell → block), which drops the table's own width distribution
   - the cell collapsed to its content (216px) and the text flexed to zero. The td keeps
   its table-cell width (full span); the wrapper splits inside it, the text block flex:1
   so it keeps that whole width as when stacked, and only the button is displaced into
   the trailing whitespace. */
.setting-band-inner { display: flex; align-items: center; gap: 16px; }
.setting-band-inner > .setting-band-main { flex: 1 1 auto; min-width: 0; }
.setting-band-aside { flex: none; margin-left: auto; }
/* Footer card: rows above it already carry their own bottom rules, so it opens with a
   top rule and closes the table instead. */
.setting-band-foot td { border-bottom: 0; border-top: 1px solid var(--rule); }

/* ---------- Microsoft 365 setup page (/microsoft-setup) ---------- */
.setup-page { max-width: 860px; }
.setup-intro { font-size: 16px; color: var(--ink-2); font-family: var(--font-display); line-height: 1.55; max-width: 70ch; margin: 0 0 18px; }
.setup-steps { margin: 28px 0 0; padding-left: 26px; display: grid; gap: 30px; counter-reset: setup-step; }
.setup-steps > li { padding-left: 6px; }
.setup-steps > li::marker { content: counter(setup-step, decimal-leading-zero) "  "; counter-increment: setup-step; font-family: var(--font-mono); font-size: 13px; color: var(--vermilion); }
.setup-steps h3 { font-size: 17px; font-family: var(--font-display); text-transform: none; letter-spacing: -0.01em; color: var(--ink); font-weight: 600; margin-bottom: 8px; }
.setup-steps p { margin: 8px 0; line-height: 1.6; }
.setup-checklist { margin: 8px 0; padding-left: 20px; display: grid; gap: 8px; line-height: 1.55; }
.setup-permissions-table { margin: 10px 0 14px; font-size: 13px; }
.setup-permissions-table code { font-size: 12px; }
.copy-field { display: flex; align-items: center; gap: 8px; margin: 8px 0; }
.copy-field code {
  flex: 1; overflow-x: auto; white-space: nowrap; padding: 8px 10px; background: var(--paper-raised);
  border: 1px solid var(--rule); border-radius: var(--radius);
}
.copy-btn.is-copied { background: var(--moss-soft); border-color: transparent; color: var(--moss); }

.today-grid { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(280px, 1fr); gap: 40px; }
.column-head { display: flex; justify-content: space-between; align-items: baseline; padding-bottom: 10px; border-bottom: 1px solid var(--ink); margin-bottom: 8px; }
.column-tools { display: flex; gap: 14px; align-items: center; }
.column-side > section + section { margin-top: 32px; }
.empty { color: var(--ink-3); font-style: italic; font-family: var(--font-display); padding: 22px 0; }
.chart { border: 1px solid var(--rule); border-radius: var(--radius); background: var(--paper); }
/* History chart spans the full column - figsize (11:3.4) matches natural width - and pins its
   bottom edge to the container so the rendered height never rounds a hair short and scrolls. */
.chart-wide { display: block; width: 100%; height: auto; min-height: 0; margin-bottom: 26px; }
/* Wraps the history chart's <img> so the hover tooltip has a positioned ancestor that is exactly
   the image's box - the tooltip's left/top are computed from the pointer's position within it. */
.chart-history-wrap { position: relative; }
.chart-tooltip {
  position: absolute; z-index: 6; transform: translate(-50%, calc(-100% - 10px));
  pointer-events: none; min-width: 130px; padding: 8px 10px;
  background: var(--paper-raised); border: 1px solid var(--rule); border-radius: var(--radius);
  box-shadow: 0 4px 14px var(--hover-ink-strong); font-size: 12px; color: var(--ink-2);
}
.chart-tooltip-title { font-weight: 600; color: var(--ink); margin-bottom: 4px; }
.chart-tooltip-row { display: flex; align-items: center; gap: 6px; line-height: 1.5; }
.chart-tooltip-swatch { width: 8px; height: 8px; border-radius: 2px; flex: none; }
.chart-tooltip-total { margin-top: 4px; padding-top: 4px; border-top: 1px solid var(--rule); }


/* ---------- Action list ---------- */
.action-list { list-style: none; margin: 0; padding: 0; counter-reset: act; }
.action-item {
  position: relative; display: grid; grid-template-columns: 28px 1fr; gap: 4px 14px; align-items: start;
  padding: 14px 0; border-bottom: 1px solid var(--rule); animation: rise 0.4s var(--ease) both;
}
/* The buttons float top-right over the first line (absolute, out of the flow); the counter and the
   whole card body then sit in the normal flow, so the title and the first lines of the email run the
   full width beneath the buttons instead of stopping short of them. Left-aligned in the one column
   past the counter gutter; the counter aligns to the title on the same row. */
.action-item > .action-buttons { position: absolute; top: 14px; right: 0; }
.action-item > .action-body { grid-column: 2 / -1; }
.action-item::before {
  counter-increment: act; content: counter(act, decimal-leading-zero);
  grid-column: 1;
  font-family: var(--font-mono); font-size: 12px; color: var(--ink-3); padding-top: 3px;
}
.action-item.is-done { opacity: 0.5; }
.action-item.is-done .action-title { text-decoration: line-through; }
.action-title { font-family: var(--font-display); font-size: 15px; font-weight: 500; line-height: 1.3; margin: 0; padding-right: 210px; }
.action-meta { display: flex; gap: 10px; align-items: center; margin-top: 5px; font-size: 12px; color: var(--ink-3); flex-wrap: wrap; }
.action-meta .link { font-size: 12px; }
.action-summary { font-size: 13px; }
.action-buttons { display: flex; gap: 6px; }
.check {
  width: 30px; height: 30px; border-radius: 50%; border: 1px solid var(--rule); background: var(--check-bg); cursor: pointer;
  display: inline-grid; place-items: center; transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.check:hover, .check.is-checked { background: var(--moss); border-color: var(--moss); color: var(--on-accent); }
/* The done circle is 30px, the row's other controls are 36px (btn-sm: 7px padding + 1px border
   either side over a 13px/1.55 line). The row aligns its columns to the top, so leaving the
   circle there kept its top flush but left all 6px of the difference as a gap underneath -
   which reads as the circle sitting too high next to Block time and Dismiss. Centring it puts
   half the difference above and half below, which is what .block-chip already does (the line
   below is the reason that chip measured an even gap top and bottom). */
.action-buttons .check { align-self: center; }

/* ---------- Action group (Today, action consolidation) ---------- */
/* One email's cards held together in a raised tile, with the email named once above them. The tile
   is the point of the grouping: it carries the same --paper-raised stock, --rule hairline, corner
   radius and --shadow the panels use, so a group reads as one lifted object and the page between
   groups gives the eye somewhere to rest. The list's <li> children are wrapped in a plain <div>
   here, which breaks the counter only at the wrapper: the items inside still increment `act`, so the
   numbering runs on across groups rather than restarting at each. The head mirrors the snoozed-group's
   mono/uppercase label so a group reads as structure, not as another card, and the subject is a real
   link - opening the email is what a header is for. */
.action-group {
  list-style: none; margin: 0 0 16px; padding: 4px 16px 8px;
  background: var(--paper); border: 1px solid var(--rule);
  border-radius: calc(var(--radius) + 2px); box-shadow: var(--shadow);
}
.action-group-head {
  display: flex; align-items: center; gap: 10px; margin-top: 12px; padding: 6px 0;
}
.action-group-title {
  font-family: var(--font-mono); font-size: 14px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-3); text-align: left;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.action-group-title:hover { color: var(--ink); }
.action-group-count {
  font-family: var(--font-mono); font-size: 11px; color: var(--ink-2); flex: none;
  margin-left: auto;
  border: 1px solid var(--rule); border-radius: 999px; padding: 0 6px; line-height: 16px;
}
/* Inside a tile the row divider belongs only *between* cards; the last card's bottom rule would sit
   orphaned just above the tile's inner padding, so it is dropped there. */
.action-group .action-item:last-child { border-bottom: 0; padding-bottom: 10px; }

/* ---------- Snoozed group (Today) ---------- */
/* Collapsed by default: a snooze hides the noise, the group keeps the hiding honest. Mirrors
   the recipient-list disclosure (caret rotates open), and the rows reuse .action-item -
   is-snoozed only dims them, never strikes through: a snoozed to-do is not a finished one. */
.snoozed-group { margin-top: 14px; border-top: 1px solid var(--rule); padding-top: 10px; }
.snoozed-group summary {
  display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none;
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ink-3);
}
.snoozed-group summary::-webkit-details-marker { display: none; }
.snoozed-group summary .caret { transition: transform 0.2s var(--ease); }
.snoozed-group[open] summary .caret { transform: rotate(90deg); }
.snoozed-group .snoozed-count { color: var(--ink-2); }
.action-item.is-snoozed { opacity: 0.7; }

/* ---------- The "Actions" menu (Today action cards) ---------- */
/* Full-width rows inside the shared rename-dialog shell - one backdrop, one Escape, one
   styling; a dropdown of absolutely-positioned buttons would have needed all three again. */
.action-menu { display: flex; flex-direction: column; }
.action-menu-item {
  display: block; width: 100%; text-align: left; background: transparent; border: 0;
  border-bottom: 1px solid var(--rule); padding: 11px 2px; cursor: pointer;
  font-family: var(--font-body); font-size: 14px; color: var(--ink);
}
.action-menu-item:last-child { border-bottom: 0; }
.action-menu-item:hover { color: var(--moss); }

/* ---------- Action details dialog: notes ---------- */
.note-list { max-height: 180px; overflow-y: auto; margin-bottom: 10px; display: flex; flex-direction: column; gap: 8px; }
.note-row { display: flex; flex-direction: column; gap: 2px; font-size: 13px; }
.note-row .note-date { font-family: var(--font-mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-3); }
.note-row .note-text { white-space: pre-wrap; }
.note-list:empty::after { content: "No notes yet."; color: var(--ink-3); font-size: 12px; }
.note-head { margin-top: 4px; }
.rename-dialog textarea { min-height: 56px; }
.rename-dialog .field-label { margin-bottom: 12px; }

/* ---------- Tags ---------- */
.tag {
  display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono); font-size: 11px;
  letter-spacing: 0.08em; text-transform: uppercase; padding: 2px 8px; border-radius: 3px;
  background: var(--paper-3); color: var(--ink-2);
}
.tag::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.tag-critical { background: var(--vermilion-soft); color: var(--vermilion); }
/* A RITEManage staff bar (docs/15 D addendum): amber, matching RITEManage's
   'blocked' badge - distinct from a local Reject's red. */
.tag-blocked { background: var(--amber-soft); color: var(--amber-ink); }
.tag-high { background: var(--amber-soft); color: var(--amber-ink); }
.tag-normal { background: var(--slate-soft); color: var(--slate); }
.tag-low { background: var(--paper-3); color: var(--ink-3); }
.tag-sent, .tag-approved { background: var(--moss-soft); color: var(--moss); }
.tag-rejected, .tag-failed { background: var(--vermilion-soft); color: var(--vermilion); }
.tag-pending { background: var(--amber-soft); color: var(--amber-ink); }
.tag-plain::before { display: none; }

/* ---------- Email list ---------- */
.email-list { display: grid; gap: 0; }
.email-row {
  display: grid; grid-template-columns: 110px minmax(0, 1fr) auto; gap: 18px; align-items: start;
  padding: 16px 0; padding-left: 14px; border-bottom: 1px solid var(--rule); cursor: pointer; animation: rise 0.4s var(--ease) both;
  transition: background 0.15s;
}
.email-row:hover { background: var(--row-hover); }
/* Critical rows carry a 3px border in place of the plain row's left inset, so 11px + the border
   keeps their text on the same 14px line as every other row. */
.email-row.is-critical { border-left: 3px solid var(--vermilion); padding-left: 11px; }
.email-when { font-family: var(--font-mono); font-size: 12px; color: var(--ink-3); padding-top: 4px; }
.email-subject { font-family: var(--font-display); font-size: 19px; font-weight: 500; margin: 0 0 3px; line-height: 1.25; }
.email-sender { font-size: 13px; color: var(--ink-2); }
.email-summary { font-size: 14px; color: var(--ink-2); margin: 6px 0 0; max-width: 80ch; }
/* The tag cluster's right edge is the row's right edge, so the ⚑ outlook tag sits flush with the
   content column's border, and the tooltip that appears over it on hover overlaps that border.
   Insetting the cluster's box pulls the tag - and the tooltip anchored to it - in from the edge.
   Mobile drops the cluster to the left of a single-column row, where the inset is pointless. */
.email-tags { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; padding-right: 22px; }
.mini-list { list-style: none; margin: 0; padding: 0; }
.mini-list li { padding: 10px 0; border-bottom: 1px solid var(--rule); font-size: 14px; display: grid; gap: 3px; cursor: pointer; }
.mini-list li strong { font-family: var(--font-display); font-weight: 500; font-size: 16px; }
.mini-list li small { color: var(--ink-3); }

/* People directory (merged Mailbox approvals + Tenant users): one row per person. The
   facts sit on top and the action buttons sit under them, still on one line (not stacked
   per action) - then the row's bottom border closes it off. Keeps .mini-list's stacked
   look; only the buttons get a tidy horizontal row of their own. */
.person-row .row-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
.person-row .row-actions .btn { white-space: nowrap; }
/* Coded meta facts (route, decision-maker, date) that carry a hover definition. The dotted
   underline says "there's more here" without shouting - the full meaning is in the title. */
.meta-term { cursor: help; text-decoration: underline dotted; text-decoration-color: var(--rule); text-underline-offset: 2px; }
.meta-term:hover { text-decoration-color: var(--ink-3); }

/* ---------- Drafts ---------- */
.draft-list { display: grid; gap: 22px; }
.draft-card {
  background: var(--paper-raised); border: 1px solid var(--rule); border-radius: 6px; box-shadow: var(--shadow);
  overflow: hidden; animation: rise 0.45s var(--ease) both;
}
.draft-head { display: flex; justify-content: space-between; gap: 16px; padding: 16px 20px; border-bottom: 1px solid var(--rule); background: var(--paper-2); }
/* The subject side takes every spare pixel (min-width:0 lets it shrink below its text); the status
   tag on the right stays put, so the subject field spans the header and stops at that tag. */
.draft-subject-head { flex: 1; min-width: 0; }
/* The card "Open draft" just jumped to: a short glow so the eye lands on it among the others. */
.draft-card.is-focused { animation: draft-focus 1.8s var(--ease); }
@keyframes draft-focus {
  0%, 55% { box-shadow: 0 0 0 3px var(--moss), var(--shadow); }
  100% { box-shadow: var(--shadow); }
}
.draft-head h2 { font-size: 18px; }
.draft-to { font-size: 13px; color: var(--ink-2); margin-top: 2px; }
.draft-context { padding: 14px 20px; font-size: 13px; color: var(--ink-2); border-bottom: 1px dashed var(--rule); }
.draft-body { padding: 18px 20px 6px; }
.draft-body textarea { font-family: var(--font-body); background: transparent; border-color: transparent; padding: 0; min-height: 120px; }
.draft-body textarea:focus { border-color: var(--rule); padding: 10px; background: var(--paper-bright); }
.draft-foot { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 12px 20px 16px; flex-wrap: wrap; }
.draft-foot .group { display: flex; gap: 8px; flex-wrap: wrap; }
.draft-error { color: var(--vermilion); font-size: 13px; padding: 0 20px 12px; }
/* The reviewer's own note, kept from the compose/revise that produced this body. Set apart from
   the draft itself because it is not part of the message - it is what the message was asked to
   become, and the thing to check the new body against. */
.draft-note { padding: 12px 20px; font-size: 13px; border-bottom: 1px dashed var(--rule); background: var(--paper-2); }
/* A standalone draft's subject is the user's (a reply's is its thread's), so it is editable right
   where it is displayed rather than in a dialog. Borderless until focused so the card header still
   reads as a heading. */
/* The subject field takes the full width of its column (which the flex header has grown to the
   free space beside the status tag), so it spans the header and stops at that tag. */
.draft-subject { display: block; width: 100%; box-sizing: border-box; margin-top: 4px; background: transparent; border-color: transparent; font-family: var(--font-display); font-size: 18px; padding: 0; }
.draft-subject:focus { border-color: var(--rule); padding: 4px 8px; background: var(--paper-bright); }

/* ---------- Compose dialog (manual drafts) ---------- */
/* Same width as the meeting composer (.profile-dialog, min(600px, 100%)) - two composers opened
   back to back should not visibly resize the dialog. */
.rename-dialog.compose-dialog { width: min(600px, 100%); }
.compose-dialog textarea { min-height: 130px; font-family: var(--font-body); }
/* Symmetric top/bottom padding: this box's background is what the user actually sees as "the To
   field", so the 36px row must be centred inside it, not flush to its top with all the breathing
   room dumped below (that reads as "the text sits high"). */
.compose-dialog .draft-recipients { padding: 10px 0; }
/* The note's label ("What should the message say") sits right after the To box in the DOM, and
   field-label's own margin is bottom-only - so it read as glued to the To box while sitting a full
   gap away from the textarea below it. Push it off the To box's border explicitly. */
.compose-dialog .draft-recipients + .field-label { margin-top: 14px; }
.compose-hint { font-size: 12.5px; }

/* ---------- Reply from an inbox row ---------- */
/* Resting at rest always; hover answers only the button actually under the pointer. The row-level
   hover rules that used to live here lifted *both* verbs whenever the pointer was anywhere near -
   you can't reach Regenerate without crossing the row, so Reply lit up as a liar. Per-button
   :hover/:focus is the whole effect now; :focus-within stays off too, same reason. */
.email-actions { display: flex; gap: 6px; }
.email-act {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--moss-soft); border: 1px solid transparent; border-radius: var(--radius);
  padding: 4px 10px; font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--moss); cursor: pointer; white-space: nowrap;
}
.email-act:hover, .email-act:focus-visible { background: var(--hover-moss-soft); border-color: var(--moss); }

/* ---------- Drawer: inline compose + the drafts section ---------- */
/* The compose panel sits between the action points and the message it answers - Outlook's shape.
   A moss wash sets it apart from the read-only sections around it without a second card border
   competing with the review card it carries. */
#drawer-compose-section { background: var(--moss-soft); border-radius: var(--radius); padding: 14px 16px; }
.drawer-compose-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; }
.drawer-compose-head h3 { margin: 0; }
#drawer-compose-body > textarea { width: 100%; font-family: var(--font-body); }
.drawer-draft { border: 1px solid var(--rule); border-radius: var(--radius); padding: 12px 14px; margin-bottom: 10px; background: var(--paper-raised); }
.drawer-draft-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
.drawer-draft-head .group { display: flex; gap: 6px; margin-left: auto; }
/* The drafts list shows the shared review card in its read-only flavour, which means a full
   .draft-card (with its own head and foot) inside the drawer's own framed head. Two subject lines
   and the "In reply to" line, twice, would be noise - the drawer's head already says all of it,
   so the card's is suppressed and only the formatted body with its real spacing shows through. */
.drawer-draft .draft-card { border: 0; padding: 0; background: transparent; box-shadow: none; }
.drawer-draft .draft-card > .draft-head { display: none; }
.drawer-draft .draft-card > .draft-context { display: none; }
.drawer-draft .draft-card > .draft-foot { padding: 10px 0 0; }
.drawer-draft .draft-card > .draft-foot .muted { display: none; }

/* ---------- Rename dialog (admin: local account email / display name) ---------- */
.rename-backdrop {
  position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center;
  background: var(--scrim); animation: fade 0.25s both; padding: 20px;
}
.rename-dialog {
  background: var(--paper); border: 1px solid var(--ink); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 24px 26px; width: min(420px, 100%);
  /* The scroll box. It lives on the dialog rather than on any one panel's content so a wide
     payload (a meeting body full of tables) grows the popup sideways instead of opening a second
     scrollbar inside it - two nested scroll wheels over each other is the thing that reads as a
     bug. flex: 1 lets a tall dialog shrink to the viewport and hand the overflow to this one
     scroll, which is the only one the user then has to find. */
  max-height: calc(100vh - 40px); overflow: auto;
  display: flex; flex-direction: column;
}
.rename-dialog h3 { margin-bottom: 16px; }
.rename-dialog .draft-foot { padding: 12px 0 0; }

/* Profile settings carries far more content (timezone + signature + reply font) than the
   rename/confirm dialogs this class is shared with - give it room to breathe and keep the
   .rename-dialog max-height/overflow above as the safety net if a viewport is ever short
   enough for even this wider, tighter version to overflow. */
.rename-dialog.profile-dialog { width: min(600px, 100%); }
.sig-actions .tz-combo-hint { font-size: 12px; line-height: 1.45; }

/* ---------- Profile settings modal: security sections + password grid -------
   Sections stay open - no boxes, no fold-outs. The 2FA section shows its state in a
   small chip beside the label; the password block is one compact grid. */

/* Current password spans the top row; the new pair sits under it with the button
   beside those - one compact block instead of a field per line. */
.pw-grid { display: grid; grid-template-columns: 1fr 1fr auto; gap: 8px; margin-bottom: 16px; }
.pw-grid input:first-child { grid-column: 1 / -1; }
@media (max-width: 480px) {
  .pw-grid { grid-template-columns: 1fr; }
  .pw-grid input:first-child { grid-column: auto; }
}

/* One label style for every heading in the Profile settings modal - the modal title,
   "Email signature", and the two security headings. The app's established eyebrow
   treatment (see .eyebrow): mono, uppercase, letter-spaced, quiet ink. */
.section-label {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-3);
}
.rename-dialog.profile-dialog .section-label { display: block; margin: 18px 0 8px; }
.rename-dialog.profile-dialog h3.section-label { margin-top: 0; margin-bottom: 16px; }
.rename-dialog.profile-dialog .field .section-label { margin-top: 0; }
/* State chip riding beside a section label (2FA's enabled / not enabled).
   Non-mono on purpose - it is a value, not another label. */
.security-value { margin-left: 8px; font-family: var(--font-body); font-size: 12px; font-weight: 400; text-transform: none; letter-spacing: 0; color: var(--ink-2); }

/* ---------- Table ---------- */
.table { width: 100%; border-collapse: collapse; font-size: 14px; }
.table th { text-align: left; font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3); padding: 8px 10px; border-bottom: 2px solid var(--ink); }
.table td { padding: 10px; border-bottom: 1px solid var(--rule); font-variant-numeric: tabular-nums; }
/* Cells holding a control (settings table: the field and the Save/Reset pair). Table cells default
   to vertical-align: baseline, and a button's baseline sits ~7px higher than a 44px field's - so the
   whole control column rendered high and its bottom rule landed ~7px above the other cells' rule
   (measured 57px vs 64px). Middle-aligning the control cells lines every rule up and centres each
   control. Scoped to :has(control) so plain text cells elsewhere keep their baseline alignment. */
.table td:has(input, select, textarea, button) { vertical-align: middle; }
.table td > .row-actions { display: flex; gap: 6px; }
/* Keep the Save/Reset pair on one line even when the row is squeezed narrow. */
.table td > .row-actions { flex-wrap: nowrap; }
.table td > .row-actions .btn { white-space: nowrap; }
/* Keep Save/Reset on one line, side by side, with the pair centred and the row rule aligned. A <td>
   forced to display:flex supplies the gap and lays the buttons out horizontally, but stops
   contributing its content width to the column (so width:1% is needed to make the column hug the
   buttons) AND it breaks the cell's normal table layout - which is why the flex cell sat high and
   dropped the row's bottom rule. So: do NOT make the cell a flex box. Keep it a plain table cell
   (vertical-align: middle centres the pair, and the cell keeps its border-bottom), and lay the
   buttons out as inline-blocks. gap does nothing between inline-blocks, so the pair is spaced with
   the second button's margin-left instead. */
#settings-table .row-actions { display: table-cell; vertical-align: middle; width: 1%; white-space: nowrap; }
#settings-table .row-actions .btn { display: inline-block; white-space: nowrap; }
#settings-table .row-actions .btn + .btn { margin-left: 6px; }
/* One uniform height for the settings fields and rows. A <select> and a text <input> render ~0.5px
   apart at the same padding (different intrinsic line-box), so the row rules step a hair wherever
   the field type changes (the Yes/No selects vs the text fields). Fixed field + row heights make
   every rule land on the same line down the table. Scoped to #settings-table so the login and
   account forms keep their natural control sizing. */
#settings-table td input, #settings-table td select { height: 44px; }
/* Shorten the Settings column and widen the field with the room it frees. The longest label
   ("Microsoft client secret expiry") renders ~118px in this table's text, so a 176px cap (118 text +
   ~28 padding + slack) holds every label to one line while releasing the slack the column had been
   holding for the stretched table; that freed width flows to the value column, which the widened
   field below then fills. */
#settings-table td:nth-child(1), #settings-table th:nth-child(1) { max-width: 216px; }
#settings-table td:nth-child(2) input, #settings-table td:nth-child(2) select {
  width: 19.2em; max-width: 19.2em; min-width: 0; text-overflow: ellipsis;
}
/* Settings are grouped into subsections via SETTING_GROUPS in app.js (one <tbody> per group) - the
   fixed 65px height targets setting rows only, so group header rows size to their content. */
#settings-table tbody tr:not(.setting-group-row) { height: 65px; }
/* The default_timezone row uses createTzCombobox, whose input carries a <p class="tz-combo-hint">
   helper line. It is empty here (no text is set until you type, and there is no room for a caption
   in the table), yet it still reserves its 5px margin + 12px line - which stacked under the 44px
   field and pushed that one row ~12px taller than every other row. :empty targets only the blank
   helper, so a populated hint (or the profile dialog's, outside this table) stays visible. */
#settings-table .tz-combo-hint:empty { display: none; }
/* Subsection headers: one <tbody> per group, its first row is a full-width caption styled like a
   run-in label - mono small-caps like the table's own column headers, on the same paper-2 tint the
   tag pills use, so groups read as tidy bands without a heavy border grid. */
.setting-group-row td {
  padding: 7px 10px; background: var(--paper-2); border-bottom: 1px solid var(--rule);
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-2); vertical-align: middle;
}
.setting-group-row .group-count { color: var(--ink-3); text-transform: none; letter-spacing: 0.04em; }

/* ---------- Triage history (chronological feed) ---------- */
/* History groups by calendar day, but within a day it is ONE stream, newest first: each run's
   brief and every action-point event sit where they happened, so the day reads top-to-bottom as
   what occurred. The #history-kind-filter select narrows the feed to one event type. */
.history-empty { color: var(--ink-3); }
.history-filter { display: flex; align-items: center; gap: 8px; }
.history-filter-label { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3); }
.history-filter select { height: 34px; font-size: 13px; padding: 0 8px; border: 1px solid var(--rule); border-radius: var(--radius); background: var(--field-bg); color: var(--ink); }
.history-day { margin-bottom: 34px; }
.history-day-head {
  display: flex; align-items: baseline; gap: 12px; margin-bottom: 12px;
  padding-bottom: 8px; border-bottom: 2px solid var(--ink);
}
.history-day-title { font-family: var(--font-display); font-size: 20px; font-weight: 500; margin: 0; }
.history-day-runs { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3); }
.history-day-empty { color: var(--ink-3); font-size: 13px; }
.history-stream { display: flex; flex-direction: column; gap: 10px; }
/* Shared shell for both event kinds: a left time gutter keeps the stream's rhythm scannable. */
.history-event-time { font-family: var(--font-mono); font-size: 13px; color: var(--ink); font-variant-numeric: tabular-nums; white-space: nowrap; }
.history-kind-chip {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-3); background: var(--paper-3); border: 1px solid var(--rule);
  border-radius: 999px; padding: 2px 8px; white-space: nowrap;
}
/* A brief: the run's card in the stream - time, chip, status, counts across the head. */
.history-brief { border: 1px solid var(--rule); border-radius: var(--r); background: var(--card); padding: 12px 16px; }
.history-event-head { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.history-run-counts { font-family: var(--font-mono); font-size: 12px; color: var(--ink-3); }
.history-run-error { color: var(--vermilion); font-size: 13px; margin-top: 6px; }
.history-summary { font-size: 13px; color: var(--ink-2); margin: 8px 0 0; line-height: 1.5; }
/* An action: a compact row - time, kind chip, title, email context, block, status, actioned. */
.history-action { display: grid; grid-template-columns: auto auto minmax(0, 1fr) auto auto auto auto; gap: 12px; align-items: center; padding: 8px 12px; border: 1px solid var(--rule); border-radius: var(--r); background: var(--card); }
.history-action-title { font-size: 14px; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.history-action-context { font-size: 12px; color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 260px; }
.history-action-block { font-family: var(--font-mono); font-size: 11px; color: var(--ink-3); white-space: nowrap; }
.history-action-draft { font-family: var(--font-mono); font-size: 11px; color: var(--ink-3); white-space: nowrap; }
.history-action-status { font-size: 12px; color: var(--ink-3); }
.history-action-when { font-family: var(--font-mono); font-size: 12px; color: var(--ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
@media (max-width: 600px) {
  .history-action { grid-template-columns: auto auto minmax(0, 1fr); }
  .history-action-context, .history-action-block, .history-action-draft { grid-column: 3 / -1; }
  .history-action-when { grid-column: 1 / -1; }
}



/* ---------- Schedule tab (docs/14 M3-13) ----------
   Agenda of one day: mirrored Outlook events + proposed/booked focus blocks, merged
   chronologically, plus a 14-day jump rail. Same paper language as the rest of the app:
   mono micro-labels for the when-column, display face for titles, tag pills for status. */
.schedule-layout { display: grid; grid-template-columns: 1fr; gap: 22px; }
.day-rail { display: flex; gap: 6px; overflow-x: auto; padding: 2px 0 8px; }
.day-chip {
  display: grid; gap: 1px; justify-items: center; min-width: 44px; padding: 6px 8px; cursor: pointer;
  background: var(--paper-raised); border: 1px solid var(--rule); border-radius: var(--radius);
  font-family: var(--font-mono); transition: background 0.15s, border-color 0.15s;
}
.day-chip:hover { border-color: var(--ink-3); }
.day-chip .day-name { font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); }
.day-chip .day-num { font-size: 16px; }
.day-chip.is-active { background: var(--ink); border-color: var(--ink); color: var(--paper); }
.day-chip.is-active .day-name { color: var(--paper-3); }
.agenda-list { display: grid; gap: 0; }
/* The side column must be sized, not left `auto`. Two things conspired against it: an `auto` track
   shrinks toward 0 as a free-floating grid item is squeezed, and the `rise` animation below leaves
   a non-none transform on every card, which makes the card a containing block for floats - so the
   float trick cannot opt the time column out of the track sizing here either. With the third track
   collapsed to nothing, justify-items:end and the foot's flex-end pushed against an edge that had
   itself collapsed to x=0, and the status chip plus Book/Dismiss painted from the left instead -
   touching the vertical rule and stacking two and three deep. A definite width keeps the track
   real, and the padding is the gap the chip and buttons need off the rule. */
.agenda-card {
  display: grid; grid-template-columns: 140px minmax(0, 1fr) minmax(180px, max-content); gap: 18px;
  align-items: start;
  padding: 14px 16px 14px 14px; border-bottom: 1px solid var(--rule);
  animation: rise 0.4s var(--ease) both;
}
.agenda-card.is-cancelled { opacity: 0.55; }
.agenda-card.agenda-block { border-left: 3px solid var(--slate); padding-left: 14px; }
.agenda-card.agenda-block.is-proposed { border-left-color: var(--amber); background: var(--card-hover); }
.agenda-card.agenda-block.is-stale { border-left-color: var(--vermilion); }
.agenda-when { font-family: var(--font-mono); font-size: 12px; color: var(--ink-2); padding-top: 3px; display: grid; gap: 1px; }
.agenda-muted { color: var(--ink-3); }
.agenda-title { font-family: var(--font-display); font-size: 17px; font-weight: 500; margin: 0 0 3px; line-height: 1.25; }
.agenda-meta { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; font-size: 13px; color: var(--ink-2); }
.agenda-side { display: grid; gap: 8px; justify-items: end; }
.agenda-rsvp { display: flex; gap: 6px; margin-top: 8px; }
.agenda-foot { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.block-status {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
  padding: 2px 8px; border-radius: 999px; border: 1px solid var(--rule); color: var(--ink-3);
}
.block-status.block-proposed { background: var(--amber-soft); color: var(--amber-ink); border-color: transparent; }
.block-status.block-booked { background: var(--moss-soft); color: var(--moss); border-color: transparent; }
.block-status.block-stale { background: var(--vermilion-soft); color: var(--vermilion); border-color: transparent; }
.block-status.block-cancelled { background: var(--paper-3); color: var(--ink-3); border-color: transparent; }
/* Small status pill on Today's action cards ("booked 14:00" / "proposed 09:30") — links to Schedule. */
.block-chip {
  align-self: center; font-family: var(--font-mono); font-size: 11px; text-decoration: none; white-space: nowrap;
  padding: 3px 8px; border-radius: 999px; border: 1px solid var(--rule); color: var(--ink-3);
}
.block-chip.block-proposed { background: var(--amber-soft); color: var(--amber-ink); border-color: transparent; }
.block-chip.block-booked { background: var(--moss-soft); color: var(--moss); border-color: transparent; }
.block-chip.block-stale { background: var(--vermilion-soft); color: var(--vermilion); border-color: transparent; }
/* A booked block the user (or the auto-complete sweep) marked finished: the chip dims and the
   agenda card behind it shows its Undo control. */
.block-chip.is-completed { opacity: 0.55; text-decoration: line-through; }
.agenda-freeslots { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; padding: 12px 0 4px; font-size: 13px; }
.free-chip {
  font-family: var(--font-mono); font-size: 11px; padding: 2px 8px; border-radius: 999px;
  background: var(--slate-soft); color: var(--slate);
}
.slot-chips { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-top: 10px; font-size: 13px; }
#schedule-toolbar .segmented { flex: 0 0 auto; }
.field-label { display: grid; gap: 4px; font-size: 13px; color: var(--ink-2); margin-bottom: 10px; }
.field-check { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-2); margin-bottom: 10px; }
.field-check input { width: auto; }

/* ---------- Calendar: an openable event ----------
   A mirrored meeting now opens the way Outlook opens one, so the agenda card has to look
   openable: the cursor and the hover lift are what tell a user that clicking the row does
   something before they click it. */
.agenda-card.is-clickable { cursor: pointer; }
.agenda-card.is-clickable:hover { background: var(--paper-raised); box-shadow: var(--shadow); }
.agenda-card.is-clickable:focus-visible { outline: 2px solid var(--slate); outline-offset: -2px; }
.month-item:not(.is-block) { cursor: pointer; }
.month-item:not(.is-block):hover { text-decoration: underline; }

/* min()/vw, not a fixed px: the body is somebody else's Word document, and the complaint this
   answers is "there's a horizontal scrollbar inside the popup". Give the width to the dialog and
   it is one familiar scroll; keep it at 660px and a wide invite forces an inner one. */
.rename-dialog.event-dialog { width: min(820px, 100vw - 40px); }
.event-when { font-family: var(--font-mono); font-size: 12px; margin: -10px 0 14px; }
/* No max-height/overflow of its own - the dialog scrolls (see .rename-dialog), and a second
   scroller nested inside it is what made the body hard to reach. min-width: 0 keeps a wide body
   table from pushing this grid open, which is how the old inner scrollbar was born. */
.event-body { display: grid; gap: 12px; min-width: 0; flex: 1; }
.event-field { display: grid; gap: 2px; font-size: 14px; margin: 0; }
.event-field-label {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-3);
}

/* Attendees as chips, not rows. One person per line meant a nine-person meeting pushed the body
   off the fold and made the dialog look like a mailing list; a wrapping row of names is a third
   the height, and the wrap happens at the edge of the line rather than at a fixed column count. */
.event-attendees { display: grid; gap: 2px; min-width: 0; }
.event-attendee-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 5px; min-width: 0; }
.event-attendee-chip {
  display: inline-flex; align-items: center; gap: 6px; max-width: 100%; font-size: 13px;
  padding: 3px 10px; border: 1px solid var(--rule); border-radius: 999px; background: var(--paper-2);
}
.event-attendee-chip.is-me { border-color: var(--slate); background: var(--slate-soft); }
.event-attendee-chip.is-declined { border-color: var(--vermilion-soft); }
/* The collapsed view is names only. Truncated, because the chip row has to wrap on a name and not
   on "Christopher Frederiksen-Smith". */
.event-attendee-chip-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The expand control is a button, not a click-anywhere row: it is the only thing in the dialog
   that changes what you are looking at, so it should look like one. */
.event-attendee-toggle {
  align-self: start; margin-top: 7px; background: none; border: 0; padding: 0; cursor: pointer;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--slate); border-bottom: 1px solid var(--rule);
}
.event-attendee-toggle:hover { color: var(--ink); }

/* The status table, hidden until asked for. */
.event-attendee-detail { margin: 0; }
.event-attendee-detail[hidden] { display: none; }
.event-attendee {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 4px 10px; align-items: baseline;
  padding: 5px 0; border-bottom: 1px solid var(--rule); font-size: 14px;
}
.event-attendee-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.event-attendee-email { grid-column: 1 / -1; font-size: 12px; }
.attendee-status {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
  padding: 2px 8px; border-radius: 999px; background: var(--paper-3); color: var(--ink-3);
}
.attendee-status.is-accepted { background: var(--moss-soft); color: var(--moss); }
.attendee-status.is-tentativelyAccepted { background: var(--amber-soft); color: var(--amber-ink); }
.attendee-status.is-declined { background: var(--vermilion-soft); color: var(--vermilion); }
.attendee-status.is-organizer { background: var(--slate-soft); color: var(--slate); }
.event-not-mirrored {
  font-size: 13px; color: var(--amber-ink); background: var(--amber-soft); padding: 9px 11px;
  border-radius: var(--radius); margin: 4px 0 0;
}
/* Somebody else's meeting notes, sanitised on write. Constrained rather than trusted: no markup
   from a calendar invite gets to decide how wide this dialog is or what it overlays. The width
   rules are what stop a Word-generated table (Outlook writes fixed cell widths in points) from
   becoming a horizontal scrollbar inside the body box. */
.event-body-html {
  border: 1px solid var(--rule); border-radius: var(--radius); padding: 12px 14px;
  font-size: 14px; line-height: 1.5; background: var(--paper-2); overflow-wrap: break-word;
  min-width: 0; max-width: 100%;
}
.event-body-html img { max-width: 100%; height: auto; }
.event-body-html table { max-width: 100%; table-layout: fixed; }
.event-body-html :is(td, th) { overflow-wrap: break-word; word-break: break-word; }
.event-body-html :is(h1, h2, h3, h4) { font-size: 15px; margin: 0.6em 0 0.3em; }
.event-body-html a { color: var(--slate); text-decoration: underline; }
/* Word/Outlook heads are stripped by the sanitizer; a stray one still must not dictate layout. */
.event-body-html :is(html, body, head) { display: contents; }

/* ---------- Calendar: attendee picker ---------- */
.attendee-field { position: relative; margin-bottom: 10px; }
/* The chips sit directly above the input, and with no gap the last row of chips reads as part of
   the field's border - the two touch and the row you can still remove looks glued to the box you
   type in. Only when there ARE chips: .attendee-chips is display:none when empty, so an always-on
   margin would open a gap above an untouched composer. */
.attendee-chips { display: flex; gap: 6px; flex-wrap: wrap; }
.attendee-chips:not(:empty) { margin-bottom: 8px; }
.attendee-chips:empty { display: none; }
.attendee-chip {
  display: inline-flex; align-items: center; gap: 6px; font-size: 13px; padding: 3px 6px 3px 10px;
  border-radius: 999px; background: var(--slate-soft); color: var(--slate); max-width: 100%;
}
.attendee-chip > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chip-x {
  border: 0; background: transparent; color: inherit; cursor: pointer; font-size: 14px;
  line-height: 1; padding: 2px 4px; border-radius: 999px;
}
.chip-x:hover { background: var(--hover-ink-soft); }
.attendee-results {
  position: absolute; z-index: 30; left: 0; right: 0; top: calc(100% - 6px);
  background: var(--paper-raised); border: 1px solid var(--rule); border-radius: var(--radius);
  box-shadow: var(--shadow); overflow: hidden; display: grid;
}
.attendee-option {
  display: grid; grid-template-columns: minmax(0, auto) minmax(0, 1fr) auto; gap: 8px; align-items: baseline;
  text-align: left; border: 0; background: transparent; padding: 8px 12px; cursor: pointer;
  border-bottom: 1px solid var(--rule); font-size: 14px; color: var(--ink);
}
.attendee-option:last-child { border-bottom: 0; }
.attendee-option:hover, .attendee-option.is-cursor { background: var(--paper-2); }
.attendee-option-name { font-weight: 500; }
.attendee-option-email, .attendee-option-src { font-size: 12px; }
.attendee-option-src { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; color: var(--ink-3); }
.attendee-empty { padding: 9px 12px; margin: 0; font-size: 13px; }
.teams-check { width: auto; }

/* ---------- Schedule: week overlay ----------
   The week borrows the month cell's whole visual language (same paper, same mono day
   number, same item ellipsis) and differs only in geometry: seven columns, taller cells,
   so the titles that the month has to truncate can actually be read. */
.week-wrap { display: grid; gap: 12px; }
.week-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.week-title { font-size: 22px; margin: 0; min-width: 260px; text-align: center; }
/* Same minmax(0, ...) reason as .month-grid: seven equal columns, never one column widened by
   an unbreakable title. Fixed height so all seven cells are the same height. */
.week-grid {
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px;
  grid-auto-rows: var(--week-cell-h);
}
.week-cell {
  display: flex; flex-direction: column; gap: 3px; align-items: stretch; text-align: left; cursor: pointer;
  /* Height from the grid's rows (see .week-grid); no overflow:hidden, so nothing inside the
     cell is clipped away from view. */
  min-height: 0; padding: 6px 8px; background: var(--paper-2); border: 1px solid var(--rule);
  border-radius: var(--radius); font: inherit; color: inherit; transition: border-color 0.2s var(--ease);
}
.week-cell > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.week-cell:hover { border-color: var(--ink-3); }
.week-cell.is-today { border: 1.5px solid var(--ink); }
.week-cell.is-selected { box-shadow: inset 0 0 0 1.5px var(--amber); }
.week-daynum { font-family: var(--font-mono); font-size: 12px; color: var(--ink-2); }
.week-cell.is-today .week-daynum { color: var(--ink); font-weight: 700; }
.week-more { font-family: var(--font-mono); font-size: 10px; color: var(--ink-3); }

/* ---------- Schedule: month overlay ---------- */
.month-wrap { display: grid; gap: 12px; }
.month-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.month-title { font-size: 22px; margin: 0; min-width: 200px; text-align: center; }
/* The month's key and its two click affordances, kept in the header instead of a paragraph under
   the grid where they read as a caption nobody re-reads. The title keeps its centre with the key
   beside it; the hint is pushed right by auto margin, and it is what drops out on a phone - once
   the cells are small the tap-any-day gesture needs no explaining, and the row number is
   discoverable by tapping it. */
.month-legend {
  display: flex; align-items: center; gap: 12px; margin: 0;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--ink-3); white-space: nowrap;
}
.legend-key { display: inline-flex; align-items: center; gap: 5px; }
.legend-mark { color: var(--ink-2); font-size: 11px; line-height: 1; }
.legend-mark.is-block { color: var(--slate); }
.month-hint { font-size: 12px; margin: 0 0 0 auto; white-space: nowrap; }
/* Nine explicit tracks: a 34px gutter for the week number, an empty spacer the width of one
   gap, then the seven days at equal width. minmax(0, ...) rather than the default
   minmax(auto, 1fr) is what makes the seven days uniform: a track's floor is its content's
   min-content width, so a single long unbreakable word (a hyphenated "Project-Standup", or the
   nowrap'd start time) would otherwise push that column wider than its six neighbours - 1fr is
   a maximum, not a limit. Zero floors every track equally. Fixed row height so rows match as
   well as columns.
   Both rows fill nine cells: the header carries an empty span where the week number goes, and
   each week number is followed by an empty span where the spacer goes. Anything less and
   auto-placement wraps the row early - Saturday lands on track 1 and the seventh day is
   measured as an 18px sliver, which is the bug this grid has now had twice. So placement is
   never left to auto: every cell below names its track, and a test walks the rows in source
   order to prove each row really is nine cells. */
.month-grid {
  display: grid;
  grid-template-columns: var(--month-gutter) var(--month-gutter-gap) repeat(7, minmax(0, 1fr));
  column-gap: var(--month-gutter-gap);
  grid-auto-rows: var(--month-cell-h);
}
/* The header is its own grid item, laying out on a copy of the day tracks via subgrid. That is
   not cosmetic: nth-of-type counts element types among siblings, and the day cells are buttons
   exactly like the week number. Keeping the seven weekday names in a container of their own
   means no header span can ever join a day cell's type count and skew the column arithmetic.
   Every cell - header name or day - names its own line rather than being auto-placed.
   Auto-placement is what broke this grid twice: a floated label eating the gutter track, then a
   short row wrapping Saturday back onto track 1. Spans are no better - see the subgrid note.
   Line map: 1 gutter | 2 spacer | 3..9 the seven days. */
.month-head-row {
  display: grid; align-items: center; grid-column: 1 / -1;
  grid-template-columns: var(--month-gutter) var(--month-gutter-gap) repeat(7, minmax(0, 1fr));
}
/* Where subgrid is available the header borrows the parent's real tracks, so a name sits on the
   identical track - and identical rounding - as the day beneath it. Each name still names its
   own line: subgrid supplies the tracks, the per-name lines say which one. */
.month-dow {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-3); padding: 2px 4px; min-width: 0; overflow: hidden;
  grid-column: 3;
}
.month-head-row .month-dow:nth-of-type(2) { grid-column: 4; }
.month-head-row .month-dow:nth-of-type(3) { grid-column: 5; }
.month-head-row .month-dow:nth-of-type(4) { grid-column: 6; }
.month-head-row .month-dow:nth-of-type(5) { grid-column: 7; }
.month-head-row .month-dow:nth-of-type(6) { grid-column: 8; }
.month-head-row .month-dow:nth-of-type(7) { grid-column: 9; }
@supports (grid-template-columns: subgrid) {
  .month-head-row { grid-template-columns: subgrid; }
}
.month-cell { grid-column: 3; }
/* The week number and the gutter spacer are spans, so nth-of-type here counts only the button
   day cells - exactly the seven-per-row rhythm these rules assume. */
.month-cell:nth-of-type(7n+2) { grid-column: 4; }
.month-cell:nth-of-type(7n+3) { grid-column: 5; }
.month-cell:nth-of-type(7n+4) { grid-column: 6; }
.month-cell:nth-of-type(7n+5) { grid-column: 7; }
.month-cell:nth-of-type(7n+6) { grid-column: 8; }
.month-cell:nth-of-type(7n+0) { grid-column: 9; }
/* The row's title, in its own gutter lane - never carved out of a day column, and never floated
   (a floated label becomes grid content in the gutter track and shoves every day right). */
.weeknum-label {
  grid-column: 1;
  display: flex; align-items: center; justify-content: center; min-width: 0; padding: 1px 2px;
  font-family: var(--font-mono); font-size: 10px; color: var(--ink-3);
  background: none; border: 1px solid transparent; border-radius: var(--radius); cursor: pointer;
}
.weeknum-label:hover { color: var(--ink); border-color: var(--rule); }
.weeknum-spacer { grid-column: 2; }
/* No margin on the day cells: the spacer track is what keeps Sunday off the gutter, so Sunday
   is exactly one 1fr track wide like the other six. */
.month-cell {
  display: flex; flex-direction: column; gap: 3px; align-items: stretch; text-align: left; cursor: pointer;
  /* Height comes from the grid's rows; no overflow:hidden here, because the row-start cell
     holds a focusable nested control and a clipped box would cut its focus ring off. Long
     titles are handled by the ellipsis on each child instead. */
  min-height: 0; padding: 6px 8px; background: var(--paper-2); border: 1px solid var(--rule);
  border-radius: var(--radius); font: inherit; color: inherit; transition: border-color 0.2s var(--ease);
}
.month-cell > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.month-cell:hover { border-color: var(--ink-3); }
.month-cell.is-out { opacity: 0.4; }
.month-cell.is-today { border: 1.5px solid var(--ink); }
.month-cell.is-selected { box-shadow: inset 0 0 0 1.5px var(--amber); }
.month-daynum { font-family: var(--font-mono); font-size: 12px; color: var(--ink-2); }
.month-cell.is-today .month-daynum { color: var(--ink); font-weight: 700; }
.month-item {
  /* Times lead every item (see monthCell/weekCell); each must sit whole on one line, so the
     ellipsis cuts the title and the time stays readable - never wrap a bare time onto its own. */
  font-size: 11px; line-height: 1.35; color: var(--ink-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.month-item.is-block { color: var(--slate); }
.month-item.is-cancelled { text-decoration: line-through; color: var(--ink-3); }
.month-more { font-family: var(--font-mono); font-size: 10px; color: var(--ink-3); }

/* ---------- Drawer ---------- */
.drawer-backdrop { position: fixed; inset: 0; background: var(--scrim); z-index: 40; animation: fade 0.25s both; }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.drawer {
  position: fixed; top: 0; right: 0; bottom: 0; width: min(680px, 100vw); z-index: 50;
  background: var(--paper); border-left: 1px solid var(--rule); box-shadow: -30px 0 60px -30px var(--edge-glow);
  transform: translateX(100%); transition: transform 0.4s var(--ease); display: flex; flex-direction: column;
}
.drawer.is-open { transform: none; }
.drawer-head { display: flex; justify-content: space-between; gap: 16px; padding: 24px 28px 18px; border-bottom: 2px solid var(--ink); }
.drawer-head h2 { font-size: 24px; line-height: 1.2; }
.drawer-from { margin: 6px 0 0; font-size: 13px; color: var(--ink-2); }
.drawer-body { overflow-y: auto; padding: 8px 28px 40px; }
.drawer-section { padding: 20px 0; border-bottom: 1px solid var(--rule); }
.drawer-section h3 { margin-bottom: 10px; }
.drawer-summary { font-family: var(--font-display); font-size: 18px; margin: 0 0 6px; }
.drawer-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }
.email-body { word-break: break-word; font-family: var(--font-body); font-size: 14px; line-height: 1.6; color: var(--ink-2); margin: 0; background: var(--paper-raised); border: 1px solid var(--rule); padding: 16px; border-radius: var(--radius); max-height: 50vh; overflow: auto; }
/* The text fallback for a row with no body_html (or an empty one) still has real newlines, so it
   keeps the pre-wrap that markup must not have: bleach emits each tag on its own line, and
   pre-wrap turns those into visible gaps between the words of an otherwise fine paragraph. */
.email-body.is-plain-text { white-space: pre-wrap; }
/* Rendered email markup. Scope is deliberately narrow - every rule below is about containing a
   document we did not write, so a 1200px Outlook table cannot blow the drawer out sideways. */
.email-body > :first-child { margin-top: 0; }
.email-body > :last-child { margin-bottom: 0; }
.email-body p { margin: 0 0 12px; }
.email-body ul, .email-body ol { margin: 0 0 12px; padding-left: 22px; }
.email-body li { margin-bottom: 4px; }
.email-body h1, .email-body h2, .email-body h3, .email-body h4 { font-family: var(--font-display); margin: 18px 0 8px; line-height: 1.25; }
.email-body a { color: var(--accent); overflow-wrap: anywhere; }
.email-body img { max-width: 100%; height: auto; }
/* An image that failed to resolve during sync. Styled as text rather than as a broken <img>
   because a broken-image glyph reads as "your connection failed"; this says the sender's image was
   not retrievable, which is a different fact (docs/17 §6.4). */
.email-body .email-img-missing { display: inline-block; padding: 2px 8px; border: 1px dashed var(--rule); border-radius: var(--radius); color: var(--ink-3); font-size: 12px; font-style: italic; }
/* An aged-out image awaiting the refetch. min-width/height reserve the slot so the logo arriving
   does not shove the text below it down the page; the animation is the "working on it" signal that
   a blank box would not give. */
.email-body img.is-loading { min-width: 40px; min-height: 40px; background: linear-gradient(90deg, var(--shimmer-a) 25%, var(--shimmer-b) 50%, var(--shimmer-a) 75%) 0 0 / 200% 100%; animation: email-img-shimmer 1.1s linear infinite; border-radius: 4px; color: transparent; }
@keyframes email-img-shimmer { to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) { .email-body img.is-loading { animation: none; } }
.email-body table { max-width: 100%; table-layout: fixed; border-collapse: collapse; }
.email-body td, .email-body th { word-break: break-word; overflow-wrap: anywhere; vertical-align: top; }
.email-body blockquote { margin: 0 0 12px; padding-left: 12px; border-left: 3px solid var(--rule); color: var(--ink-2); }
.email-body hr { border: 0; border-top: 1px solid var(--rule); margin: 16px 0; }
/* .drawer-draft itself and the compose panel are styled up in the drawer-draft block near the
   compose styles; the old plain-text <pre> rule died with the text-blob rendering. */
.drawer-draft .draft-body { max-height: 40vh; overflow-y: auto; }
/* .draft-body's own padding (shared with the full-page Drafts tab) inset the editor box 20px on
   each side relative to the To/Cc/Bcc row above it and the footer below, and pushed it 18px down
   off that row. Zeroing the sides and the top - the bottom inset and the max-height cap above are
   untouched - lets the editor box span the exact same width as the To/Cc/Bcc row, and sit tight
   under it instead of leaving a top gap. Scoped to the drawer only, so the Drafts tab's own card
   keeps its original padding. */
.drawer-draft .draft-body { padding: 0 0 6px; }

/* ---------- Responsive ---------- */
@media (max-width: 980px) {
  .login-layout { grid-template-columns: 1fr; gap: 28px; align-items: start; }
  .today-grid { grid-template-columns: 1fr; }
  .stat-strip { grid-template-columns: repeat(3, 1fr); }
  .stat:nth-child(3n) { border-right: 0; }
  .topbar { grid-template-columns: 1fr auto; }
  .topbar-nav { grid-column: 1 / -1; justify-content: flex-start; overflow-x: auto; }
  .brief-header { grid-template-columns: 1fr; }
  .brief-actions { flex-direction: row; flex-wrap: wrap; }
  /* Two month lines need the month's name, the arrows, and the dot key; the click sentence is
     the fourth thing competing for that row. Its auto margin is what would otherwise drop it
     alone onto a line under the arrows - the under-grid caption this moved off in the first
     place - so it stops here rather than at the phone breakpoint. */
  .month-hint { display: none; }
  .month-legend { margin-left: auto; }
}
@media (max-width: 600px) {
  .email-row { grid-template-columns: 1fr; gap: 6px; }
  .email-tags { justify-content: flex-start; padding-right: 0; }
  .stat-strip { grid-template-columns: repeat(2, 1fr); }
  .inline-form { grid-template-columns: 1fr; }
  .toolbar { flex-wrap: wrap; }
  .toolbar input { width: 100%; }
  .month-legend { margin-left: auto; }
  /* The week's row is arrows + a long title ("Week 39 · 20 Sep – 26 Sep"), which leaves no room for
     the key beside them; taking its own centred line matches what the month does one line below. */
  .week-head .month-legend { order: 3; width: 100%; margin-left: 0; justify-content: center; gap: 10px; }
  /* Phones keep the seven-column month (it is a survey, not a reader) but shrink the gutter to
     the bare number's own lane. Heights relax to auto so a tall week's rows are not clipped
     where the screen is shortest. */
  :root { --month-gutter: 22px; --month-gutter-gap: 4px; --month-cell-h: 60px; }
  .month-cell { padding: 4px 5px; }
  /* A phone header cannot hold the month title *and* a key, so the key takes a line of its own
     under the month - centred, rather than squeezing the arrows off the month's centre. */
  .month-legend { order: 3; width: 100%; margin-left: 0; justify-content: center; gap: 10px; }
  /* Phones lose the titles but must not lose the *times* - "when am I busy" is the one thing
     worth reading from a month, and it survives here as the marker (● meeting, ◆ block). */
  .month-item { font-size: 0; }
  .month-item::before { content: attr(data-mark); font-size: 11px; }
  /* Seven columns is unreadable on a phone: the week collapses to one day per row, which
     keeps every title the desktop week shows - there is simply no room to be a grid here. */
  .week-grid { grid-template-columns: 1fr; }
  .week-cell { min-height: 0; }
  .week-title { min-width: 0; flex: 1; }
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/* --- Settings tab (reply style, font, timezone) -------------------------------- */
/* Each preference is its own editorial card - paper panel, hairline rule, the soft ink shadow the
   rest of the app carries - with a mono index numeral and an underlined heading. The cards sit in a
   two-up grid at equal (half) width and auto-place left-to-right, so Reply style and Timezone share
   the top row and Reply font starts the next. Collapses to a single column on a narrow window. */
/* The display must ride on .is-active, not the bare id: an id selector (specificity 1,0,0)
   outranks the generic ".view { display: none }" (0,1,0), so a bare "#view-settings { display:
   grid }" would beat the hide and leave these cards laid out - and visibly painted - at the bottom
   of every other view. Everything else here (tracks, placement) is display-independent and can
   safely live on the id. */
#view-settings.is-active { display: block; }
/* The AI Analysis tab borrows the Settings card look (a single full-width editorial card holding
   the learned panel), so it needs the same display gate and the same display: block at measure.
   Kept separate from #view-settings: the analysis view has no column flow and no danger block. */
#view-analysis.is-active { display: block; }
#view-analysis > .settings-flow { display: block; }
/* Two cards, two rows: they read as one wall of text with no gap when flush, so each card keeps the
   same 28px separation Settings uses between its cards. */
#view-analysis .settings-group { margin-bottom: 28px; }
#view-analysis .settings-group:last-child { margin-bottom: 0; }
/* Section 1 goes two-up like the ledger: the maturity gauges pair off into two columns (four gauges
   become two rows of two), and the two prose blocks pair in a matching half-width band. Collapses to
   one column when the card narrows. */
.learned-gauges { display: grid; grid-template-columns: 1fr 1fr; column-gap: 28px; }
.learned-pair { display: grid; grid-template-columns: 1fr 1fr; column-gap: 28px; align-items: start; }
.learned-pair > .learned-pair-col { min-width: 0; }
@media (max-width: 720px) {
  .learned-gauges { grid-template-columns: 1fr; }
  .learned-pair { grid-template-columns: 1fr; }
}
/* Two balanced columns via CSS multi-column, not grid. A grid's auto rows each size to their
   tallest item, so two cards sharing a row band leave a gap below the shorter one that differs from
   the taller one's - the vertical rhythm reads unevenly between columns (the complaint this
   replaces). Columns flow each card down the left column, then the right, with ONE uniform gap
   (column-gap across, margin-bottom down) no matter where a card lands. The fixed card order keeps
   Reply style (01) at the top of the left column as the tall anchor. */
#view-settings {
  columns: 2;
  column-gap: 28px;
}
/* A column-spanning heading: breaks out of the column flow to run full width across the top, the
   cards resume beneath it in their two columns. -webkit- prefix for Chrome's older column-span. */
#view-settings > .view-head,
#view-settings > .settings-actions {
  column-span: all;
  -webkit-column-span: all;
}
#view-settings > .view-head { margin-bottom: 28px; }
#view-settings > .settings-group {
  min-width: 0;
  break-inside: avoid;
  -webkit-column-break-inside: avoid;
  margin-bottom: 28px;
}
#view-settings > .settings-actions { margin-top: 4px; }
/* The two danger cards (forget-learned / delete-account) sit BELOW the preference columns as one
   full-width row, never as a card inside the flow. column-span: all is what takes it out of the
   columns, and it is safe here only because nothing unspanned follows it inside #view-settings
   except the spanned save bar (Chrome misrenders a span that has unspanned content after it).
   Inside, a plain flex row: each card half width, sharing one row like the preference cards.
   The gap above it is padding-top and cannot be a margin above the cards. Measured in Chrome: the
   span's margin box always lands at the BOTTOM of the columns (969 = the 770 bottom of "Reply
   style", the tall left column, plus its swallowed 28px margin, plus the span's own margin-top) -
   never beside the card it appears to sit under. Timezone, at the bottom of that left column, ends
   at 969 too, so with nothing on the span itself the Forget card started at exactly the same y:
   touching. Padding belongs to the span's own box, so it is the one space that always lands between
   the columns and the two cards. 28px, the same gap the cards keep from each other.
   margin-bottom (not padding-bottom) so the save bar keeps the 4px it has everywhere else. */
#view-settings > .settings-danger {
  column-span: all;
  -webkit-column-span: all;
  display: flex;
  flex-wrap: wrap;
  gap: 28px;
  padding-top: 28px;
  margin-bottom: 28px;
}
.settings-danger > .settings-group {
  flex: 1 1 calc(50% - 14px);
  min-width: 0;
  margin-bottom: 0;
}
/* Reply style (the tall lede card, first in DOM order) leads the left column; the rest flow down it
   then into the right. DOM order is visual order (columns balance by content, and a card never splits
   across a column - break-inside: avoid). Collapses to one column below. */
.settings-group {
  background: var(--paper-raised); border: 1px solid var(--rule); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 22px 24px 24px;
}
/* Numbered, editorial section head: a mono index beside the display title, underlined like the
   column heads elsewhere in the app, with a short lede beneath. */
.settings-group-head {
  display: flex; align-items: baseline; gap: 12px; padding-bottom: 12px; margin-bottom: 16px;
  border-bottom: 2px solid var(--ink);
}
.settings-index {
  font-family: var(--font-mono); font-size: 12px; color: var(--vermilion);
  letter-spacing: 0.06em; flex: 0 0 auto;
}
.settings-group-head h2 { font-size: 19px; line-height: 1.1; }
/* The head owns the rule and the space below it, so the lede sits close beneath without a negative
   margin (which would misplace it if the head were ever absent). */
.settings-group-head + .settings-lede { margin-top: 0; }
.settings-lede { font-size: 13px; color: var(--ink-3); line-height: 1.6; margin: 0 0 18px; }
.settings-lede code { font-family: var(--font-mono); font-size: 12px; }
.settings-group .field:last-child { margin-bottom: 0; }
/* A .field is a grid, so each child is a stretched grid item - which would pull the segmented pill
   to the full card width. Keep it hugging its two labels on its own row instead. */
.settings-group .field .segmented { justify-self: start; }
.settings-actions { display: flex; align-items: center; gap: 14px; margin-top: 4px; }
.settings-actions .draft-error { flex: 1; margin: 0; }
@media (max-width: 720px) {
  /* Single column: drop the multi-column flow so header, the cards, and the action bar stack top to
     bottom in DOM order. Each card keeps its bottom margin, so the vertical gap stays the same. */
  #view-settings { columns: 1; }
  /* ...which is exactly when the danger block's padding-top becomes a DOUBLE gap: in one column the
     card above it is genuinely the last flow item, so its 28px margin-bottom now lands too. Hand the
     gap back to that margin by dropping the padding. The two cards stack too, because every other
     setting is one per row by now and a 213px-wide lede is not readable. */
  #view-settings > .settings-danger { padding-top: 0; }
  .settings-danger > .settings-group { flex: 1 1 100%; }
}

/* --- Draft recipients (To / Cc / Bcc) ----------------------------------------- */
.draft-recipients {
  display: grid; gap: 8px; padding: 12px 20px; border-bottom: 1px solid var(--rule);
  background: var(--paper-2, #f7f2e8);
}
/* Collapsed (the default): only the summary row shows. The editor keeps its full contents in the
   DOM so nothing is lost, it simply does not take vertical space until asked for. */
.draft-recipients.is-collapsed .rcpt-body { display: none; }
.draft-recipients .rcpt-summary-row { display: flex; }
.draft-recipients .rcpt-toggle {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  border: 0; background: transparent; cursor: pointer; padding: 2px 0; color: var(--ink-2);
  font-size: 12.5px;
}
.draft-recipients .rcpt-caret {
  flex: 0 0 auto; font-size: 10px; color: var(--ink-3); transition: transform 0.12s ease;
}
.draft-recipients:not(.is-collapsed) .rcpt-caret { transform: rotate(90deg); }
.draft-recipients .rcpt-summary {
  color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.draft-recipients .rcpt-summary-row:hover .rcpt-summary { color: var(--ink); }

/* Vertically centred by baseline, not just by box: "To" and the field's text share the field's
   font-size and line-height (12.5px / 36px), so their baselines coincide and neither reads as
   riding high - a plain font-size difference between the caption and the input is what made the
   box-centred layout still look top-aligned. line-height:36px centres each single line within the
   36px field; the shared font-size puts those two centred lines on the same baseline. */
.draft-recipients .rcpt-row {
  display: flex; gap: 8px; align-items: center; position: relative;
}
.draft-recipients .rcpt-label {
  flex: 0 0 44px; margin-left: 4px; font-size: 12.5px; line-height: 36px; font-weight: 600;
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-3); padding: 0 8px 0 6px;
}
.draft-recipients .rcpt-chips {
  display: flex; flex-wrap: wrap; gap: 6px; align-items: center; flex: 1; min-height: 36px;
  padding-right: 10px;
}
.rcpt-chip {
  display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--rule);
  border-radius: 999px; padding: 3px 4px 3px 10px; background: var(--paper-raised); font-size: 12.5px;
  line-height: 1.35; white-space: nowrap;
}
.rcpt-chip .rcpt-remove {
  border: 0; background: transparent; cursor: pointer; font-size: 14px; line-height: 1;
  color: var(--ink-3); padding: 2px 6px;
}
.rcpt-chip .rcpt-remove:hover { color: var(--crit, #a3320e); }
.rcpt-chip.is-unknown { border-style: dashed; color: var(--ink-3); font-style: italic; }
.rcpt-input {
  flex: 1; min-width: 160px; border: 1px dashed transparent; background: transparent;
  padding: 6px; font-size: 12.5px;
}
.rcpt-input:focus { outline: none; border-color: var(--rule); background: var(--paper-bright); }
/* New draft's Cc/Bcc: collapsed behind a small link so the dialog reads as "To, then the note" -
   the common case - rather than three chip rows deep before the message the user came to write. */
.rcpt-ccbcc-toggle { display: flex; justify-content: flex-end; margin-top: 6px; }
.rcpt-ccbcc-toggle .link { font-size: 12px; }
.rcpt-ccbcc-rows { display: grid; gap: 8px; margin-top: 8px; }
/* Address suggestions (the recipient picker, mirroring the meeting composer's attendee list):
   anchored to the row's right column, dropped just under it, and above the reply body. */
.rcpt-results {
  position: absolute; top: calc(100% + 3px); left: 52px; right: 0; z-index: 40;
  background: var(--paper-bright); border: 1px solid var(--rule); border-radius: 8px;
  box-shadow: 0 8px 22px var(--scrim); padding: 4px; max-height: 244px; overflow-y: auto;
}
.rcpt-option {
  display: grid; grid-template-columns: minmax(0, auto) minmax(0, 1fr) auto; gap: 8px;
  align-items: baseline; width: 100%; text-align: left;
  border: 0; background: transparent; cursor: pointer; padding: 7px 10px; border-radius: 6px;
  color: var(--ink); font-size: 12.5px;
}
.rcpt-option.is-cursor, .rcpt-option:hover { background: var(--paper-2, #f2ecdf); }
.rcpt-option-name { font-weight: 600; }
.rcpt-option-email { color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Same "directory" vs "recent" provenance tag the meeting composer's attendee picker shows -
   the two pickers share /api/calendar/people, so a suggestion here deserves the same label. */
.rcpt-option-src {
  font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; color: var(--ink-3);
}

.rcpt-replyall { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--ink-2); }
.rcpt-replyall input { width: auto; }
.rcpt-hint { font-size: 11.5px; color: var(--ink-3); line-height: 1.5; }

/* --- "What the AI has learned" transparency panel (docs/18 §8.1) ------------- */
.learned-gauges { display: grid; gap: 12px; margin-bottom: 8px; }
.learned-gauge-top { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px; }
.learned-gauge-label { font-weight: 600; font-size: 13.5px; color: var(--ink); }
.learned-gauge-stage { font-family: var(--font-mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-3); }
.learned-bar { height: 8px; border-radius: 999px; background: var(--paper-2, #f2ecdf); overflow: hidden; }
.learned-bar-fill { height: 100%; border-radius: 999px; background: var(--ink-2); transition: width 0.3s ease; }
.learned-gauge-count { font-size: 11.5px; margin-top: 3px; display: inline-block; }

/* Learned-usefulness ledger (docs/21 §7.2) - the same bar shape as the maturity gauges above: it is
   the same kind of claim, "this is how much it has learned, and here is the evidence", and a fancier
   visual would imply a precision the 0-100 scale does not have.

   The eight rows share one identical track list, which is what lines the labels, bars and thumbs up
   in columns down the list; a stack of near-identical blocks otherwise makes the reader re-read each
   row to find the differences, which is the whole point of the panel.

   Every child is placed explicitly, and nothing spans. The DOM order the renderer emits is
   label-bar, bar, sub-line, thumbs - thumbs LAST - so anything left to auto-placement lands in an
   implicit fourth row and adds ~25px of dead height to every row. Spanning is the other trap: a
   spanning item does not contribute its height to the rows it crosses, so a 41px label placed
   "1 / span 2" overflowed a 23px row 1 rather than sizing it, and the row it shared with the thumbs
   pushed the bar 20px below the label it is supposed to sit level with. */
/* The ledger is a two-column grid: the eight rows are compact tiles, not full-width stacked lines,
   so the panel reads at a glance instead of scrolling. Each tile is its own little grid (label/score,
   bar, sub-line, thumbs). Row separators live on the tiles; the tile grid owns the column gutter.
   One column below the width where a 50% tile would squeeze the bar to nothing. */
.learned-use-grid { display: grid; grid-template-columns: 1fr 1fr; column-gap: 28px; }
.learned-use-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto auto;
  column-gap: 12px;
  border-top: 1px solid var(--rule, #e2d9c8);
  padding: 7px 0;
}
/* The name and its number take the first line at full width; the scale bar and the thumbs share the
   line beneath it, thumbs at their natural width so the bar fills the rest and the thumbs always sit to
   the right of the scale bar. The earlier 1fr-thumb-track failure put the thumbs in a ~92px column that
   squeezed the bar to 209px while the sub-line ran full width - the bar looked stranded under the
   name/number. Two rows, both full width, is what keeps bar, thumbs, and sub-line one clean column. */
.learned-use-row {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto auto auto;
  column-gap: 0;
  padding: 6px 0;
}
.learned-use-top {
  grid-column: 1; grid-row: 1;
  display: flex; flex-direction: row; align-items: baseline; justify-content: space-between;
  gap: 6px; min-width: 0;
}
/* Weight and colour must both be stated here rather than left to a wrapper: the tiles live inside the
   panel's .field element, and ".field span" (:411) scores 0,1,2 against this rule's 0,1,0, so that
   generic caption rule - --ink-2 at weight 500 - is what every metric heading paints unless this one
   says otherwise. (The old provisional override was the single rule strong enough to beat it, which
   is exactly why untouched tiles alone came out a shade lighter than their neighbours.) */
.learned-use-top .learned-use-label { font-weight: 600; font-size: 13.5px; color: var(--ink); white-space: nowrap; }
.learned-use-score { white-space: nowrap; flex: 0 0 auto; }
.learned-use-score { font-family: var(--font-mono); font-size: 11px; color: var(--ink-3); }
/* Bar and thumbs share row 2 so the buttons sit to the right of the scale bar; the bar fills the
   track and the thumbs overlay its right end, opaque so they read as sitting on it rather than as a
   gap. Both center on the row (align-self center) so a 6px bar and taller thumbs share one midline. */
.learned-use-bar {
  grid-column: 1; grid-row: 2; align-self: center;
  display: flex; align-items: center; height: 6px;
}
.learned-use-thumbs {
  grid-column: 1; grid-row: 2; justify-self: end; align-self: center;
  display: flex; gap: 6px;
  /* Emoji sits high in the button box, so the pair needs a small downward nudge to share the bar's
     midline; the background lets them read as sitting on the bar's right end rather than a gap. */
  transform: translateY(3px);
  background: var(--paper, #fffdf7); padding-left: 8px; border-radius: 999px;
}
.learned-use-sub {
  grid-column: 1 / -1; grid-row: 3; display: flex; gap: 8px; align-items: baseline;
  flex-wrap: wrap; font-size: 11.5px; margin-top: 2px;
}
.learned-use-chip {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase;
  border: 1px solid var(--rule, #e2d9c8); border-radius: 999px; padding: 0 7px; white-space: nowrap;
}
.learned-use-chip.is-settled { color: var(--moss, #4f6f4a); border-color: var(--moss, #4f6f4a); }
.learned-thumb { padding: 1px 8px; font-size: 12px; line-height: 1.6; }
/* The house button language (.btn-ghost, .check): a clear outline at rest, colour only when the
   pointer arrives or the state is chosen. The earlier treatment left these plain-rule buttons whose
   emoji glyph rendered in the OS palette - the Windows thumbs are mustard-yellow, which read as a
   warning colour this app reserves for amber states and never means here. So: a clearer ink rule at
   rest (the ask was "a clearer outside"), the thumb filling with colour on hover - moss, which is
   exactly the .check:hover treatment - and the chosen thumb keeping that fill until it is pressed
   again. One shared moss for BOTH thumbs when chosen: the down thumb used to light in a faint
   neutral gray while the up thumb lit moss, and "weigh it less" being near-invisible next to its
   own unlit neighbour read as the press doing nothing. Which way the axis leans is carried by the
   glyph itself and by aria-pressed; the fill only answers "is this axis spoken for". The earlier
   reason for the gray - a negative lean must never borrow the app's vermilion, which means error -
   still holds; moss is not vermilion. The SVG glyphs (app.js thumbIcon) inherit all of it through
   currentColor and their cuff through the border colour, so no emoji palette can leak in at any
   size; the saturate(0) stays as the belt-and-braces guard for a glyph a future font still renders
   in colour at rest, and drops away exactly where the button goes coloured. The one thing this
   exception must always win back is the background: since the raised-chip tokens landed, .btn-ghost
   paints --btn-neutral-bg (white in both editions), which would turn a resting thumb into the filled
   chip the design calls the chosen state - so transparent is stated outright, not left to inherit. */
.learned-thumb { background: transparent; border-color: var(--outline-ink-strong); color: var(--ink-2); }
.learned-thumb svg { filter: saturate(0); }
.learned-thumb:hover, .learned-thumb:focus-visible {
  border-color: var(--moss); background: var(--moss-soft); color: var(--moss);
}
.learned-thumb:hover svg, .learned-thumb:focus-visible svg { filter: none; }
.learned-thumb.is-active { border-color: var(--moss, #4f6f4a); background: var(--moss-soft, #eaf0e6); color: var(--moss, #4f6f4a); }
.learned-thumb.is-active svg { filter: none; }
/* One colour for every metric bar, in every state. The eight tiles sit in one grid and read as eight
   of a kind, so the two ways the ledger used to tint them - a settled-and-factual bar going slate, a
   provisional bar going faint - made them look like different kinds of thing, which is a claim the
   panel never meant to make: no axis here matters more than another, it is only better evidenced.
   Whether an axis has settled, and whether it may quiet a card, is spelled out by its own chip
   ("needs 3" / "can quiet" / "never quiets"), so the tint carried nothing the text does not already
   say. The fill is therefore the one --ink-2 the maturity gauges above use, and it stays scoped to a
   ledger row rather than folded into .learned-bar-fill: this rule scores 0,2,0, which is what it takes
   to beat the state rules it retires, and the gauges' own fill cannot reach that on its own. */
.learned-use-row .learned-bar-fill { background: var(--ink-2); }
@media (max-width: 640px) {
  /* Block flow: label and score on one line, then the bar, then the thumbs, then the sub-line. */
  .learned-use-row { display: block; }
  .learned-use-top {
    display: flex; flex-direction: row; justify-content: space-between; align-items: baseline;
  }
  .learned-use-bar { margin-top: 4px; }
  .learned-use-thumbs { margin-top: 5px; }
  .learned-use-sub { margin-top: 3px; }
}
/* The ledger goes two-up on wide layouts; below the card width a 50% tile squeezes the bar, so it
   falls back to a single column. */
@media (max-width: 720px) {
  .learned-use-grid { grid-template-columns: 1fr; }
}

/* The drawer's grounds for the surfacing decision (docs/21 §7.1). Set in the muted tone and mono,
   the way the gauge stages are, so it reads as the system's arithmetic rather than as prose the
   model wrote about the email. */
.drawer-why {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  font-family: var(--font-mono); font-size: 11px; line-height: 1.5; color: var(--ink-3);
People I work with  border-left: 2px solid var(--rule, #e2d9c8); padding-left: 8px; margin: 8px 0 0;
}
.drawer-why-label {
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3);
}
/* One box per weighed factor, so the reader can see where one consideration stops and the next
   starts. The same pill shape as the learning log rather than a new one. */
.drawer-why-chip {
  border: 1px solid var(--rule, #e2d9c8); border-radius: 999px; padding: 1px 8px;
  color: var(--ink-2);
}
/* Weight of the argument, in two channels at once: hue is the direction and background intensity is
   how hard it argued (|3| darkest, |2| medium, |1| faint). A metric arguing FOR the email keeps green
   (the same moss the tags use for keep/approved); one arguing AGAINST it goes neutral gray - "this
   reads as ordinary background mail", never red, because vermilion is this app's reserved alarm for
   critical/error and quieting a card is not an error. Amber is the weak +/-1 lean either way. Classes
   come from whyToneClass in app.js, which mirrors the contribution tables in usefulness.py. */
.drawer-why-chip.is-surf-strong { background: var(--hover-moss-strong); border-color: var(--outline-moss-strong); color: var(--moss); }
.drawer-why-chip.is-surf-med { background: var(--hover-moss-faint); border-color: var(--outline-moss-mid); color: var(--moss); }
.drawer-why-chip.is-quiet-med { background: var(--hover-ink-faint); border-color: var(--hover-ink-soft); color: var(--ink-2); }
.drawer-why-chip.is-quiet-strong { background: var(--hover-ink-strong); border-color: var(--outline-ink-strong); color: var(--ink); }
.drawer-why-chip.is-lean-weak { background: var(--amber-soft); border-color: var(--outline-amber-mid); color: var(--amber-ink, #9a5f12); }
.learned-corrections { font-size: 12px; margin: 6px 0 2px; }
.learned-h { margin: 18px 0 6px; }
.learned-digest {
  font-size: 13.5px; line-height: 1.55; color: var(--ink-2);
  padding: 12px 14px; border-radius: 8px; background: var(--paper-2, #f2ecdf);
}
.learned-log { display: flex; flex-direction: column; gap: 8px; }
.learned-log-item { display: flex; gap: 8px; align-items: baseline; font-size: 13px; color: var(--ink-2); }
.learned-log-kind {
  flex: none; font-family: var(--font-mono); font-size: 10px; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--ink-3); border: 1px solid var(--rule, #e2d9c8);
  border-radius: 999px; padding: 1px 7px;
}
.learned-log-rejected { color: var(--crit, #a3320e); border-color: var(--crit, #a3320e); }

/* Relationship memory (docs/19 Stage 2/3): one row per person whose mail the loop has read. The
   counts are the observation and sit in the muted tone; only the learned line - the assistant's
   reading of those counts, present for a minority of people - is given the body ink, so a reader
   can tell what happened from what was concluded. Two columns, filled down one side then the other
   (grid-auto-flow: column), whole rows only - a person's counts and learned line never split. The
   row slots must be definite (repeat(4, auto)): with indefinite auto-rows, column flow has no limit
   and drops every item into the first column, stacking them vertically. One column when it narrows. */
.learned-contacts {
  display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: repeat(4, auto);
  grid-auto-flow: column; column-gap: 28px; row-gap: 2px;
}
@media (max-width: 720px) {
  .learned-contacts {
    grid-template-columns: 1fr; grid-template-rows: none; grid-auto-flow: row;
  }
}
.learned-contact { border-top: 1px solid var(--rule, #e2d9c8); padding-top: 8px; }
.learned-contact-top { display: flex; gap: 10px; align-items: baseline; justify-content: space-between; }
.learned-contact-name { font-size: 13px; color: var(--ink-2); overflow-wrap: anywhere; }
.learned-contact-count { font-family: var(--font-mono); font-size: 11px; flex: none; }
.learned-contact-sub { font-size: 12px; margin-top: 2px; }
.learned-contact-learned { font-size: 12.5px; color: var(--ink-2); margin-top: 4px; font-style: italic; }
.learned-controls { margin-top: 18px; }
