/* ==========================================================================
   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 and its cells can be resized together
     by the phone breakpoint. The gutter is a real track, so the seven day columns are never narrowed
     by the week number; there are no gaps between tracks (the month is one ruled table). */
  --month-gutter: 34px;   /* fits a two-digit week number at the 10px mono size */
  /* The month's week rows FILL the viewport (see .month-grid); --month-cell-h is the floor a
     row never shrinks under, and the short-window minimum is built from it. */
  --month-cell-h: 96px;
  /* What the page spends above the survey, subtracted whole from 100vh so the grid's last row
     ends at the window's bottom edge: sticky top bar + its rule (~64), .app-main's top padding
     (36), the "Calendar" view-head (title/toolbar line ~58 + 18 padding + 2 rule + 28 margin),
     plus generous slack (~58) so a taller toolbar (wrapped on a narrow desktop, or the offline
     banner appearing) never re-opens a page scroll — slack only costs a few pixels of row
     height, a short budget costs the locked-in view. NOT --week-chrome-h: the week's budget
     also carries the agenda list the survey hides. */
  --month-chrome-h: 256px;
  --month-pad-y: 40px;            /* air below the grid */
  --month-head-gap: 12px;         /* air between the month changer and the grid - the week view's own .week-wrap gap */
  --month-chrome-h-phone: 340px;  /* the toolbar wraps to several lines at phone widths */
  /* Week time grid. --week-hour-h sets the whole track's scale: 24 rows of it is the day's height,
     so it is the one number that decides how readable a 30-minute meeting is. 56px puts a quarter
     hour above the 11px title line, which is the point at which a block's height stops being a
     guess. --week-hour-w holds the hour axis, sized for "24:00" at the 10px mono.
     --week-chrome-h is what the grid's viewport cap subtracts: the sticky topbar (~63px, plus its
     rule) and the schedule toolbar, so a full-height grid does not push the last hour's label under
     the window's bottom edge. Overhead is generous by intent - a grid that runs a few pixels short
     is invisible, one that overflows hides an hour. */
   --week-hour-h: 56px;
   --week-hour-w: 54px;
   --week-col-min: 104px;
   --week-chrome-h: 320px;
   --week-chrome-h-phone: 380px;
   --week-slot-h: calc(var(--week-hour-h) / 4);  /* 15-minute snap height (MINUTE_STEP grid) */
}

*, *::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;
  /* Menu-rollover stock (feedback #21). The hovers below (.user-menu-btn/.user-menu-item,
     .edition-btn/.edition-item, .tz-combo-option) print a mid-grey ~12 L* under the surfaces they
     rest on, where a --paper-3 wash is only ~4 L* off this grey stock and reads as no feedback.
     The tokens live here with the rest of the re-pointing so this stays the sheet's sole
     html[data-theme="afterdark"] declaration block; the hover rules that consume it sit below. */
  --paper-3-hover: #d2d1cc;
  /* 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.
   Feedback #21 (revised): an earlier pass gave these chips a fill step and a full-ink border
   on hover - reverted at the user's request. Buttons must hover exactly as Morning Edition's
   do (border firms, no fill change, no black border); the #21 complaint was about the menu
   rollovers - the nav tabs and the dropdown menus - which are strengthened below. */
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); }
/* Pressroom menu rollovers (feedback #21): the dropdown/menu hovers (.user-menu-btn,
   .user-menu-item, .edition-btn, .edition-item, .tz-combo-option) print --paper-3, a step of
   only ~4 L* off this grey stock - no visible feedback. They ride a clear mid-grey here
   instead, ~12 L* below the surfaces they rest on (the topbar bar, the menu sheet, the tz
   dropdown). The nav tabs carry an even stronger wash beside this (see .nav-link:hover). The
   --paper-3-hover token they consume is re-pointed up in the Pressroom block above. */
html[data-theme="afterdark"] .user-menu-btn:hover,
html[data-theme="afterdark"] .user-menu-item:hover,
html[data-theme="afterdark"] .edition-btn:hover,
html[data-theme="afterdark"] .edition-item:hover,
html[data-theme="afterdark"] .tz-combo-option:hover,
html[data-theme="afterdark"] .tz-combo-option.is-active { background: var(--paper-3-hover); }
/* 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 {
  margin: 0; display: inline-flex; align-items: baseline; gap: 5px;
  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;
}

/* ---------- Microsoft permissions notice (docs/38 section 3.14) ---------- */
.perm-dialog { max-width: 560px; }
.perm-list { list-style: none; margin: 12px 0; padding: 0; display: grid; gap: 8px; }
.perm-row { display: grid; grid-template-columns: 22px 1fr; gap: 8px; align-items: start; }
.perm-mark { font-weight: 700; text-align: center; line-height: 1.4; }
.perm-row.is-granted .perm-mark { color: var(--moss); }
.perm-row.is-missing .perm-mark { color: var(--vermilion); }
.perm-row.is-unknown .perm-mark { color: var(--ink-3); }
.perm-main { display: grid; gap: 2px; }
.perm-scope { font-family: var(--font-mono); font-size: 11.5px; color: var(--ink-3); }
.perm-why { font-size: 12.5px; color: var(--ink-2); line-height: 1.4; }
.perm-row.is-missing .perm-why { color: var(--ink); }
.perm-users { margin: 4px 0 0; font-size: 12.5px; }
.perm-help { font-size: 12.5px; }
.perm-dialog .draft-foot { margin-top: 14px; justify-content: flex-end; gap: 8px; }

/* ---------- Status dock: live sync line + build line (docs/38 section 3.11) ---------- */
/* One fixed card, bottom-right. Line 1 is the live sync status, line 2 the build stamp, both in the build
   stamp's monospace so they read as one instrument. The dock itself ignores the pointer (it must never
   swallow a click meant for the page behind it); only the pill and the open details panel take clicks. */
.status-dock {
  position: fixed; right: 14px; bottom: 10px; z-index: 31; pointer-events: none;
  display: flex; flex-direction: column; align-items: flex-end; gap: 8px; max-width: min(86vw, 340px);
}
.status-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
  padding: 6px 12px; border: 1px solid var(--rule); border-radius: 14px;
  background: var(--paper-raised); box-shadow: var(--shadow);
}
.sync-wrap { display: block; max-width: 100%; }
.sync-pill {
  pointer-events: auto; display: inline-flex; align-items: center; gap: 8px; max-width: 100%;
  margin: 0; padding: 0; border: 0; background: none; cursor: pointer;
  font-family: var(--font-mono); font-size: 11px; color: var(--ink); font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em; white-space: nowrap;
}
.sync-pill:hover .sync-text { text-decoration: underline; text-underline-offset: 2px; }
.sync-pill:focus-visible { outline: 2px solid var(--slate); outline-offset: 3px; border-radius: 4px; }
.sync-text { overflow: hidden; text-overflow: ellipsis; }
.sync-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--moss); }
.sync-pill.is-ok .sync-dot { background: var(--moss); }
.sync-pill.is-warn .sync-dot { background: var(--amber); }
.sync-pill.is-error .sync-dot { background: var(--vermilion); }
.sync-pill.is-error { color: var(--vermilion); }
/* Busy: a spinning ring instead of a solid dot - the only animated state. */
.sync-pill.is-busy .sync-dot {
  background: transparent; border: 2px solid var(--slate-soft); border-top-color: var(--slate);
  animation: spin 0.8s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .sync-pill.is-busy .sync-dot { animation: none; border-color: var(--slate); background: var(--slate-soft); }
}
.sync-panel {
  pointer-events: auto; width: min(86vw, 330px); padding: 12px 14px; border: 1px solid var(--rule);
  border-radius: var(--radius); background: var(--paper-raised); box-shadow: var(--shadow);
  font-size: 12.5px; color: var(--ink-2);
}
.sync-panel-title { margin: 0 0 6px; font-size: 13px; color: var(--ink); }
.sync-panel-push { margin: 0 0 8px; color: var(--ink-3); line-height: 1.45; }
.sync-panel-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; max-height: 220px; overflow: auto; }
.sync-panel-list li { display: grid; grid-template-columns: auto 1fr; gap: 8px; }
.sync-panel-list time { font-family: var(--font-mono); font-size: 11px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.sync-panel-list .is-error { color: var(--vermilion); }
.sync-panel-empty { color: var(--ink-3); }

/* ---------- 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. */
.topbar-nav { display: flex; gap: 4px; justify-content: center; min-width: 0; }
.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;
  white-space: nowrap;   /* a tab is one line: "Email Intelligence" never stacks into two */
}
.nav-toggle { display: none; }
.nav-link:hover { background: var(--paper-3); color: var(--ink); }
/* Pressroom nav hover (feedback #21): the tab pills (.nav-link - Today, Critical, Drafts,
   Inbox...) rest transparent on the topbar --paper, and --paper-3 (#d3d0c9) moved only ~4 L*
   off it, so the hover read as nothing. A 20% ink wash lands ~25 L* down from the bar on this
   edition (Morning's --paper-3 step stays untouched) - unmistakable whatever pill it lands on,
   and the active tab's solid-ink fill is far darker still, so hover never fakes selection. */
html[data-theme="afterdark"] .nav-link:hover { background: rgba(22, 24, 26, 0.2); }
.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; min-width: 0; }
.edition-btn, .user-menu-btn { white-space: nowrap; }
.user-name { min-width: 0; max-width: 180px; overflow: hidden; text-overflow: ellipsis; }
.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; max-width: 74rem; }
.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; }
/* 70ch held the brief to 770px even though the header gave it ~950, and the gap only widened as
   the viewport grew (measured ~390px of dead text at 1440, the "wasted space" feedback). The
   measure is what the rule guards - ragged-long lines are unreadable - so widen it to ~95ch and
   cap the header to the same reading measure (~74rem) at large viewports: the title, summary and
   button column then stop drifting apart on wide screens instead of leaving a void. */
.brief-summary { max-width: 95ch; 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; }
/* A sliding on/off switch (docs/38 s.3.24): drawn at the size and shape of the text field it replaces, with "Off"
   and "On" both readable. A real checkbox sits invisibly over the whole control (keyboard, screen readers, form
   state); the thumb is a half-width block that slides from the Off half to the On half when it is clicked. */
.switch { position: relative; display: block; width: 100%; box-sizing: border-box; min-height: 42px; cursor: pointer;
  border: 1px solid var(--rule); border-radius: var(--radius); background: var(--field-bg); overflow: hidden;
  user-select: none; transition: border-color 0.2s, box-shadow 0.2s; }
.switch input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; z-index: 2; }
.switch-track { position: absolute; inset: 0; display: grid; grid-template-columns: 1fr 1fr; align-items: center;
  text-align: center; font-size: 13px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-2); }
.switch-track::before { content: ""; position: absolute; top: 3px; bottom: 3px; left: 3px; width: calc(50% - 3px);
  border-radius: calc(var(--radius) - 2px); background: var(--ink-3); z-index: 0;
  transition: transform 0.22s var(--ease), background 0.22s var(--ease); }
.switch-track span { position: relative; z-index: 1; transition: color 0.22s var(--ease); }
.switch input:checked + .switch-track::before { transform: translateX(100%); background: var(--slate); }
.switch input:not(:checked) + .switch-track .switch-off,
.switch input:checked + .switch-track .switch-on { color: #fff; font-weight: 600; }
.switch:hover { border-color: var(--ink-3); }
.switch:focus-within { outline: 2px solid var(--ink); outline-offset: 2px; }
.switch input:disabled { cursor: not-allowed; }
.switch input:disabled + .switch-track { opacity: 0.5; }
@media (prefers-reduced-motion: reduce) { .switch-track::before, .switch-track span { transition: none; } }
.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); }

/* ---------- Admin: client onboarding (right column, docs/23) ---------- */
/* The minted link is shown once, only under the click that minted it - a wrapping code so a
   long URL never stretches the narrow column. The list reads like the People list: one line
   per link, tag + dates + (for a live link) its Revoke button pinned right. */
.setup-link-once { margin: 0 0 8px; }
.setup-link-once code {
  font-size: 12px; word-break: break-all; display: block;
  background: var(--paper-2, #f7f2e8); border: 1px solid var(--rule); border-radius: 6px;
  padding: 8px 10px;
}
.setup-links-list { display: grid; gap: 8px; }
/* Revoke is pinned to the right of the row and never wraps (flex: none, row does not wrap): the
   info block beside it (tag + dates) is the part that gives way - min-width: 0 lets it shrink, and
   its own flex-wrap drops the dates onto a second line beneath the tag when they do not fit. */
.setup-link-item {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 10px;
  font-size: 12px; flex-wrap: nowrap;
}
/* Tag in the first column, vertically centred against the dates; the dates are one text column beside
   it, a line each, so "expires" starts exactly under "created" and the block is as narrow as its
   longest line. */
.setup-link-info { display: flex; align-items: center; gap: 10px; min-width: 0; flex: 1 1 auto; }
.setup-link-info .tag { flex: none; }
.setup-link-dates { display: grid; gap: 0; min-width: 0; line-height: 21px; }
.setup-link-item .btn { flex: none; margin-left: auto; white-space: nowrap; }
/* The mint button and the Copy button of a freshly-minted link share one line: the minted
   block normally sits empty above the Create button, and when it fills, its Copy joins
   Create on the row below the URL rather than stacking two separate buttons. */
.setup-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.setup-link-once { margin: 0 0 10px; }

/* ---------- 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; padding: 14px 0; border-bottom: 1px solid var(--rule);
}
/* The two-column card (counter gutter + body) lives on .action-body, not the <li>, so the <li> is a
   plain block whose height we CAN animate on collapse (a display:list-item or a display:grid box
   ignores its own max-height). The counter stays in normal flow inside the body's first column, so it
   still increments per card and aligns to the title on the same row. The buttons float top-right over
   the first line, out of the flow, so the title and the email's first lines run full width beneath
   them rather than stopping short of the cluster. No `rise` entrance: the desk re-fetches after every
   action, and re-inserted rows replaying rise (translateY(14px)→0) is what read as the whole list
   jumping when one card finished. New rows now simply appear; a completion is animated by the collapse
   + FLIP instead (see app.js collapseActionCard / flipRows). */
.action-item > .action-body {
  display: grid; grid-template-columns: 28px 1fr; gap: 4px 14px; align-items: start;
}
.action-item > .action-body > .action-buttons { position: absolute; top: 14px; right: 0; }
.action-item > .action-body > .action-body-inner { grid-column: 2 / -1; }
.action-item > .action-body > .action-body-inner::before {
  counter-increment: act; content: counter(act, decimal-leading-zero);
  position: absolute; left: 0;
  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; }
/* The settle-away for a card the user just finished (Done or Reject). The card is a <li>
   (display:list-item), which cannot animate its own max-height/padding — that is exactly why an
   earlier collapse "just disappeared". is-collapsing therefore FIRST makes it a plain block, then
   drives its pinned height + padding to 0 so its bottom edge creases up to its top like a roll-up
   blind until nothing is left and JS drops the node. The fade is deliberately gentle (→~0.25): the
   SHRINK is the motion the eye reads, so the text stays legible as it compresses rather than vanishing
   before it has creased. Rows below glide up via FLIP (app.js flipRows), not a re-render jump. */
.action-item.is-collapsing {
  display: block;
  overflow: hidden;
  transform-origin: top center;
  transition: opacity 0.2s var(--ease), max-height 0.4s cubic-bezier(0.32, 0, 0.24, 1),
              padding 0.4s cubic-bezier(0.32, 0, 0.24, 1), border-color 0.4s var(--ease);
  max-height: 0;
  opacity: 0.25;
  padding-top: 0;
  padding-bottom: 0;
  border-bottom-color: transparent;
}
.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; }

/* The card's flag verb (docs/27 D1). An icon button sized to the done circle, so the cluster's
   centring rule above holds without another line: same box, same white chip stock as every other
   control in the row (the done circle's own house stock - see --check-bg - which is also what the
   Pressroom sheet lifts the ghost buttons onto), and vermilion only when the mail carries the
   flag - the app's single attention colour doing attention's work. */
.action-buttons .flag {
  width: 30px; height: 30px; border-radius: 50%; border: 1px solid var(--btn-neutral-border); background: var(--check-bg);
  cursor: pointer; font-size: 15px; line-height: 1; color: var(--ink-3);
  display: inline-grid; place-items: center; align-self: center; transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.action-buttons .flag:hover { border-color: var(--ink); color: var(--ink); }
.action-buttons .flag.is-flagged { color: var(--vermilion); border-color: var(--vermilion); }

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

/* ---------- Priority rail groups (Today right column) ---------- */
/* No margin on the section itself: .column-side > section + section already spaces every rail
   group (32px) with zero added work, and the folded head is exactly Critical's head at the same
   distance from the group above it. The head is Critical's own
   furniture - a real .column-head with a plain h2 on the hairline, at the same size and the
   same ink - so the three priorities read as one kind of thing, folded or open. The fold's
   furniture is only the caret: it rides the head's far end where "See all" sits for Critical,
   and a folded group is a head whose rows are simply not printed. The count sits inside the
   h2, after the label, at a muted step down - it belongs to the title, not to the rule. */
.priority-rail summary { display: block; cursor: pointer; list-style: none; }
.priority-rail summary::-webkit-details-marker { display: none; }
.priority-rail summary .column-head { margin-bottom: 0; }
.priority-rail summary:hover h2 { color: var(--ink); }
/* 22px (~2x the snoozed group's ~11px glyph). The 44px pass read too big beside the 20px
   title and was reverted the same session - the caret steps up to head scale, it does not
   out-shout the headline. transform is spoken for by the rotate rule below, so the step is a
   font-size, not a scale. */
.priority-rail .caret { transition: transform 0.2s var(--ease); color: var(--ink-3); font-size: 22px; }
.priority-rail details[open] summary .caret { transform: rotate(90deg); }
.priority-rail details[open] summary .column-head { margin-bottom: 8px; }
.priority-rail-count { font-family: var(--font-mono); font-size: 11px; color: var(--ink-3); font-weight: 400; }
.priority-rail .mini-list { margin-top: 0; }

/* ---------- 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; }
/* Unread (docs/34): a stripe like Critical's, but slate not vermilion - vermilion means "critical",
   and two red stripes on the same screen would blur the one distinction that matters. The stripe
   plus the weight carries read/unread at a glance (weight alone read as unread-but-easy-to-miss);
   the slate accent ink is already in the palette and clearly not the danger colour. */
.email-row.is-unread { border-left: 3px solid var(--slate); padding-left: 11px; }
.email-row.is-unread.is-critical { border-left-color: var(--vermilion); }
/* Unread (feedback #2): the desk mirrors Outlook, so unread mail must look unread. Weight, not
   a dot: a dot would be decoration, while the heavier subject is the mailbox's own grammar.
   Critical stays heavier still - the vermilion border and the tag already say critical, and
   the two states must never cancel each other out. */
.email-row.is-unread .email-subject { font-weight: 600; }
.email-row.is-unread .email-when { color: var(--ink-2); }
.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; }
/* A meeting invitation / reply / update carries a small calendar glyph before its subject (docs/38 s.3.24), so it
   reads as a meeting at a glance rather than as ordinary mail. Inline, so the subject keeps its width and lines. */
.email-meeting-icon { display: inline-block; width: 0.85em; height: 0.85em; margin-right: 8px; color: var(--slate);
  vertical-align: -0.04em; flex: none; }
.email-meeting-icon svg { display: block; width: 100%; height: 100%; }
.email-meeting-icon.is-response { color: var(--ink-3); }
.email-sender { font-size: 13px; color: var(--ink-2); min-width: 0; }
/* The second line: tags first, then the sender's name and address (docs/38 s.3.22). The subject keeps its full
   width (s.3.19: a right-hand tag column cost it width and made rows taller); this line wraps, so the sender
   drops under the chips only when it genuinely cannot fit beside them. */
.email-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; }
.email-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; flex: none; }
.email-chips:empty { display: none; }
.email-summary { font-size: 14px; color: var(--ink-2); margin: 6px 0 0; max-width: 80ch; }
/* The row's right-hand column is now only the Reply button(s). Its right edge is the row's right edge, so it is
   inset the way the old tag cluster was (the tooltip anchored to it must not overlap the content border).
   Mobile drops it to the left of a single-column row, where the inset is pointless. */
.email-row > .email-actions { 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); }
/* The five draft buttons (Approve & send, Send to Outlook drafts, Save edits, AI Regenerate, Reject) are meant to
   sit on ONE row. They need ~573px at the default spacing, which the drawer's card (~556px inside) does not
   have, so the second group wrapped onto its own line. Tighter padding and gaps bring that to ~578px of card
   width without shrinking the type (measured in headless Chromium, tests/test_draft_footer_layout.py); a
   genuinely narrow card still wraps rather than clipping. */
.draft-foot { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 12px 16px 16px; flex-wrap: wrap; }
.draft-foot .group { display: flex; gap: 6px; flex-wrap: wrap; }
.draft-foot .btn-sm { padding: 7px 10px; }
.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); }
/* Cases (docs/25 §7.2). A card is a standing question, so it borrows the draft/action card skin and
   the .link primitive, and deliberately NOT the due-date grammar: the horizon reads as a quiet line,
   never the vermilion of lateness, because silence is the healthy state, not a missed deadline. */
.case-list { display: flex; flex-direction: column; gap: 14px; }
.case-card {
  background: var(--paper-raised); border: 1px solid var(--rule); border-radius: 6px; box-shadow: var(--shadow);
  padding: 16px 20px; animation: rise 0.45s var(--ease) both;
  display: flex; gap: 16px; justify-content: space-between; align-items: flex-start; flex-wrap: wrap;
}
.case-card.is-expired { border-left: 3px solid var(--moss); }
.case-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.case-reason { font-size: 15px; font-weight: 600; color: var(--ink); }
.case-why { font-size: 13px; color: var(--ink-2); }
.case-criterion { font-size: 13px; color: var(--ink-2); }
.case-criterion-label { font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-3); }
.case-meta { display: flex; flex-wrap: wrap; gap: 12px; font-size: 12px; color: var(--ink-2); font-family: var(--font-mono); }
.case-status { text-transform: uppercase; letter-spacing: 0.04em; }
.case-status-develops { color: var(--moss); }
.case-status-expired { color: var(--vermilion); }
.case-status-retired, .case-status-cancelled { color: var(--ink-3); }
.case-horizon.is-expired { color: var(--vermilion); }
.case-actions { display: flex; flex-wrap: wrap; gap: 8px; align-content: flex-start; }
.btn-danger-ghost { color: var(--vermilion); }
.case-log { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; }
.case-event { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; font-size: 13px; padding-top: 6px; border-top: 1px dashed var(--rule); }
.case-event-r { color: var(--ink-2); font-style: italic; }
.case-event time { margin-left: auto; color: var(--ink-3); font-family: var(--font-mono); font-size: 11px; }
.case-verdict { font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
.case-verdict-certain { color: var(--moss); }
.case-verdict-lead { color: var(--ink-3); }
/* The forgetting control and its privacy note sit below the list, quiet and low-priority - the bulk
   delete must read as available but never as the point of the screen (docs/25 §6.3/§7.3). */
.cases-footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 20px; padding-top: 14px; border-top: 1px solid var(--rule); }
.cases-footer .case-privacy { flex: 1; font-size: 12px; line-height: 1.5; margin: 0; }
/* Two optional fast-path fields share one row in the + Add case dialog, so they read as extras
   beneath the two required fields rather than two full-width "optional" inputs. Scoped to the
   dialog so it cannot collide with a .field-row elsewhere. */
.rename-dialog .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
/* 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%); }
/* File-as-case and + Add case: 150% of the 420px shell so a multi-line story-so-far (title +
   details) is readable without scrolling sideways. Both dialogs share one width: filing from a
   card and adding a case are the same act seen from two screens. */
.rename-dialog.case-dialog { width: min(630px, 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); }
/* docs/37: a Reply button on mail that REQUIRES a reply is drawn in the action colour (vermilion,
   the same ink the critical marker uses) instead of the neutral draft-from-scratch green. It folds
   the old static "reply" verdict tag into the button, so one control says both "owed a reply" and
   "click to write it" - and two "reply" words no longer double up on one row. The hover keeps the
   same moss-free treatment so the button never flickers green under the pointer. */
.email-act.email-act-reply {
  background: var(--vermilion-soft); color: var(--vermilion);
}
.email-act.email-act-reply:hover, .email-act.email-act-reply:focus-visible {
  border-color: var(--vermilion);
}

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

/* ---------- Block-time dialog (Today's "Block time") ------------------------
   The availability windows are offered as a vertical list of one-tap choices, so the
   weekday+start time reads first and the length trails it. Stacks full-width rather than
   wrapping like the snooze presets, because each choice is a distinct slot to compare against
   the next, not a guess-word to scan. */
.block-time-dialog { width: min(460px, 100%); }
.slot-choices { display: flex; flex-direction: column; gap: 8px; }
.slot-choices .slot-choice { justify-content: flex-start; text-align: left; font-variant-numeric: tabular-nums; }

/* ---------- 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; }
/* Keeps its width so Reset lines up with the other rows; see settingRow (a switch saves when moved). */
#settings-table .row-actions .btn.is-unused { visibility: hidden; pointer-events: none; }
/* 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 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: 176px; }
#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; }
/* Air between sections (feedback #17): the band caption used to sit flush on the previous
   group's last row. This table is collapsed (.table), and a collapsed table ignores any
   border-spacing - so instead add explicit margin above every band row. .setting-group-row is
   the first row of each tbody (app.js renderSettingsTable) and the Microsoft card band sits
   directly under its caption, so one top margin there is one gap per section break. */
.setting-group-row td { padding-top: 28px; }

/* ---------- 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; }
/* A draft/send reuses the action row's grid (same columns: time, chip, title, context, status,
   when); it omits the block/draft spans so it carries five cells, but the grid leaves the gaps
   empty rather than misaligning. A sent row gets a left accent so an outbound message reads at a
   glance from a drafted one. */
.history-draft { 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-left: 3px solid var(--rule); border-radius: var(--r); background: var(--card); }
.history-draft:has(.tag-sent) { border-left-color: var(--moss); }
@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; }
  .history-draft { grid-template-columns: auto auto minmax(0, 1fr); }
  .history-draft .history-action-context { grid-column: 3 / -1; }
  .history-draft .history-action-status, .history-draft .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; }
/* Status pill on Today's action cards ("booked 14:00" / "proposed 09:30") — links to Schedule.
   It sits in the card's meta line (the row already carrying priority, due date, duration and the
   notes link) rather than in the floating button cluster: as a button-cluster item it made that
   absolute row wider than the .action-title's reserved padding-right, so the chip painted over the
   end of the title. In the meta line it wraps with the other facts and can never reach them.
   Deliberately NOT .btn: the button-cluster alignment test selects `.action-buttons .btn` by
   position, and a chip in it would shift that measurement. */
.block-chip {
  font-family: var(--font-mono); font-size: 11px; text-decoration: none; white-space: nowrap;
  padding: 2px 8px; border-radius: 999px; border: 1px solid var(--rule); color: var(--ink-3);
}
.block-chip:hover { border-color: var(--ink-3); color: var(--ink); }
.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; }
/* The foot strip (`.agenda-freeslots`/`.free-chip`) retired with the design it came from: free
   gaps now render in place as `.agenda-gap` rows between the cards they sit between. The meeting
   composer's own slot chips (`.slot-chips`) are a different affordance and stay. */
/* A free gap rendered in place, between the cards it actually sits between. It borrows the card
   grid's time column so the clock lines up with every meeting's, and it is a button because the
   whole row is the click target - the affordance is "book here", not "read that a hole exists".
   It is quiet at rest - hairline text on paper, a dashed rule underneath where a commitment's solid
   one sits - and warms on hover/focus to the moss the booked state earns, promised one step early.
   It deliberately is NOT an .agenda-card: no raised paper, no shadow, nothing that claims a
   commitment exists. The box metrics are an EVENT card's own (14px padding, 140px/18px time column,
   no left rule) so its clock lands on the same column a meeting's does; a block card spends 3px on
   its status rule, which is why the row must not spend it here - the affordance is the background,
   not a left border, or the day's one time column stops being one. */
.agenda-gap {
  display: grid; grid-template-columns: 140px minmax(0, 1fr); gap: 18px; align-items: center;
  width: 100%; text-align: left; cursor: pointer;
  background: none; border: 0; border-bottom: 1px dashed var(--rule);
  padding: 14px 16px 14px 14px; font: inherit; color: var(--ink-3);
  transition: background .18s var(--ease), color .18s var(--ease);
}
.agenda-gap:hover, .agenda-gap:focus-visible { background: var(--moss-soft); color: var(--moss); }
.agenda-gap:focus-visible { outline: 2px solid var(--moss); outline-offset: -2px; }
.agenda-gap:active { background: var(--moss); color: var(--paper); }
.agenda-gap-when { font-family: var(--font-mono); font-size: 12px; }
.agenda-gap-label { font-size: 13px; color: inherit; }
.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 layout ----------
   A time grid: hours down the side, seven day columns, each commitment painted at the height it
   occupies. The month cell's visual language is borrowed where the two layouts genuinely agree
   (paper, the mono day number, the ellipsis on a title); it is dropped where they do not - the
   week is no longer a list of cells to stack, so nothing about it sizes by content. */
.week-wrap { display: grid; gap: 12px; }
/* Text for the accessibility tree only. A week slot is a 30px-tall card of clipped text, so the
   sentence that names it properly ("Meeting, 09:00 – 09:45: Project standup") has nowhere to be
   painted; it rides along on the element via aria-labelledby and lives here.
   clip-path (rather than the older clip: rect()) with position:absolute keeps the node in the tree
   and focusable-safe without ever occupying layout - it must not add a line to a slot whose height
   is already exactly its duration. */
.sr-only {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
.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; }

/* One grid for the whole layout: an hour-label track and seven day tracks, the day headers on row 1
   and the 24-hour track on row 2. The hour lines, the headers and the columns are items of THIS grid,
   which is what makes a header and its column the same track by construction - the alternative
   (seven columns each drawing its own rules) drifts the moment one column's content differs.
   minmax(0, 1fr) for the same reason as .month-grid: a track's floor is its content's min-content
   width, so one long unbreakable title would otherwise widen its own column past its six neighbours.
   max-height + overflow makes row 2 the scroll container; sticky headers (below) then pin against
   THIS box, not the window, which is what keeps them over their own columns while the hours scroll.
   The cap is a floor-free viewport height: the chrome above (top bar, toolbar) is sticky, so the
   grid gets the rest of the window rather than the whole of it. */
.week-grid {
  display: grid;
  grid-template-columns: var(--week-hour-w) repeat(7, minmax(0, 1fr));
  grid-template-rows: auto repeat(24, var(--week-hour-h));
  column-gap: 0; row-gap: 0;
  position: relative;
  max-height: calc(100vh - var(--week-chrome-h));
  overflow-y: auto; overscroll-behavior: contain;
  /* The grid's own surface, plus ONE vertical rule dividing the hour axis (column 1) from the day
     columns. Drawn as a background line on the scroll box itself rather than per hour row: it spans the
     full content height (auto backgrounds scroll with the content), so the divider never breaks at an
     hour boundary and never participates in the rows' sizing. */
  background-image: linear-gradient(
    to bottom, var(--rule) 0, var(--rule) 100%);
  background-size: 1px 100%;
  background-position: calc(var(--week-hour-w) - 1px) 0;
  background-repeat: no-repeat;
  background-attachment: local;
  background-color: var(--paper-2);
  border: 1px solid var(--rule); border-radius: var(--radius);
}
.week-cellbox { min-width: 0; }

.week-corner {
  grid-area: 1 / 1 / 2 / 2; position: sticky; top: 0; left: 0; z-index: 4;
  display: flex; align-items: center; justify-content: center; padding: 6px;
  background: var(--paper); border-bottom: 1px solid var(--rule); border-right: 1px solid var(--rule);
}
/* The axis is in the user's zone; the page says so once, here, rather than on every hour label and
   every block. Half the confusion this app has been reported for is "is this my time or Outlook's".
   Centered on both axes (see .week-corner's align-items/justify-content) rather than bottom-pinned -
   the corner cell is a fixed height and short, so centring it vertically reads as a label sitting
   "in" the corner the way the hour numbers now sit centred in the gutter column below it, not
   hanging off one edge. shortZoneLabel is NOT truncated to a 3-letter code: IATA airport codes
   (SYD) and 3-letter timezone abbreviations (AEST/AEDT, which also shift with DST) are two different
   things, neither reliably derived from an IANA zone id, and neither is what a person actually reads
   - "Sydney" is. A long name ("New York", "Los Angeles", "Santa Isabel") instead ellipsises off the
   full name via text-overflow below, which stays legible and honest to the real zone rather than
   inventing a code nobody recognises. */
.week-corner-tz {
  font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: center;
}

/* One header per day track - the same single track as the column below it, which is what centres the
   day's name and number over the column they name. It does NOT span two: with a span, the header's
   centred content (justify-content: center) sits over the seam between two columns, so the name reads
   like it belongs to Friday while its column is Saturday. A one-cell header is safe here because row 1
   is otherwise empty apart from the corner (column 1), and these seven are placed with a definite
   column each, so there is no auto-placed neighbour to shove one sideways. */
.week-dayhead {
  grid-row: 1 / 2;
  position: sticky; top: 0; z-index: 3; cursor: pointer;
  display: flex; align-items: baseline; justify-content: center; gap: 6px;
  padding: 6px 4px; font: inherit; color: inherit;
  background: var(--paper); border: 0; border-bottom: 1px solid var(--rule); border-right: 1px solid var(--rule);
}
/* The LAST day is the grid's rightmost item, so the box's own border closes it; a rule here would sit
   beside it as a doubled line. With sideways panning on a phone the same holds only when nothing is
   scrolled past, so the narrow ruleset below gives every header its edge and takes it back off the
   corner one by other means. */
.week-dayhead:last-of-type { border-right: 0; }
.week-dayhead:hover { background: var(--card-hover); }
.week-dayhead.is-today { box-shadow: inset 0 -2px 0 var(--ink); }
.week-daynum { font-family: var(--font-mono); font-size: 13px; color: var(--ink-2); }
.week-dayhead.is-today .week-daynum { color: var(--ink); font-weight: 700; }
/* The day's row count, kept from the survey: the week can no longer show every commitment at once,
   so a column with three things painted in it may hold eleven, and silence would read as "quiet
   day". It is the only number that answers "how full is Thursday" without scrolling. */
.week-daycount {
  font-family: var(--font-mono); font-size: 9px; color: var(--ink-3);
  border: 1px solid var(--rule); border-radius: 999px; padding: 0 4px; align-self: center;
}

/* Hour rows: 24 rows of --week-hour-h labelled in the first track. One .week-hourline per hour is the
   unit of BOTH the layout and the geometry tests (each is measured for height and counted), so the row is
   the axis rather than a decorative overlay. A subgrid would let the number and the right-edge tick line
   up with the day tracks without repeating the track list - but a sticky `top` on a subgrid ITEM is
   measured against that 56px row's own box, not the scroll container, so a label pinned to the top of the
   gutter never moves and the whole grid scrolls under it; worse, a per-hour opaque gutter background
   tiled all twenty-four 56px rows into one solid column that hid the number sliding past its neighbour.
   So each hour row is a plain grid item with its own track list (the gutter plus seven day tracks, the
   same widths .week-grid uses) and draws its hour rule with one border-top; the number is a text label
   in the gutter. This is the design that measured correct in the browser (hour rules cross the grid, the
   tick pins to the right edge, the gutter stays legible); the pinned-ruler idea that "keeps a live time
   at the top while you scroll" is what the subgrid and the tiled background both silently defeated, so
   the hour you are reading sits beside the block whose rule you are tracing. */
.week-hourline {
  /* A definite row range so the hour rows flow into rows 2..25 under the headers. grid-row alone leaves
     the column unresolved and the auto-placement algorithm drops a row into row 1's cells (the corner and
     the seven headers), which put a weekday name over the wrong day once - naming the column explicitly
     removes the ambiguity. -1 is the grid's last line; --week-hour-w is the gutter, so an axis row never
     claims a day track's width. */
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: var(--week-hour-w) repeat(7, minmax(0, 1fr));
  align-items: center;
  position: relative;
  height: var(--week-hour-h); border-top: 1px solid var(--rule);
}
.week-hourlabel {
  grid-column: 1 / 2; justify-self: center; align-self: start; translate: 0 -50%;
  font-family: var(--font-mono); font-size: 13px; color: var(--ink-2);
}
.week-hourline .week-hourlabel {
  position: sticky; left: 0; z-index: 2;
  background: var(--paper-2);   /* matches the grid surface so a slot never bleeds through behind it */
}

/* The right-edge mark, on the SAME boundary as the number and the row rule. Like the ::after gutter
   divider beside it, it is positioned absolutely against the relative hour row: `top: 0` lands its border
   on the row's top edge, `right: 0` puts it at the far edge of the day tracks. Absolute (not sticky) is
   deliberate - a sticky grid item fights the row's align-items and the scroll container's two axes at
   once and parks mid-row, whereas the absolute box is deterministic. It scrolls with its hour, which is
   right: the tick belongs to the boundary it marks, and on a phone panning sideways each row carries its
   own mark to the trailing edge. */
.week-hourtick {
  grid-column: 1 / -1; justify-self: end;
  position: absolute; top: 0; right: 0; z-index: 2;
  width: 12px; border-top: 1px solid var(--rule);
}
/* The vertical line the user expects between the hour axis and the first day column. It is drawn per
   hour row so it scrolls with the hour scale and never leaves a gap. z-index: 0 puts it below the day
   columns in the grid's pooled layer (.week-grid is not itself a stacking context, so every positioned
   element out here competes on one level regardless of the isolation inside .week-col - that isolation
   only caps a day's OWN children, it does nothing against a sibling grid item like this row's own rule).
   The horizontal hour rule never had this problem - an element's own background/border is painted before
   its positioned children join a comparison, so the columns already won it; a positioned ::after is
   different, and at any positive z-index this divider out-ranked every column and painted straight down
   the middle of whichever day's track it crossed, reading as a block split in two. */
.week-hourline::after {
  content: ""; position: absolute; top: 0; bottom: 0;
  left: calc(var(--week-hour-w) - 1px); width: 1px; background: var(--rule); z-index: 0;
}
.week-col {
  grid-row: 2 / -1; position: relative;
  /* An explicit track height rather than a stretch, so the box the commitments are measured against
     is a number the sheet states - `calc(24 * var(--week-hour-h))`, and the hour rules are 24 rows of
     the same variable. A stretch would let a phone's `align-items: start` hour line collapse the row
     to nothing once the columns held absolutely-positioned children, which hold nothing open. */
  height: calc(var(--week-hour-h) * 24); border-right: 1px solid var(--rule);
  /* This box, not the grid, is where the day's commitments are stacked against one another. The slots
     carry z-index by start time (see --wk-i), and .week-grid is NOT a stacking context - so without
     this, grid items that aren't themselves stacking contexts (the columns) let their positioned
     children join the grid's POOLED z-index layer. A third overlapping commitment reaches z-index 3,
     which is exactly the sticky day header's, and the header loses - an evening block paints over the
     day name as you scroll. Isolating each column caps a day's internal ordering INSIDE that column,
     so no block can out-rank anything outside it: the header (a grid item at z-index 3) and the
      now-line (z-index 5) then always sit above the whole day's paint, whatever that day's stacking. */
   isolation: isolate;
    /* The column doubles as a "click an empty time to book it" target - see the click handler in
       weekDayColumn (app.js). Slots are real <button>s (see weekSlot) and consume their own clicks, so
       a click landing directly on this box can only be empty track.
       crosshair + the hover ghost (see .week-hover-ghost below) read together: the cursor says "you can
       place something here", and the ghost box snaps to the 15-minute grid and shows exactly where a
       click would land. Together they are the affordance for creating a meeting. */
     cursor: crosshair;
}
.week-col:last-child { border-right: 0; }
/* Today is boxed rather than washed. --paper-3 is a strong step in Morning and a much fainter one in
   After Dark, so a background alone would mark the day hard on one edition and barely at all on the
   other; an --ink hairline is the same visible step against the grid's surface in both. The now-line
   below does the second half of the job - it says where the day has got to. */
.week-col.is-today { box-shadow: inset 0 0 0 1px var(--ink-2); }

/* A commitment, painted where it happens. Absolute inside its column, top/height as a percentage of
   the 24-hour track - the numbers come from app.js and are the same arithmetic the hour lines use, so
   a block's edge and the hour rule beside it cannot disagree.
   Width is NOT the full column: overlapping commitments share it. app.js measures the day and writes
   each slot's lane (--wk-lane, zero-based) and how many the overlap cluster is widest at (--wk-lanes).
   One width and one offset divided by that count puts every overlapping commitment beside its
   neighbours in the SAME column width, so a crowded morning shrinks its cards without the column ever
   growing past its track - and a non-overlapping pair (--wk-lanes: 1) keeps the whole column. The day's
   column stays the containing block and the stacking context (see .week-col's isolation), so nothing a
   day paints can rise over the sticky header above it. */
.week-slot {
  --wk-lane: 0;
  --wk-lanes: 1;
  position: absolute;
  left: calc(var(--wk-lane) * (100% / var(--wk-lanes)) + 1px);
  width: calc(100% / var(--wk-lanes) - 3px);
  display: flex; flex-direction: column; gap: 1px; overflow: hidden; text-align: left;
  padding: 3px 5px; font: inherit; color: var(--ink); cursor: pointer;
  background: var(--paper-raised); border: 1px solid var(--rule); border-left: 3px solid var(--slate);
  border-radius: 3px; line-height: 1.25;
}
button.week-slot { -webkit-appearance: none; appearance: none; }
.week-slot:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }
.week-slot-title {
  font-size: 11.5px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
}
/* Hover card: a fixed-position popover that shows the full title of a clamped slot, sitting above the
   grid and escaping every stacking constraint (.week-col's isolation, lane widths, overflow: hidden).
   The slot is a button with overflow: hidden and absolute-positioned geometry tied to the time track,
   so a CSS :hover growth can never truly "pop out" — it needs to leave the column entirely. The card is
   positioned by JS (app.js: hoverSlotCard) using getBoundingClientRect, so it tracks the slot's visual
   position without inheriting its transform/overflow/lane constraints.
   z-index 40 sits above the grid's pooled layer (day-header 3, now-line 5) but below modals/drawers (40
   and above), matching the layer other non-blocking overlays use. */
.week-slot-hover-card {
  position: fixed; z-index: 40; pointer-events: none;
  max-width: 320px;
  padding: 6px 10px;
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-left: 3px solid var(--slate);
  border-radius: 3px;
  box-shadow: var(--shadow);
  line-height: 1.3;
  font: inherit;
  color: var(--ink);
}
.week-slot-hover-card .week-hover-time {
  display: block;
  font-size: 11px;
  font-family: var(--font-mono);
  color: var(--ink-3);
}
.week-slot-hover-card .week-hover-title {
  display: block;
  font-size: 12.5px;
}
/* A commitment's own time line is gone: the hour axis is the clock, so the block's top edge already
   says "09:00" and a painted time repeated it while eating the row a long title needs. The time is not
   lost - it is the hover tooltip and the sr-only sentence - it just no longer takes vertical space. A
   15-minute box (14px desktop, 11px phone) now has room for its title alone, which is the whole reason
   the row was worth reclaiming. */
.week-slot.is-cancelled { text-decoration: line-through; color: var(--ink-3); border-left-color: var(--rule); }
/* A triage block's own outline is dropped (transparent, not --rule): with one fewer line crossing the
   shape, an hour rule passing through it has less to sit on top of. The bar keeps its status colour, so
   this never makes a block read like a meeting. The fill is --slate-soft/--amber-soft etc, all of which
   are low-alpha rgba washes - on their own they let the hour rule and the grid's own background show
   through, which is what makes an hour line read as painted ACROSS the block instead of behind it. They
   are therefore layered over --paper-raised (the same opaque base a meeting's card uses) so each block
   is a fully opaque surface, and only the rule is behind it - the same promise a meeting already makes. */
.week-slot.is-block { border-color: transparent; background: linear-gradient(var(--slate-soft), var(--slate-soft)), var(--paper-raised); }
.week-slot.is-block.block-proposed { border-left-color: var(--amber); background: linear-gradient(var(--amber-soft), var(--amber-soft)), var(--paper-raised); }
.week-slot.is-block.block-booked { border-left-color: var(--moss); background: linear-gradient(var(--moss-soft), var(--moss-soft)), var(--paper-raised); }
.week-slot.is-block.block-stale { border-left-color: var(--vermilion); background: linear-gradient(var(--vermilion-soft), var(--vermilion-soft)), var(--paper-raised); }
.week-slot.is-status-accepted { border-left-color: var(--moss); }
.week-slot.is-status-declined { border-left-color: var(--rule); opacity: 0.6; }
.week-slot.is-status-tentativelyAccepted { border-left-color: var(--amber); }

/* Hover ghost: a 15-minute preview box that follows the pointer over empty track, showing the user
   exactly where a click would create a meeting. Absolute inside .week-col (which is already the
   containing block), positioned by top% the same way the real slots are. The dashed vermilion border
   and --vermilion-soft tint (10% red) read as "an empty slot you could fill" without looking like a
   real commitment or a calendar conflict - vermilion is also the composer dialog's CTA color, so the
   hover and the resulting action share a visual language. Sits under everything the user might be
   reading: the now-line (z-index 5), the day headers (z-index 3), and any real slot already on that
   row. pointer-events: none so the ghost itself never intercepts the mousemove that drives it, or the
   click that fires the composer. */
.week-hover-ghost {
  position: absolute; left: 1px; right: 2px; height: var(--week-slot-h);
  top: 0; /* overridden inline by weekDayColumn's onmousemove */
  background: linear-gradient(var(--vermilion-soft), var(--vermilion-soft)), var(--paper-raised);
  border: 1px dashed var(--vermilion);
  border-radius: 3px;
  pointer-events: none;
  z-index: 0;
}

/* Now, across the whole week. A grid you scroll inside is exactly the layout that hides whether
   today's meeting has already happened, so the line is drawn once, across all seven columns, and the
   today column is boxed so the eye finds the right one first. */
.week-now {
  grid-row: 2 / -1; grid-column: 2 / -1; position: relative; pointer-events: none;
  height: calc(var(--week-hour-h) * 24);
}
.week-now-line {
  position: absolute; left: 0; right: 0; height: 0; border-top: 1.5px solid var(--vermilion); z-index: 5;
}
.week-now-dot {
  position: absolute; left: -4px; top: -4px; width: 7px; height: 7px; border-radius: 50%;
  background: var(--vermilion);
}

/* ---------- Schedule: month overlay ---------- */
/* The month is locked to the viewport the way the week's grid is (see .week-grid's cap): the
   survey view is the whole point of the tab, and the window chrome above it is fixed-height, so
   the grid gets every pixel left - no page scroll, no dead strip under the last row. */
.month-wrap { display: grid; gap: 0; min-height: 0; padding-bottom: var(--month-pad-y); }
/* With the survey on screen the page must not scroll under the grid: the toolbar, the month
   changer, and the title are the controls the survey is read *with*, and a page scroll slid them
   off the top while the table that needed looking at scrolled in their place. The week view never
   had this problem - its grid scrolls itself inside a viewport cap (.week-grid's max-height) - and
   the month now answers the same contract: the grid fits its band exactly, so there is nothing
   left over to scroll, and this rule is the backstop for any rounding left over anyway. */
body:has(.view.is-active .month-wrap:not([hidden])) { overflow: hidden; }
/* .app-main reserves 80px under every page. The month page already ends in its own --month-pad-y of
   air, so that reserve is cancelled here: left in, it made the page 80px taller than the window - a
   strip below the grid that the lock above stops you scrolling to, and that the browser still counts
   when it restores a scroll position. */
.app-main:has(.view.is-active .month-wrap:not([hidden])) { padding-bottom: 0; }
.month-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: var(--month-head-gap); }
.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); }
/* The week grid tells meeting and triage block apart by the slot itself, not by a glyph: a meeting is
   the neutral slate, a triage block is the planning amber (the --amber left bar and --amber-soft wash
   its proposed/booked states already wear). The old ●/◆ key taught shapes the week never paints, so the
   key now shows a colour chip per kind - slate for a meeting, amber for a block - the same hues that
   carry the distinction on the grid, so the legend is a picture of the thing rather than a symbol
   standing in for it. */
.week-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-swatch {
  width: 14px; height: 11px; border: 1px solid var(--rule); border-left: 3px solid var(--slate);
  border-radius: 2px; flex: 0 0 auto;
}
.legend-swatch.is-meeting { background: var(--paper-raised); border-left-color: var(--slate); }
.legend-swatch.is-block { background: var(--amber-soft); border-left-color: var(--amber); }
.month-hint { font-size: 12px; margin: 0 0 0 auto; white-space: nowrap; }
/* The month is ONE ruled table, the way the week grid is: eight explicit tracks - a gutter for the
   week number, then the seven days at equal width - with no gap anywhere. The rules between cells are
   the cells' own right/bottom borders (the week's recipe) and the grid's own border closes the
   outside, so it reads as a single table rather than a field of tiles.
   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.
   Row 1 is the column titles on a line of their own (the corner "Wk" over the week numbers, then
   the full weekday names); each week below opens with its number. Placement is never left to auto:
   every item names its track, and a test walks the rows to prove each week really is eight items. */
.month-grid {
  display: grid;
  grid-template-columns: var(--month-gutter) repeat(7, minmax(0, 1fr));
  column-gap: 0; row-gap: 0;
  /* The title row is max-content, NOT auto: its cells clip their overflow, so an auto track's
     minimum is 0 and, in a window too short for the weeks, it collapsed to a sliver and the sticky
     titles sat on top of the first week's day numbers. max-content pins it at its text height.
     Then one track per week the month really has (--month-rows,
     set by loadMonth: 4-6). A fixed seven left empty tracks under the last week of a five-week month -
     the dead strip - because the grid fills its band whatever it holds. */
  grid-template-rows: max-content repeat(var(--month-rows, 6), minmax(var(--month-cell-h), 1fr));
  /* The band the survey gets: the window, minus everything the page spends above it (top bar, the
     "Calendar" head with its rule, the app-main padding) as the --month-chrome-* tokens, minus the
     air above and below the grid. NOT the week grid's budget, which also carries the agenda list
     the survey hides. The grid fills the band to the pixel, so the last row ends where the bottom
     padding begins: no dead strip, and the PAGE never scrolls. When the window is too short for every
     week to keep its minimum row height (--month-cell-h), the rows hold that floor and the TABLE
     scrolls inside itself - the week grid's own arrangement - with the title row pinned. */
  height: calc(100vh - var(--month-chrome-h) - var(--month-head-gap) - var(--month-pad-y));
  overflow-y: auto; overscroll-behavior: contain;
  /* Rows are fr tracks of a definite height, so every week resolves equal by construction - the
     one thing this grid has been tested for since it shipped broken twice. */
  align-content: stretch;
  background: var(--paper-2);
  border: 1px solid var(--rule); border-radius: var(--radius);
}
/* Line map: 1 gutter | 2..8 the seven days. Row 1 is these titles. They are the week's header
   strip exactly - paper fill, centred, hairline rules right and below - so the month and the week
   are one style, not two. The corner titles the week-number column. These are direct children of
   the grid and always its first eight, which is what the :nth-child lines below rely on (the rig
   builds them with the app's own monthHead). .month-dow is NOT used here: the week's day headers
   already own that class. */
.month-corner, .month-colhead {
  display: flex; align-items: center; justify-content: center; min-width: 0;
  padding: 6px 4px; overflow: hidden; white-space: nowrap;
  position: sticky; top: 0; z-index: 2;   /* pinned while the table scrolls under it */
  background: var(--paper); border-bottom: 1px solid var(--rule); border-right: 1px solid var(--rule);
  font-family: var(--font-mono); text-transform: uppercase; color: var(--ink-3);
}
.month-corner { grid-column: 1; font-size: 9px; letter-spacing: 0.06em; }
.month-colhead { font-size: 11px; letter-spacing: 0.08em; }
.month-colhead:nth-child(2) { grid-column: 2; }
.month-colhead:nth-child(3) { grid-column: 3; }
.month-colhead:nth-child(4) { grid-column: 4; }
.month-colhead:nth-child(5) { grid-column: 5; }
.month-colhead:nth-child(6) { grid-column: 6; }
.month-colhead:nth-child(7) { grid-column: 7; }
.month-colhead:nth-child(8) { grid-column: 8; border-right: 0; }
.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;
}
/* The week number and the column titles are spans, so nth-of-type below counts only the button
   day cells - exactly the seven-per-week rhythm these rules assume. Every cell names its own
   track: 2..8, one per day. There is deliberately NO bare `.month-cell { grid-column }`: a base
   rule lower in the cascade would re-land every day on one column (the staircase this grid kept
   growing). */
.month-cell:nth-of-type(7n+1) { grid-column: 2; }
.month-cell:nth-of-type(7n+2) { grid-column: 3; }
.month-cell:nth-of-type(7n+3) { grid-column: 4; }
.month-cell:nth-of-type(7n+4) { grid-column: 5; }
.month-cell:nth-of-type(7n+5) { grid-column: 6; }
.month-cell:nth-of-type(7n+6) { grid-column: 7; }
.month-cell:nth-of-type(7n+0) { grid-column: 8; }
/* The week number, in its own gutter lane - never floated (a floated label becomes grid content
   in the gutter track and shoves every day right). A paper cell ruled like the titles. */
.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: var(--paper); border: 0; border-bottom: 1px solid var(--rule); border-right: 1px solid var(--rule);
  border-radius: 0; cursor: pointer;
}
.weeknum-label:hover { color: var(--ink); background: var(--card-hover); }
.weeknum-label:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }
.weeknum-label:last-of-type { border-bottom: 0; }
/* The cell is a miniature of the week's day column - header strip, then commitments. It clips its
   own overflow (the week number is its own control now, not nested here) so a crowded day never
   spills over the rule below it; its own focus ring is an outline, which overflow cannot clip. */
.month-cell {
  display: flex; flex-direction: column; gap: 2px; align-items: stretch; text-align: left; cursor: pointer;
  min-height: 0; padding: 4px; overflow: hidden;
  background: var(--paper-2); border: 0; border-right: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
  border-radius: 0; font: inherit; color: inherit; transition: background 0.2s var(--ease);
}
/* The grid's own border closes the right and bottom edges, so the last column and last week
   drop theirs rather than doubling it. */
.month-cell:nth-of-type(7n+0) { border-right: 0; }
.month-cell:nth-last-of-type(-n+7) { border-bottom: 0; }
/* The day number and its count: the week's .week-dayhead strip, at the head of every cell. Same
   recipe - a flex row, content centred, baseline aligned, 6px gap, paper fill over a hairline
   rule - running edge to edge of the cell (negative margins cancel the cell's padding). The
   number is the week's 13px mono. */
.month-dayhead {
  display: flex; align-items: baseline; justify-content: center; gap: 6px;
  margin: -4px -4px 2px; padding: 6px 4px;
  font: inherit; color: inherit;
  background: var(--paper); border-bottom: 1px solid var(--rule);
}
.month-dayhead .month-daynum { font-family: var(--font-mono); font-size: 13px; color: var(--ink-2); }
.month-cell.is-today .month-dayhead .month-daynum { color: var(--ink); font-weight: 700; }
.month-daycount {
  font-family: var(--font-mono); font-size: 9px; color: var(--ink-3);
  border: 1px solid var(--rule); border-radius: 999px; padding: 0 4px; align-self: center;
}
.month-cell > * { min-width: 0; }
.month-cell:hover { background: var(--card-hover); }
.month-cell.is-out { opacity: 0.4; }
/* Today and the selected day are outlines inside the cell, not borders: a border would move the
   shared rules between cells, and an inset box-shadow would be painted under the header strip. */
.month-cell.is-today { outline: 2px solid var(--ink); outline-offset: -2px; }
.month-cell.is-selected { outline: 2px solid var(--amber); outline-offset: -2px; }
.month-cell:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }
/* The kind colouring, at parity with the week grid's .week-slot: a meeting is slate, a triage
   block wears its status amber/moss/vermilion as a left bar over a soft wash of the same hue.
   The month said "kind" with a coloured glyph only; once the chips are chips, the hue itself is
   the channel — and the week and the month stop teaching two dialects of the same survey. The
   two-line chip is the week's hover card in miniature: time on line one, title on line two, each
   truncated on its own line (the ellipses land where the hover card then spells the whole out).
   The glyph survives only as the phone-breakpoint mark (see .month-item::before / data-mark). */
.month-item {
  --chip-fill: var(--paper-raised); --chip-bar: var(--slate); --chip-text: var(--ink-2);
  display: block; min-width: 0; width: 100%;
  font-size: 11.5px; line-height: 1.25; color: var(--chip-text);
  background: var(--chip-fill); border: 1px solid var(--rule); border-left: 3px solid var(--chip-bar);
  border-radius: 3px; padding: 3px 5px;
}
.month-item.block-proposed { --chip-fill: var(--amber-soft); --chip-bar: var(--amber); }
.month-item.block-booked { --chip-fill: var(--moss-soft); --chip-bar: var(--moss); }
.month-item.block-stale { --chip-fill: var(--vermilion-soft); --chip-bar: var(--vermilion); }
.month-item.is-status-accepted { --chip-bar: var(--moss); }
.month-item.is-status-tentativelyAccepted { --chip-bar: var(--amber); }
.month-item.is-status-declined { --chip-bar: var(--rule); opacity: 0.6; }
.month-item.is-cancelled { text-decoration: line-through; color: var(--ink-3); }
.month-item-time { display: block; font-family: var(--font-mono); font-size: 11px; letter-spacing: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.month-item-title { display: block; font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.month-more { font-family: var(--font-mono); font-size: 11px; 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); }
/* Meeting invitation card (docs/38 s.3.18): label / value rows in the drawer's own mono-caps voice. */
.meeting-card { display: grid; gap: 8px; font-size: 14px; }
.meeting-row { display: grid; grid-template-columns: 84px 1fr; gap: 10px; align-items: baseline; }
.meeting-label { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); }
.meeting-when strong { font-size: 15px; white-space: pre-wrap; }
.meeting-join { margin-left: 4px; }
.meeting-people { display: flex; flex-wrap: wrap; gap: 4px 12px; }
.meeting-person i { font-style: normal; margin-right: 3px; font-weight: 700; }
.meeting-person.is-accepted i { color: var(--moss); }
.meeting-person.is-declined i { color: var(--vermilion); }
.meeting-person.is-tentativelyAccepted i { color: var(--amber); }
.meeting-clash.has-clash { color: var(--ink); }
.meeting-clash.has-clash > span:last-child { background: var(--amber-soft); border-radius: 4px; padding: 2px 8px; }
.meeting-rsvp { align-items: center; }
.meeting-rsvp { display: flex; flex-wrap: wrap; gap: 8px; }
.meeting-rsvp .meeting-label { width: 84px; }
.meeting-status.is-accepted { color: var(--moss); }
.meeting-status.is-declined { color: var(--vermilion); }
.meeting-rsvp .is-current { outline: 2px solid var(--slate); outline-offset: -2px; }
.meeting-banner { margin: 0; padding: 6px 10px; border-radius: 4px; background: var(--paper-2); }
.meeting-banner.is-cancelled { background: var(--vermilion-soft, #f6e1dc); color: var(--vermilion); font-weight: 600; }
.meeting-banner.is-update { background: var(--amber-soft); }
.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; }
/* Archived-body note shown in place of a full body the retention sweep reclaimed: the preview is
   intact but the authoritative copy is in the mailbox, so it reads as an aside, not an error. */
.email-body .body-thinned-note { margin: 12px 0 0; font-size: 13px; font-style: italic; color: var(--ink-3); white-space: normal; }
/* 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; }

/* ---------- Top bar tiers ----------
   The bar is brand | tabs | edition + account on one row. The tabs are the part that cannot shrink
   (ten of them, "Email Intelligence" among them), and how much room the whole row needs depends on
   things CSS cannot know: the account name, the count badges, whether Admin is shown. So the switch
   to the hamburger is NOT a guessed width - fitTopbar() in app.js measures what the row needs and
   sets .is-menu on the bar the moment the tabs would no longer fit beside the brand and the account.
   Below that point the tiers only make things smaller, so the tabs stay inline for as long as
   possible:
     < 1500px  compact:   tighter tabs; the edition shows its swatch only (its name is in the menu)
     < 1240px  condensed: also the mark alone, without "The Morning Brief"
     .is-menu             the tabs fold into a hamburger dropdown (whatever the window width) */
@media (max-width: 1499px) {
  .topbar { gap: 16px; }
  .topbar-nav { gap: 0; }
  .nav-link { padding: 8px 10px; font-size: 13.5px; gap: 6px; }
  #edition-label { display: none; }
}
@media (max-width: 1239px) {
  .brand-word { display: none; }
}
.topbar.is-menu { grid-template-columns: 1fr auto; gap: 12px; }
.topbar.is-menu .topbar-brand { gap: 12px; }
.topbar.is-menu .nav-toggle {
  display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px;
  border: 1px solid var(--rule); border-radius: 999px; background: none; cursor: pointer; color: var(--ink);
}
.topbar.is-menu .nav-toggle:hover { background: var(--paper-3); }
.topbar.is-menu .nav-toggle:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
/* Three bars drawn from one element: the bar itself plus its two pseudo-bars. Open turns it to an X. */
.nav-toggle-bars, .nav-toggle-bars::before, .nav-toggle-bars::after {
  display: block; width: 18px; height: 2px; border-radius: 2px; background: currentColor;
  transition: transform 0.2s var(--ease), background 0.2s;
}
.nav-toggle-bars { position: relative; }
.nav-toggle-bars::before, .nav-toggle-bars::after { content: ""; position: absolute; left: 0; }
.nav-toggle-bars::before { top: -6px; }
.nav-toggle-bars::after { top: 6px; }
.topbar.is-nav-open .nav-toggle-bars { background: transparent; }
.topbar.is-nav-open .nav-toggle-bars::before { transform: translateY(6px) rotate(45deg); }
.topbar.is-nav-open .nav-toggle-bars::after { transform: translateY(-6px) rotate(-45deg); }
/* The tabs become a dropdown panel hung from the bar's own bottom edge. */
.topbar.is-menu .topbar-nav {
  display: none; position: absolute; top: 100%; left: clamp(16px, 3vw, 40px);
  width: min(300px, calc(100vw - 32px)); flex-direction: column; align-items: stretch; gap: 2px;
  padding: 8px; background: var(--paper-raised); border: 1px solid var(--rule);
  border-radius: var(--radius); box-shadow: 0 12px 28px var(--scrim); z-index: 30;
}
.topbar.is-menu.is-nav-open .topbar-nav { display: flex; }
.topbar.is-menu .topbar-nav .nav-link { justify-content: space-between; padding: 11px 14px; font-size: 15px; border-radius: var(--radius); }
@media (max-width: 520px) {
  .user-name { max-width: 96px; }
}
@media (max-width: 400px) {
  .user-name { display: none; }
  .topbar { padding-inline: 12px; }
}

/* ---------- 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; }
  .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-row > .email-actions { 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-cell-h: 60px; --month-pad-y: 12px; }
  .month-cell { padding: 4px 5px; }
  .month-dayhead { margin: -4px -5px 2px; padding: 4px 2px; }
  /* A full weekday name cannot fit a phone column: the title shows the three-letter token the
     header carries as data-short, in a size a ~45px column holds. */
  .month-colhead { font-size: 0; padding: 4px 1px; }
  .month-colhead::before { content: attr(data-short); font-size: 10px; letter-spacing: 0.02em; }
  .month-corner { font-size: 8px; letter-spacing: 0; padding: 4px 1px; }
  /* 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. The chip's own time line shrinks and takes the data-mark's
     ●/◆ glyph (kind) at its head; the title line drops out with the chip's font-size. */
  .month-item { font-size: 0; padding: 0 3px; }
  .month-item::before { content: attr(data-mark); font-size: 11px; }
  .month-item-title { display: none; }
  .week-title { min-width: 0; flex: 1; }
}
/* Narrow: seven time columns are unreadable, so the week keeps its shape but scrolls sideways.
   Collapsing it to one column would trade the grid's whole point - seeing Thursday against Friday -
   for a taller agenda the day view already is. The day headers stay sticky to the TOP only; with the
   box also panning sideways they would otherwise float over the wrong column. A phone gets the
   shorter hours it can read, and the viewport cap loosens because the topbar wraps taller here. */
/* The survey keeps its own chrome budget on a phone too: the toolbar wraps to several
   lines, so the loosening mirrors the week's --week-chrome-h-phone. */
@media (max-width: 900px) {
  .month-grid { height: calc(100vh - var(--month-chrome-h-phone) - var(--month-head-gap) - var(--month-pad-y)); }
  .week-grid {
    grid-template-columns: var(--week-hour-w) repeat(7, minmax(var(--week-col-min), 1fr));
    max-height: calc(100vh - var(--week-chrome-h-phone));
    overflow-x: auto;
  }
  .week-corner { left: 0; }
  .week-dayhead { position: sticky; top: 0; }
  .week-daycount { display: none; }
}
@media (max-width: 600px) {
  :root { --week-hour-h: 44px; --week-hour-w: 40px; --week-col-min: 96px; }
  .week-slot-title { -webkit-line-clamp: 2; font-size: 11px; }
}
/* The blanket below is why the triage bar looked dead to a reduced-motion user (Windows'
   accessibility default and headless Chrome both report `reduce`): a frozen fill is not
   stillness - it is a hang the page cannot tell the truth about. Progress indicators keep
   their motion. They repeat the animation declaration with !important, so at equal
   importance the later (indicator) rule wins. `animation: revert-layer` was tried first and
   is NOT the lever: this engine still resolves the blanket's !important over it - measured,
   not assumed. Transitions stay dead for everyone: decorative motion is exactly what the
   preference asks to stop. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
  .triage-progress.is-indeterminate .triage-progress-fill {
    animation: triage-indeterminate 1.3s ease-in-out infinite !important;
  }
  .btn.is-busy .btn-spinner {
    animation: spin 0.8s linear infinite !important;
  }
  .email-body img.is-loading {
    animation: email-img-shimmer 1.1s linear infinite !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. Two independent
   columns sit side by side below the heading, collapsing to one 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 is display-independent and can safely live on the id. */
/* Two independent columns, exactly like a two-cell table: a wrapper div sits full-width under the
   heading and its two children are the columns. Each column stacks its own cards in plain flow, so
   the columns share NOTHING: no row tracks, no alignment between them, no shared row heights. One
   column's card heights can never move the other column's gaps, and both first cards sit at the
   same top. Gap under every card = the column's 28px gap - uniform by construction, at any height.
   Why not a grid: every pinned-track arrangement measured either zeroed a gap (a right card taller
   than the left row it shared) or opened a massive one (28px lattice slack, 41-95px) - a grid
   couples the columns through its row tracks; independent columns must not couple.
   The danger block and save bar are siblings of the wrapper: normal block flow, full width, below
   the columns like everything else after the heading. */
#view-settings.is-active {
  display: block;
}
#view-settings > .view-head { margin-bottom: 28px; }
.settings-cols {
  display: flex;
  align-items: flex-start;
  gap: 28px;
}
.settings-col {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 28px;
}
.settings-col > .settings-group { min-width: 0; }
@media (max-width: 720px) {
  /* Single column: the columns wrapper stacks its two columns in source order (left's four
     cards, then right's three), and the wrapper's flex gap keeps the 28px between every card -
     the same rhythm as the desktop row-gap. */
  .settings-cols { flex-direction: column; }
  /* ...which is exactly when the danger block's margin-top becomes a DOUBLE gap: in one column
     the card above it is genuinely the last flow item, so the wrapper gap lands above it too.
     Hand the gap back to that gap by dropping the block's own margin. The two danger cards
     stack too - every other setting is one per row by now and a 213px-wide lede is not readable. */
  #view-settings > .settings-danger { margin-top: 0; }
  .settings-danger > .settings-group { flex: 1 1 100%; }
}
/* The danger row sits below BOTH columns as one full-width block, below the tallest column by
   plain block flow. Inside, a flex row: each card half width, sharing one row like the preference
   cards. The 28px separation comes from the block's own margin-top, and margin-bottom (not
   padding-bottom) keeps the save bar's usual 4px below it. */
#view-settings > .settings-danger {
  display: flex;
  flex-wrap: wrap;
  gap: 28px;
  margin-top: 28px;
  margin-bottom: 28px;
}
.settings-danger > .settings-group {
  flex: 1 1 calc(50% - 14px);
  min-width: 0;
  margin-bottom: 0;
}
/* The card chrome every settings block shares (and the analysis tab borrows). */
.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. */
/* A .field is a grid, so every direct child is a stretched grid item - which pulls the
   segmented pill to full width (the override below), and the same arithmetic makes an
   overflow:hidden wrapper clip its overflowing children. The transparency panels are
   .field elements too, but their tiles are one grid item each; the override is scoped to
   the controls that actually take a whole line. */
.settings-group .field .segmented { justify-self: start; }
/* The transparency panels (.field grids) stack full-width blocks, not a caption's rows -
   six rows of 6px would tear the tiles, gauges and log to ribbons. A row count can leave
   items to auto-place into implicit rows; a definite track list leaves nothing implicit, so
   an eighth block can never land in an unstyled row. */
#learned-panel, #usefulness-panel { grid-auto-rows: auto; grid-template-rows: repeat(8, auto); }
.settings-actions { display: flex; align-items: center; gap: 14px; margin-top: 4px; }
.settings-actions .draft-error { flex: 1; margin: 0; }

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

/* --- 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. */
.rcpt-field .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;
}
.rcpt-field .rcpt-chips {
  display: flex; flex-wrap: wrap; gap: 6px; align-items: center; flex: 1; min-height: 36px;
  padding: 0 10px; border: 1px solid var(--rule); border-radius: var(--radius, 8px);
  background: var(--paper-bright);
}
.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: 70;
  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-empty { margin: 0; padding: 7px 10px; }

.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; }
/* Same arithmetic as the ledger labels below: through the .field wrapper, so the caption rule
   above cannot paint it as a form caption. */
.field .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, 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 DOM groups its parts into three explicit line wrappers (learned-use-line1/2/3) so the
   placement is structural - a line's parts can never auto-place into another line, and the
   tile's height is exactly three lines: name, scale, sub-line - the same height the maturity
   gauges above stand. */
/* 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 (name line,
   bar+thumb line, sub-line). 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;
}
.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 bar must end at the tile's right end exactly - it is a SCALE, and one that stops
   90px short of its track (a 225px bar under a 313px label) lies about the number printed
   above it. The thumb pair therefore never shares the bar's line: it is an absolute
   overlay, so the scale line sizes to the bar alone and the buttons cost it zero track.
   (A flex pair beside the bar always made it short of track; a thumb line of its own made
   every tile ~41px against the gauges' ~36px and blew the panel's compact budget.)
   Three explicit lines - name, scale, sub - are the tile's exact budget, the same three
   the single-column maturity gauges above stand. */
.learned-use-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto auto auto;
  column-gap: 0;
  border-top: 1px solid var(--rule, #e2d9c8);
  padding: 6px 0;
}
/* The name/number pair (label left, score right, vertically centred) - the thumbs no
   longer live here; they ride the scale line below, at the bar's right end.
   align-items:center is load-bearing: baseline alignment lets .field span's 12px metrics
   set the line height to 30px (against the label's own 21px), pushing the bar 41px below
   the name and out of the panel's 34px budget. One centred line, always.
   The line-height is NOT 1. A value of 1 sets the box to exactly the font size - 14px for
   this 13.5px label - while the font's real ascender/descender range measures 15px, so the
   box was one pixel short of its own text and every label had its ink sliced at the
   baseline. The letters that reach the cut are the descenders, so the panel read as though
   "Promotional Content", "Timing" and "Message" had been truncated with a razor; measured
   on the page, scrollHeight exceeded clientHeight on all seven. 1.15 (15.5px) clears the
   ink. It is still far below the 30px the inherited 1.55 would open, which is the failure
   the original value was guarding. test_ledger_headings_are_not_sliced_vertical is the
   assertion that keeps it that way - the row-height budget never noticed, because the row
   grew to accommodate the label while the label itself stayed clipped. */
 .learned-use-top {
   grid-column: 1; grid-row: 1;
   display: flex; flex-direction: row; align-items: center; justify-content: space-between;
   flex-wrap: nowrap; gap: 6px; min-width: 0; line-height: 1.15;
}
.field .learned-use-top .learned-use-label {
  font-weight: 600; font-size: 13.5px; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Line 2 (the scale line) sizes to the bar and hosts the thumb pair as an overlay. */
.learned-use-line { grid-column: 1; min-width: 0; position: relative; }
/* The bar sits behind the thumb pair rather than beside it: absolute across the whole
   track (left:0 right:0 against the line), so the buttons cost the scale no width - a
   pair beside the bar in the same flex line always left it short of its track (a 227px
   scale under a 313px label), the "bar looks stranded under the name" defect this panel
   has burned three layouts on. The line's definite 18px height is the pair's own height:
   the line is as tall as the buttons whether they flow or not, the bar hangs at its
   midline, and the tile cannot grow past it. */
.learned-use-line2 {
  grid-row: 2; margin-top: 8px;
  position: relative; height: 18px;
}
.learned-use-line3 { grid-row: 3; margin-top: 3px; }
.learned-use-bar {
  position: absolute; left: 0; right: 0; top: 50%;
  /* translateY(-50%) resolves against the bar's own 6px, so its midline lands exactly on
     the line's midline - the same midline the thumb pair hangs on. */
  transform: translateY(-50%);
  height: 6px;
  /* The track is the scale the fill is measured against, and on a bipolar axis the unused half is
     as meaningful as the used half: a short fill must read as "argues weakly", not "the line is
     short". So the groove is back, hairline and quiet - it is a ruler, not a claim. It was removed
     when the fill was left-anchored, where the fill's own width was the whole reading. */
  background: var(--paper-3); overflow: hidden;
}
/* The neutral origin at 50%, from which every fill grows outward. Present on every scale so the
   eye has the same reference on all seven tiles - without it, a fill hugging the centre and a fill
   hugging an end are indistinguishable at a glance, which is the whole distinction the panel is
   for. It stays visible under a zero-width fill, so an unweighed axis still shows its origin. */
.learned-use-mid {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 1px; height: 12px; background: var(--ink-3); opacity: .55;
}
/* absolute, so the inline left/width the ledger writes per row actually take - a static element
   ignores `left` entirely, and the fill sat pinned at the track's left edge painting the right
   width in the wrong place. */
.learned-use-bar > .learned-bar-fill { position: absolute; top: 0; height: 6px; }
/* An axis never weighed on the user's mail earns no fill at all (see learnedUseGeom); the track and
   its origin carry the tile, and the dim class is what the panel already uses for "no view formed",
   so this adds no new visual language. visibility rather than display:none keeps the fill laid out
   at zero width, so the probe measures an honest 0% instead of finding no box to measure - a
   missing element and an empty bar must not read as the same number. */
.learned-use-row.is-unweighed .learned-bar-fill { visibility: hidden; }
.learned-use-thumbs {
  /* Absolute, centred on the line's midline like the bar - and out of flow, so a pair of
     any width never costs the scale one pixel of its track. */
  position: absolute; right: 0; top: 50%; transform: translateY(-50%);
  display: flex; gap: 6px;
  /* A paper pill - opaque, with a wide left cuff - lets the buttons read as sitting ON the
     bar's right end rather than a gap punched out of the scale. */
  background: var(--paper, #fffdf7); padding-left: 8px; border-radius: 999px;
}
/* Weight/colour/size must be stated through the pair: both panels are .field containers,
   and ".field span" (:417) scores 0,1,1 against a bare label class's 0,1,0 - that generic
   caption rule (12px, 500, --ink-2) is what every metric heading paints unless the pair
   says otherwise, which is exactly how untouched tiles alone came out lighter than their
   neighbours. The score never shrinks: when the tile narrows, the label is the half that
   gives way (ellipsis), so the number the bar measures always prints whole. */
.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); }
/* The sub-line takes row 3, full width. Its parts - the Done/Reject count and the weight
   chip - fit one line at every tile width the grid serves; nowrap keeps that true, and the
   explicit line-height stops .field span's inherited 1.55 leading from opening a taller box
   the 16px parts cannot fill, which reads as a wrapped line against the compactness budget.
   1.25 rather than 1, for the reason spelled out on .learned-use-top above: a 12px box from
   a 12px count was one pixel short of the font's own ink range and sliced the descenders out
   of "decisions", "reversed" and "weighed" - the words carrying the count this whole line
   exists to show. 1.25 (15px) clears them and is still under half the 30px the inherited
   value would open. */
 .learned-use-sub {
   grid-column: 1; grid-row: 3; margin-top: 3px;
   display: flex; gap: 8px; align-items: baseline; flex-wrap: nowrap;
   min-width: 0; font-size: 11.5px; line-height: 1.25;
}
.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;
}
/* The chip's class is learned-log-<token> from learnedDecisionToken in app.js: the verb a
   card verdict records (done/dismissed/kept/rejected) or a draft decision's kind, mapped to
   the reader's word (applied/ignored/reversed/overridden). Each word states its own colour:
   applied and ignored take the base caption ink, the corrections wear the house red.
   'rejected' is the draft's internal kind and the token 'reversed' shows - a discarded draft
   writes kind 'rejected' with a NULL outcome - so the rule is spelled on both the kind and
   the word; a card's Reject verdict keeps printing the raw verb 'rejected' as its word.
   The doubled class is deliberate: ".field span" - the generic form-caption rule - scores
   (0,1,1) and would outrank a bare class, painting every chip --ink-2, so these rules
   re-state the base class beside their own word for (0,2,0) - higher than the caption,
   with no element name a <span> could dodge. The corrections spend --vermilion: it holds
   across the theme flip by design (the Pressroom block deliberately does not re-mix it), so
   the chip's ink and border move as one. The earlier var(--crit) spelled a token this sheet
   never defined and quietly painted its #a3320e fallback - a dead token is a bug, not a
   default. */
.learned-log-kind.learned-log-applied { color: var(--ink-3); border-color: var(--rule); }
.learned-log-kind.learned-log-ignored { color: var(--ink-3); border-color: var(--rule); }
.learned-log-kind.learned-log-reversed,
.learned-log-kind.learned-log-overridden,
.learned-log-kind.learned-log-rejected { color: var(--vermilion); border-color: var(--vermilion); }

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