/* ==========================================================================
   EXP-PM — design system
   --------------------------------------------------------------------------
   One stylesheet, no build step, no framework. Everything below is tokens
   first and components second, so a colour or spacing decision is made in
   exactly one place.

   THEMING RULE, and it is load-bearing:
   The viewer has THREE states, not two. An explicit choice stamps
   data-theme="dark"/"light" on <html>; the default "system" setting stamps
   nothing at all, and only prefers-color-scheme separates light from dark.
   So the complete light palette lives on bare :root, the dark palette is
   redefined TOKEN-ONLY in two places, and no component rule ever sets a
   colour inside a media query. A colour whose only definition sits behind
   a media query renders one theme's text on the other theme's ground.
   ========================================================================== */

:root {
  /* --- ground and surfaces ------------------------------------------------
     Neutrals carry a faint green bias rather than being pure grey. A true
     mid-grey reads as unconsidered next to a saturated accent; biasing the
     neutral a few degrees toward the accent makes the whole surface look
     chosen. */
  --ground:      #F1F4F1;
  --surface:     #FFFFFF;
  --raise:       #FAFBF9;
  --sunk:        #E7EBE6;
  --sunk-2:      #DCE2DA;

  --line:        #DBE1D9;
  --line-soft:   #E9EDE7;
  --line-strong: #C3CCC1;

  --ink:         #121614;
  --ink-2:       #444C47;
  /* Darkened to clear 4.5:1 against --ground. The previous values looked
     right and measured at about 3.4:1 — which is the whole reason contrast
     is checked by a tool rather than by eye. */
  --ink-3:       #5C655F;
  --ink-4:       #69736D;

  /* --- brand --------------------------------------------------------------
     A deep viridian. Dark enough to carry white text at small sizes, and far
     enough from the default blue/purple that the product does not read as
     a template. */
  --brand:       #0F6B58;
  --brand-hi:    #0B5646;
  --brand-wash:  #DCEDE7;
  --brand-ink:   #084A3D;
  --on-brand:    #FFFFFF;

  /* --- semantic (separate from brand, never used decoratively) ----------- */
  --good:        #2C7550;  --good-wash:  #DEEDE3;
  --warn:        #8E6410;  --warn-wash:  #F7EED8;
  --bad:         #A93B2C;  --bad-wash:   #F8E2DE;
  --info:        #2A5C93;  --info-wash:  #E0E9F6;

  /* Ink for text sitting ON --bad, the same idea as --on-brand above.
     Light theme keeps white: #A93B2C is dark enough to carry it at 6.5:1.
     The dark theme's --bad is a pastel and cannot — see the dark blocks. */
  --on-bad:      #FFFFFF;

  /* --- label swatches -----------------------------------------------------
     Stored in the database as names, resolved to colour here. That is what
     lets one stored value render correctly in both themes; a stored hex would
     freeze one theme's rendering into the row forever. */
  --sw-slate:  #4C5A53;  --sw-slate-bg:  #E6EAE7;  --sw-slate-dot:  #6B7A72;
  --sw-red:    #A03328;  --sw-red-bg:    #FAE3E0;  --sw-red-dot:    #C2483A;
  --sw-amber:  #8A5D08;  --sw-amber-bg:  #FAEED5;  --sw-amber-dot:  #C08A1E;
  --sw-green:  #2B6B41;  --sw-green-bg:  #DDEEE1;  --sw-green-dot:  #3E8C57;
  --sw-teal:   #10635C;  --sw-teal-bg:   #D8EDEB;  --sw-teal-dot:   #158C82;
  --sw-blue:   #27548D;  --sw-blue-bg:   #DEE9F7;  --sw-blue-dot:   #3B72BE;
  --sw-violet: #5A3F94;  --sw-violet-bg: #E8E2F7;  --sw-violet-dot: #7A5CC4;
  --sw-pink:   #95356A;  --sw-pink-bg:   #F9E0EE;  --sw-pink-dot:   #C0518E;

  /* --- depth --------------------------------------------------------------
     Layered and transparent, so a card keeps its edge over any surface
     underneath. Borders separate; shadows lift. Using both for the same job
     is what makes an interface look muddy. */
  --lift-1: 0 1px 2px rgba(16, 26, 21, .05);
  --lift-2: 0 1px 2px rgba(16, 26, 21, .06), 0 3px 8px rgba(16, 26, 21, .06);
  --lift-3: 0 2px 4px rgba(16, 26, 21, .07), 0 10px 26px rgba(16, 26, 21, .11);
  --ring:   0 0 0 3px rgba(15, 107, 88, .22);

  /* --- type --------------------------------------------------------------- */
  --sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI",
          system-ui, "Helvetica Neue", sans-serif;
  --mono: ui-monospace, "Cascadia Mono", "SF Mono", Consolas, monospace;

  --t-2xs: .625rem;
  --t-xs:  .6875rem;
  --t-sm:  .75rem;
  --t-md:  .8125rem;
  --t-base: .875rem;
  --t-lg:  1.0625rem;
  --t-xl:  1.3125rem;

  /* --- geometry -----------------------------------------------------------
     Concentric: a child's radius is its parent's minus the parent's padding,
     so nested corners stay optically parallel instead of drifting. */
  --r-xs: 4px;
  --r-sm: 6px;
  --r:    9px;
  --r-lg: 13px;
  --r-xl: 18px;

  --pad-col: 6px;   /* board column padding — card radius is derived from it */

  /* --- motion ------------------------------------------------------------- */
  --ease: cubic-bezier(.2, .7, .3, 1);
  --fast: 120ms;
  --med:  190ms;

  /* --- material -----------------------------------------------------------
     Liquid glass, and the honest version of it.

     Glass is a material for things that FLOAT over content: menus, sheets,
     the task drawer, the mobile navigation, the scrim behind them. It is not
     a skin for the whole app. A bar with nothing scrolling underneath has
     nothing to refract, so blurring it costs a compositor layer and buys a
     flat tint — which is exactly why the header and tab strip are left
     opaque. They sit in normal flow above the pane; nothing passes behind
     them, and glass there would be pure cost for no depth.

     Two densities, and the difference between them is contrast, not taste:

       --glass       chrome carrying icons and short, full-strength labels
       --glass-deep  anything carrying body text or muted text

     Text contrast on glass depends on whatever happens to be behind it,
     which a stylesheet cannot know. At --glass density over a dark backdrop
     --ink still clears 4.5:1, but --ink-3 and --ink-4 fall to roughly 2.8:1
     — so every surface that carries prose or secondary text uses
     --glass-deep, which stays near its opaque equivalent even in the worst
     case. That rule is the whole reason there are two tokens and not one. */
  --glass-blur:  18px;
  --glass-sat:   180%;
  --glass:       rgba(255, 255, 255, .70);
  --glass-deep:  rgba(255, 255, 255, .88);
  /* The specular edge. A light top rim is what reads as thickness rather
     than as opacity — it is the single detail separating this from a plain
     semi-transparent div. One inset hairline, never a gradient. */
  --glass-line:  rgba(255, 255, 255, .60);
  --glass-edge:  rgba(16, 26, 21, .12);
  --glass-scrim: rgba(8, 14, 11, .30);
}

/* Dark, for viewers on "system". Guarded so an explicit light choice still
   wins over a dark operating system. Tokens only — never components. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ground:      #0A0D0C;
    --surface:     #121716;
    --raise:       #171E1C;
    --sunk:        #1B2321;
    --sunk-2:      #232C29;

    --line:        #26302C;
    --line-soft:   #1C2422;
    --line-strong: #38443F;

    --ink:         #E3EAE6;
    --ink-2:       #B2BDB7;
    --ink-3:       #929D97;
    --ink-4:       #848F89;

    --brand:       #45B096;
    --brand-hi:    #63C9AF;
    --brand-wash:  #0E2F28;
    --brand-ink:   #8ADCC3;
    --on-brand:    #04211B;

    --good: #5CB98B;  --good-wash: #10301F;
    --warn: #D3A345;  --warn-wash: #33280C;
    --bad:  #DB7A6D;  --bad-wash:  #351814;
    --info: #6BA0DC;  --info-wash: #132538;

    /* Dark ink, not white. White on this pastel measures 2.86:1 — the Delete
       button, the single most consequential control in the product, was the
       hardest thing in the drawer to read. Caught only once the drawer scan
       ran in dark as well as light. */
    --on-bad: #2A0F0B;

    --sw-slate:  #9DAAA3;  --sw-slate-bg:  #222B27;  --sw-slate-dot:  #7E8C85;
    --sw-red:    #E0938A;  --sw-red-bg:    #331A17;  --sw-red-dot:    #C96A5C;
    --sw-amber:  #DDAE5A;  --sw-amber-bg:  #322512;  --sw-amber-dot:  #C79239;
    --sw-green:  #7CC495;  --sw-green-bg:  #16301F;  --sw-green-dot:  #57A472;
    --sw-teal:   #63C0B6;  --sw-teal-bg:   #10302D;  --sw-teal-dot:   #3EA396;
    --sw-blue:   #83AEE2;  --sw-blue-bg:   #14243A;  --sw-blue-dot:   #5A8CC7;
    --sw-violet: #AE9AE0;  --sw-violet-bg: #211C38;  --sw-violet-dot: #8C74CB;
    --sw-pink:   #DE94BE;  --sw-pink-bg:   #331A29;  --sw-pink-dot:   #C46C9F;

    --lift-1: 0 1px 2px rgba(0, 0, 0, .5);
    --lift-2: 0 1px 2px rgba(0, 0, 0, .5), 0 3px 10px rgba(0, 0, 0, .42);
    --lift-3: 0 2px 6px rgba(0, 0, 0, .55), 0 12px 30px rgba(0, 0, 0, .5);
    --ring:   0 0 0 3px rgba(69, 176, 150, .28);

    /* Material. Dark glass is DARKER than its own surface, not lighter: it is
       a smoked pane over the content. Lightening it reads as fog. */
    --glass:       rgba(18, 23, 22, .62);
    --glass-deep:  rgba(18, 23, 22, .90);
    --glass-line:  rgba(255, 255, 255, .09);
    --glass-edge:  rgba(0, 0, 0, .50);
    --glass-scrim: rgba(0, 0, 0, .50);
  }
}

/* Dark, chosen explicitly. Same tokens, so the toggle wins in both
   directions rather than only when the OS already agrees. */
:root[data-theme="dark"] {
  --ground:      #0A0D0C;
  --surface:     #121716;
  --raise:       #171E1C;
  --sunk:        #1B2321;
  --sunk-2:      #232C29;

  --line:        #26302C;
  --line-soft:   #1C2422;
  --line-strong: #38443F;

  --ink:         #E3EAE6;
  --ink-2:       #B2BDB7;
  --ink-3:       #929D97;
  --ink-4:       #848F89;

  --brand:       #45B096;
  --brand-hi:    #63C9AF;
  --brand-wash:  #0E2F28;
  --brand-ink:   #8ADCC3;
  --on-brand:    #04211B;

  --good: #5CB98B;  --good-wash: #10301F;
  --warn: #D3A345;  --warn-wash: #33280C;
  --bad:  #DB7A6D;  --bad-wash:  #351814;
  --info: #6BA0DC;  --info-wash: #132538;

  /* Must match the media-query block above; see the note there. */
  --on-bad: #2A0F0B;

  --sw-slate:  #9DAAA3;  --sw-slate-bg:  #222B27;  --sw-slate-dot:  #7E8C85;
  --sw-red:    #E0938A;  --sw-red-bg:    #331A17;  --sw-red-dot:    #C96A5C;
  --sw-amber:  #DDAE5A;  --sw-amber-bg:  #322512;  --sw-amber-dot:  #C79239;
  --sw-green:  #7CC495;  --sw-green-bg:  #16301F;  --sw-green-dot:  #57A472;
  --sw-teal:   #63C0B6;  --sw-teal-bg:   #10302D;  --sw-teal-dot:   #3EA396;
  --sw-blue:   #83AEE2;  --sw-blue-bg:   #14243A;  --sw-blue-dot:   #5A8CC7;
  --sw-violet: #AE9AE0;  --sw-violet-bg: #211C38;  --sw-violet-dot: #8C74CB;
  --sw-pink:   #DE94BE;  --sw-pink-bg:   #331A29;  --sw-pink-dot:   #C46C9F;

  --lift-1: 0 1px 2px rgba(0, 0, 0, .5);
  --lift-2: 0 1px 2px rgba(0, 0, 0, .5), 0 3px 10px rgba(0, 0, 0, .42);
  --lift-3: 0 2px 6px rgba(0, 0, 0, .55), 0 12px 30px rgba(0, 0, 0, .5);
  --ring:   0 0 0 3px rgba(69, 176, 150, .28);

  /* Material. Dark glass is DARKER than its own surface, not lighter: it is
     a smoked pane over the content. Lightening it reads as fog. */
  --glass:       rgba(18, 23, 22, .62);
  --glass-deep:  rgba(18, 23, 22, .90);
  --glass-line:  rgba(255, 255, 255, .09);
  --glass-edge:  rgba(0, 0, 0, .50);
  --glass-scrim: rgba(0, 0, 0, .50);
}

/* ==========================================================================
   Base
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }

/* The hidden attribute has to win, and without this it loses.
   `display: none` for [hidden] comes from the USER-AGENT stylesheet, and any
   author rule that sets display beats it outright — author styles outrank UA
   styles, no !important required. So `.banner { display: flex }` further down
   silently made `<div class="banner" hidden>` visible, and the temporary
   sign-in warning — "anyone reaching this URL can act as any user" — showed on
   every production page load from the day Entra went live, while
   /api/dev/mode was returning 404 and insecureDevAuth was false.

   A security banner that cannot be switched off is worse than no banner: it
   teaches everyone to ignore the one that will matter.

   Three elements depend on this attribute (the banner, the dev actor picker in
   app.js, the AI-help input in aihelp.js), which is why this is global rather
   than a display:none on .banner. */
[hidden] { display: none !important; }

html {
  height: 100%;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body {
  height: 100%;
  margin: 0;
  /* Explicit, from a token. A transparent body borrows the host's ground and
     silently renders one theme's text on the other theme's background. */
  background: var(--ground);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--t-base);
  line-height: 1.5;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 640;
  letter-spacing: -.014em;
  text-wrap: balance;
}

p { margin: 0; text-wrap: pretty; }

a { color: var(--brand); text-decoration-thickness: 1px; text-underline-offset: 2px; }

img {
  max-width: 100%;
  /* Neutral, never brand-tinted — a coloured outline reads as a UI element
     rather than as the image's own edge. */
  outline: 1px solid rgba(0, 0, 0, .1);
  outline-offset: -1px;
}
:root[data-theme="dark"] img { outline-color: rgba(255, 255, 255, .1); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) img { outline-color: rgba(255, 255, 255, .1); }
}

code, .mono, .num {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}

/* Any digit that changes in place — counts, dates, durations — must not
   reflow its neighbours as it changes width. */
.num, .count, td.num, .tick, .cal-daynum { font-variant-numeric: tabular-nums; }

:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

::selection { background: var(--brand-wash); color: var(--brand-ink); }

/* Scrollbars, quietly. A default chrome scrollbar on a dark surface is the
   single most obvious "unstyled web app" tell. */
* { scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border-radius: 99px;
  border: 3px solid transparent;
  background-clip: content-box;
}
*::-webkit-scrollbar-track { background: transparent; }

/* ==========================================================================
   Controls
   ========================================================================== */

button, input, select, textarea {
  font: inherit;
  color: inherit;
}

.btn {
  --btn-bg: var(--brand);
  --btn-fg: var(--on-brand);
  --btn-line: transparent;

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .4em;
  min-height: 32px;
  padding: 0 .7rem;
  border: 1px solid var(--btn-line);
  border-radius: var(--r-sm);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-size: var(--t-md);
  font-weight: 600;
  letter-spacing: -.005em;
  white-space: nowrap;
  cursor: pointer;
  /* Never `transition: all` — it animates layout properties by accident and
     makes every hover cost a reflow. */
  transition-property: background-color, border-color, color, box-shadow, transform;
  transition-duration: var(--fast);
  transition-timing-function: ease-out;
}
.btn:hover { --btn-bg: var(--brand-hi); }
.btn:active { transform: scale(.97); }

.btn.ghost {
  --btn-bg: var(--surface);
  --btn-fg: var(--ink-2);
  --btn-line: var(--line);
}
.btn.ghost:hover { --btn-bg: var(--sunk); --btn-fg: var(--ink); }

.btn.quiet {
  --btn-bg: transparent;
  --btn-fg: var(--ink-3);
  --btn-line: transparent;
}
.btn.quiet:hover { --btn-bg: var(--sunk); --btn-fg: var(--ink); }

.btn.danger { --btn-bg: var(--bad); --btn-fg: var(--on-bad); }
.btn.danger:hover { --btn-bg: var(--bad); filter: brightness(1.1); }

.btn.sm { min-height: 26px; padding: 0 .5rem; font-size: var(--t-sm); }
.btn:disabled { opacity: .45; cursor: not-allowed; transform: none; }

/* Icon-only controls are visually small but must stay comfortably tappable.
   The pseudo-element grows the hit area without moving anything on screen. */
.icon-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  /* flex: none, or the declared size is a suggestion.
     These sit in flex rows, and flex items shrink below their width by
     default — the header's theme button measured 23.9px at phone width, just
     under the WCAG 2.2 target-size minimum of 24px, and it got there purely by
     being squeezed. A touch target that quietly shrinks as its toolbar fills
     up is the worst kind: it passes every measurement taken on a wide screen.
     min-width/min-height are belt and braces for grid and table contexts,
     where flex-basis does not apply. */
  flex: none;
  min-width: 28px;
  min-height: 28px;
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink-3);
  font-size: var(--t-base);
  line-height: 1;
  cursor: pointer;
  transition-property: background-color, color;
  transition-duration: var(--fast);
}
.icon-btn::after {
  content: "";
  position: absolute;
  /* 28px control → 40px hit area. Kept to exactly the shortfall so adjacent
     controls in a toolbar cannot end up with overlapping targets. */
  inset: -6px;
}
.icon-btn:hover { background: var(--sunk); color: var(--ink); }
.icon-btn.on { background: var(--brand-wash); color: var(--brand-ink); }

.fld {
  min-height: 32px;
  padding: .32rem .55rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink);
  font-size: var(--t-md);
  max-width: 100%;
  transition-property: border-color, box-shadow;
  transition-duration: var(--fast);
}
.fld:focus { outline: none; border-color: var(--brand); box-shadow: var(--ring); }
.fld::placeholder { color: var(--ink-4); }

textarea.fld {
  width: 100%;
  min-height: 8rem;
  font-family: var(--mono);
  font-size: var(--t-sm);
  line-height: 1.55;
  resize: vertical;
}

select.fld { cursor: pointer; }

.lbl {
  margin: 0 0 .4rem;
  font-size: var(--t-2xs);
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  /* --ink-3, not --ink-4. At 10px these measured 4.32:1 and needed 4.5:1 —
     small uppercase text is the least forgiving thing on the page, so it gets
     the darker of the two muted tokens. */
  color: var(--ink-3);
}

.muted { color: var(--ink-3); font-size: var(--t-md); text-wrap: pretty; }

/* ==========================================================================
   Chrome — top bar
   ========================================================================== */

.banner {
  display: flex;
  align-items: center;
  gap: .55rem;
  padding: .4rem 1rem;
  background: var(--warn-wash);
  color: var(--warn);
  border-bottom: 1px solid var(--warn);
  font-size: var(--t-sm);
  font-weight: 640;
}

.chrome {
  display: flex;
  align-items: center;
  gap: .7rem;
  flex: none;
  height: 48px;
  padding: 0 .75rem 0 1rem;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}

.brand {
  display: flex;
  align-items: center;
  gap: .5rem;
  font-weight: 750;
  font-size: var(--t-base);
  letter-spacing: -.03em;
  color: var(--ink);
}
.mark {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  /* Concentric with nothing, but its own corner is soft enough to read as a
     mark rather than a button. */
  border-radius: 7px;
  background: var(--brand);
  color: var(--on-brand);
  font-size: var(--t-sm);
  font-weight: 800;
  letter-spacing: -.05em;
}

.grow { flex: 1; }

/* The search field is a button dressed as an input: clicking it opens the
   command palette rather than typing in place, so there is one search
   surface instead of two that behave differently. */
.searchbar {
  display: flex;
  align-items: center;
  gap: .5rem;
  width: min(420px, 40vw);
  min-height: 30px;
  padding: 0 .55rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--ground);
  color: var(--ink-3);
  font-size: var(--t-md);
  cursor: pointer;
  transition-property: border-color, background-color, color;
  transition-duration: var(--fast);
}
.searchbar:hover { border-color: var(--line-strong); color: var(--ink-2); }
.kbd {
  padding: .1em .35em;
  border: 1px solid var(--line);
  border-radius: var(--r-xs);
  background: var(--surface);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  color: var(--ink-3);
}

.who { display: flex; align-items: center; gap: .4rem; }

/* ==========================================================================
   Shell
   ========================================================================== */

.shell {
  flex: 1;
  display: grid;
  grid-template-columns: 244px minmax(0, 1fr);
  min-height: 0;
}

/* min-width:0 on both tracks is load-bearing. Grid children default to
   min-content width, so without it a long project name pushes the rail wider
   than its track and it clips its own controls. */
.rail {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
  padding: .85rem .6rem;
  background: var(--surface);
  border-right: 1px solid var(--line);
  overflow-y: auto;
  overflow-x: hidden;
}
.rail * { min-width: 0; }

.rail-sec { display: flex; flex-direction: column; gap: 1px; }
.rail-sec > .lbl {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 .45rem;
}

.nav {
  display: flex;
  align-items: center;
  gap: .55rem;
  width: 100%;
  min-height: 30px;
  padding: 0 .45rem;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink-2);
  font-size: var(--t-md);
  font-weight: 550;
  text-align: left;
  cursor: pointer;
  transition-property: background-color, color;
  transition-duration: var(--fast);
}
.nav:hover { background: var(--sunk); color: var(--ink); }
.nav.on { background: var(--brand-wash); color: var(--brand-ink); font-weight: 640; }
.nav .glyph { flex: none; width: 1rem; text-align: center; opacity: .85; }
.nav .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav .badge {
  flex: none;
  min-width: 1.15rem;
  padding: .05em .3em;
  border-radius: 99px;
  background: var(--sunk-2);
  color: var(--ink-3);
  font-size: var(--t-2xs);
  font-weight: 700;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.nav.on .badge { background: var(--brand); color: var(--on-brand); }

.dot {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 99px;
  background: var(--sw-slate-dot);
}

.rail-foot { margin-top: auto; padding-top: .6rem; border-top: 1px solid var(--line-soft); }

/* Hidden at desktop widths, where the rail is always a column on screen.
   Declared BEFORE the media query below: both selectors have the same
   specificity, so whichever comes last wins — and putting this after was
   exactly the bug that made the responsive layout dead code the first time. */
.rail-toggle { display: none; }

/* --- phones ---------------------------------------------------------------
   This block MUST come after the .rail rules above. It used to sit before
   them, and since both selectors have the same specificity the later plain
   `.rail { display: flex }` silently won — so the whole responsive layout was
   dead code that measured as working because the sidebar simply never hid.

   The breakpoint is 700px, not 900px. An iPad in portrait is 810 CSS pixels:
   a 244px rail still leaves 566px of content, which is a usable two-column
   layout and better than making people open a menu to change project. Below
   700px there genuinely is not room, so the rail becomes an overlay — never
   simply hidden, which would leave no navigation at all. */
@media (max-width: 700px) {
  .shell { grid-template-columns: minmax(0, 1fr); }

  .rail {
    position: fixed;
    inset: 0 auto 0 0;
    /* Below the modal layer. A dialog belongs above the navigation, and a
       translated-off-screen element is still in the stacking order — with the
       rail above the drawer, contrast auditing measured drawer text against
       the rail's background and reported the entire dialog as failing. */
    z-index: 30;
    width: 268px;
    transform: translateX(-100%);
    /* Hidden, not merely moved. A closed menu must be out of the tab order and
       out of the accessibility tree, or a screen-reader user tabs into a
       sidebar that is not on screen. */
    visibility: hidden;
    box-shadow: var(--lift-3);
    /* visibility gets a ZERO-duration transition with a delay, not a timed
       one. Timing it means the panel stays invisible for the whole slide and
       nothing in it can be clicked yet; this flips it immediately on open and
       defers hiding until the slide-out has finished. */
    transition: transform var(--med) var(--ease),
                visibility 0s linear var(--med);
  }
  .rail.open {
    transform: none;
    visibility: visible;
    transition: transform var(--med) var(--ease), visibility 0s;
  }

  /* The trigger only exists where it is needed. */
  .rail-toggle { display: inline-flex; }

  .head { padding-top: .6rem; }
  .searchbar { width: auto; flex: 1; min-width: 0; }
  /* The "Acting as" label goes; the control itself stays. While the dev
     sign-in stub is the only way in, hiding it would leave an iPad with no
     way to choose who you are — which is exactly the sort of "saves space"
     decision that makes a screen unusable. */
  .who .lbl { display: none; }
  .who .fld { max-width: 9rem; }
}


.rail-scrim {
  position: fixed;
  inset: 0;
  z-index: 29;
  background: rgba(8, 14, 11, .4);
  animation: fade var(--fast) var(--ease);
}

/* ==========================================================================
   Stage — header, tabs, toolbar, pane
   ========================================================================== */

/* Both min-* values are load-bearing, for the same reason and on both axes.
   A grid item's automatic minimum size is its MIN-CONTENT size, not zero,
   unless the item is itself a scroll container — which the rail is (it has
   overflow-y) and the stage is not.

   Without min-height, the stage grows to fit whatever is in the pane. A long
   imported document then makes it taller than the shell, `.pane { flex: 1 }`
   resolves against that overgrown parent so its content fits exactly, its
   `overflow: auto` finds nothing to scroll, and the excess is clipped by
   `body { overflow: hidden }`. The result reads as a page that renders
   correctly and simply stops halfway down, with no scrollbar anywhere. */
.stage { min-width: 0; min-height: 0; display: flex; flex-direction: column; }

.head {
  display: flex;
  align-items: center;
  gap: .65rem;
  flex: none;
  padding: .8rem 1.1rem .55rem;
}
.head h1 {
  font-size: var(--t-xl);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.head .sub {
  color: var(--ink-3);
  font-size: var(--t-sm);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.tabs {
  display: flex;
  /* Wraps rather than scrolls.

     A project has eleven views, and this used to be a horizontal scroller with
     `scrollbar-width: none` and the WebKit scrollbar set to display:none. On
     any screen too narrow for all eleven the remainder were not merely off
     screen — with the scrollbar suppressed there was nothing whatsoever to say
     they existed. On a phone that hid five of them; on an iPad it also hid
     some, because the rail is still a 244px column until 700px and the strip
     only gets what is left.

     Wrapping needs no breakpoint, which is the point: the row count follows
     the width actually available, so it is right on a phone, on an iPad with
     a rail, and in a narrowed desktop window alike — and a second row appears
     only when there was no room for one row. */
  flex-wrap: wrap;
  gap: 0 .1rem;
  flex: none;
  padding: 0 1.1rem;
  border-bottom: 1px solid var(--line);
}
.tab {
  position: relative;
  min-height: 34px;
  padding: 0 .6rem;
  border: 0;
  background: transparent;
  color: var(--ink-3);
  font-size: var(--t-md);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition-property: color;
  transition-duration: var(--fast);
}
.tab::after {
  content: "";
  position: absolute;
  left: .35rem;
  right: .35rem;
  bottom: -1px;
  height: 2px;
  border-radius: 2px 2px 0 0;
  background: transparent;
  transition-property: background-color;
  transition-duration: var(--fast);
}
.tab:hover { color: var(--ink); }
.tab.on { color: var(--brand); }
.tab.on::after { background: var(--brand); }

/* --------------------------------------------------------------------------
   The projection switch
   --------------------------------------------------------------------------
   Board, Timeline, Weeks, List and Slip used to be five of eleven top-level
   tabs. They are five ways of drawing the same work, so they are now one tab
   with this underneath it.

   flex-basis:100% is what puts it on its own line: .tabs is a wrapping flex
   row, so a full-width item forces a break without needing a second
   container above the tab strip. It renders only while a work view is open —
   a switch that is always present would hand back the vertical space the
   consolidation just won.
   -------------------------------------------------------------------------- */
.subtabs {
  display: flex;
  flex-wrap: wrap;
  gap: .2rem;
  flex-basis: 100%;
  padding: .1rem 0 .45rem;
}
/* .density-sub shares the pill shape but NOT the .subtab class, because that
   class means "a projection of the work" and a test asserts exactly which
   six exist. Density changes how the work looks, not which work you are
   looking at, so it borrows the styling and stays out of that set. */
.subtab,
.density-sub {
  min-height: 26px;
  padding: 0 .55rem;
  border: 1px solid transparent;
  border-radius: 99px;
  background: transparent;
  color: var(--ink-3);
  font-family: inherit;
  font-size: var(--t-2xs);
  font-weight: 640;
  white-space: nowrap;
  cursor: pointer;
}
.subtab:hover,
.density-sub:hover { color: var(--ink); border-color: var(--line); }

/* At the far end of the same strip, pushed right by its own auto margin so
   it reads as a separate control rather than a seventh projection. */
.density-sub { margin-left: auto; }
.density-sub[aria-pressed="true"] {
  background: var(--brand-wash);
  color: var(--brand-ink);
  border-color: var(--brand);
}
/* A filled pill rather than the underline the tabs use. Two underlines
   stacked read as two tab strips, which is the thing being removed. */
.subtab.on {
  background: var(--brand-wash);
  border-color: var(--brand);
  color: var(--brand-ink);
}

/* ==========================================================================
   Sales — the pipeline
   ========================================================================== */

.crm-totals {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-bottom: .9rem;
}
.crm-stat {
  flex: 1 1 8rem;
  padding: .55rem .7rem;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface);
}
.crm-stat-n {
  font-size: var(--t-lg);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.crm-stat-l { color: var(--ink-3); font-size: var(--t-2xs); }

/* Columns scroll sideways, the same shape as the task board. The page does
   not: the overflow lives here, so a long pipeline never takes the whole app
   with it. */
.crm-board {
  display: flex;
  gap: .6rem;
  align-items: flex-start;
  overflow-x: auto;
  padding-bottom: .5rem;
}
.crm-col {
  flex: 0 0 15rem;
  display: flex;
  flex-direction: column;
  gap: .4rem;
  padding: .5rem;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--sunk);
}
/* Won and lost are tinted at the column, not the card. The question asked of
   a pipeline is "where is everything", and tinting every card would make
   that harder to see rather than easier. */
.crm-col.kind-won  { background: var(--good-wash); border-color: var(--good); }
.crm-col.kind-lost { background: var(--bad-wash);  border-color: var(--bad); }

.crm-col-head {
  display: flex;
  align-items: center;
  gap: .4rem;
  font-size: var(--t-sm);
  font-weight: 700;
}
.crm-col-name { flex: 1; min-width: 0; }
.crm-col-sum {
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  font-variant-numeric: tabular-nums;
}

.crm-cards { display: flex; flex-direction: column; gap: .4rem; }
.crm-deal {
  padding: .5rem .6rem;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: var(--lift-1);
}
.crm-deal-title { font-weight: 640; font-size: var(--t-md); }
.crm-deal-meta {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem .6rem;
  margin-top: .25rem;
  color: var(--ink-3);
  font-size: var(--t-2xs);
}
.crm-deal-value {
  color: var(--ink);
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
.crm-deal-outcome {
  margin-top: .35rem;
  color: var(--ink-2);
  font-size: var(--t-xs);
  text-wrap: pretty;
}

.crm-sec { margin-top: 1.1rem; }
.crm-sec > summary { font-weight: 640; cursor: pointer; }
/* The table gets its own scroller. Five columns of contact detail do not fit
   390px, and a table that widens the page takes the app sideways with it. */
.crm-scroll { overflow-x: auto; margin-top: .5rem; }
.crm-table { width: 100%; border-collapse: collapse; font-size: var(--t-md); }
.crm-table th,
.crm-table td {
  padding: .35rem .5rem;
  border-bottom: 1px solid var(--line-soft);
  text-align: left;
  white-space: nowrap;
}
.crm-table th {
  color: var(--ink-3);
  font-size: var(--t-2xs);
  text-transform: uppercase;
  letter-spacing: .06em;
}

/* Consent, on every row. The whole reason it is stored is so whoever is
   about to email somebody can see it without going looking, which means it
   has to be legible at a glance rather than merely present. */
.consent {
  padding: .1em .4em;
  border-radius: var(--r-xs);
  font-size: var(--t-2xs);
  font-weight: 700;
}
.consent.explicit { background: var(--good-wash); color: var(--good); }
.consent.legitimate { background: var(--info-wash); color: var(--info); }
.consent.unknown { background: var(--sunk-2); color: var(--ink-3); }
.consent.declined { background: var(--bad-wash); color: var(--bad); }

/* ==========================================================================
   A card's history
   --------------------------------------------------------------------------
   Calls, emails, meetings and links, in the drawer under the fields. Kept
   visually quieter than the fields above it: this is read far more often
   than it is written, and a thread styled as loudly as the controls turns
   every drawer into a wall.
   ========================================================================== */

.act { margin-top: 1.1rem; }
.act-list { display: flex; flex-direction: column; gap: .5rem; margin: .4rem 0 .8rem; }

.act-row {
  padding: .45rem .55rem;
  border: 1px solid var(--line-soft);
  border-left: 3px solid var(--line-strong);
  border-radius: var(--r-sm);
  background: var(--sunk);
}
/* The kind is carried by the left edge rather than a coloured badge, so a
   thread of twelve entries reads as a timeline instead of a pile of chips. */
.act-row.kind-call    { border-left-color: var(--good); }
.act-row.kind-email   { border-left-color: var(--info); }
.act-row.kind-meeting { border-left-color: var(--warn); }
.act-row.kind-file    { border-left-color: var(--brand); }

.act-head {
  display: flex;
  align-items: baseline;
  gap: .4rem;
  font-size: var(--t-2xs);
  color: var(--ink-3);
}
.act-kind {
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-2);
}
.act-who { font-weight: 640; color: var(--ink-2); }
.act-with { font-style: italic; }
.act-edited { font-style: italic; }

.act-body {
  margin-top: .25rem;
  font-size: var(--t-md);
  text-wrap: pretty;
  /* Preserves the line breaks somebody typed. A pasted set of call notes
     collapsed into one paragraph is unreadable and reads as data loss. */
  white-space: pre-wrap;
}

.act-link {
  display: inline-block;
  margin-top: .3rem;
  font-size: var(--t-md);
  /* A SharePoint url is enormous and has no spaces in it. Without this it
     widens the drawer and takes the page sideways with it. */
  word-break: break-all;
}

/* The grid — line items down the side, an axis across the top.
   No new colour tokens: the cells borrow the .st-* status classes the pills
   already use, so a done cell is the same green here as everywhere else and
   both dark definitions come for free. */
.grid { border-collapse: separate; border-spacing: 0; font-size: var(--t-sm); }

/* Ruled in BOTH directions. A table with only horizontal rules reads as a
   list of rows; a spreadsheet is a lattice, and the vertical rule is most of
   what makes three hundred squares legible as a grid rather than as noise.
   This was the largest single reason the first version did not read as the
   document it replaces. */
.grid th, .grid td {
  border-bottom: 1px solid var(--line);
  border-right: 1px solid var(--line);
  padding: 0;
}

/* The row-number gutter, as a spreadsheet has one. Sticky at the far left so
   the count stays put while the companies scroll; the line-item column then
   sticks immediately after it, which is what --gnum keeps in step. */
.grid { --gnum: 2.1rem; }

.grid .grid-num {
  position: sticky;
  left: 0;
  z-index: 1;
  width: var(--gnum);
  min-width: var(--gnum);
  background: var(--sunk);
  color: var(--ink-4);
  font-size: var(--t-2xs);
  font-variant-numeric: tabular-nums;
  text-align: right;
  padding: 0 .3rem;
  /* Dragging across a sheet to read it should not select the row numbers
     along with the content. */
  user-select: none;
}
/* Above both sticky axes, like the corner below it. */
.grid thead .grid-num-h { z-index: 3; }

/* The first column stays put while the companies scroll.
   A grid whose row labels scroll off is a grid of anonymous squares — the
   one thing a wide table must never lose is what each row is. */
.grid .grid-corner,
.grid .grid-row {
  position: sticky;
  left: var(--gnum);
  z-index: 1;
  /* Opaque, not transparent: cells slide underneath this. */
  background: var(--ground);
  text-align: left;
  font-weight: 500;
  border-right: 1px solid var(--line);
  min-width: 180px;
  max-width: 320px;
}

.grid thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--ground);
  border-bottom: 1px solid var(--line);
  font-size: var(--t-2xs);
  color: var(--ink-3);
  font-weight: 620;
  letter-spacing: .02em;
  padding: .3rem .4rem;
  white-space: nowrap;
}
/* The corner sits above both sticky axes, so neither slides over it. */
.grid thead .grid-corner { z-index: 3; }

.grid-rowbtn {
  display: flex;
  align-items: center;
  gap: .4rem;
  width: 100%;
  min-height: 32px;
  padding: .25rem .4rem;
  border: 0;
  background: transparent;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.grid-rowbtn:hover { background: var(--sunk); }
.grid-rowtitle { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.grid-cell { text-align: center; }

.gcell {
  display: grid;
  place-items: center;
  /* 32px square: above the 24px WCAG 2.2 target floor, and the whole point
     of the grid is that a lot of them fit on one screen. */
  width: 100%;
  min-width: 34px;
  min-height: 32px;
  border: 0;
  background: transparent;
  color: var(--ink-3);
  font: inherit;
  font-size: var(--t-2xs);
  font-weight: 700;
  letter-spacing: .02em;
  cursor: pointer;
}
.gcell:hover { outline: 2px solid var(--brand); outline-offset: -2px; }

/* An empty square means "does not apply here", so it is quiet and inert
   until hovered. The plus only appears on approach — fifty rows of visible
   add buttons is a grid nobody can read. */
.gcell-add { color: transparent; }
.gcell-add::after { content: "+"; color: transparent; }
.gcell-add:hover::after, .gcell-add:focus-visible::after { color: var(--ink-4); }

/* On a touch screen there is no hover, so the rule above hid this control
   completely — a square you can tap and cannot see. The iPad is the primary
   device here, which made "reveal on approach" a way of shipping a feature
   nobody could find.

   Shown always, in the faintest ink that still reads, so fifty rows of them
   stay quiet enough to see the grid through. */
@media (hover: none) {
  .gcell-add::after { color: var(--line-strong); }
}

/* A blank square is focusable, so the cursor can rest on one and read "does
   not apply". It takes the same focus ring as a real cell — a cursor that
   became invisible on blanks would be a cursor you lose. */
.gcell-none { display: block; min-height: 32px; }

/* "Does not apply here", which is the commonest mark on a real close sheet.
   Grey fill, exactly as the spreadsheet shades it — the point is that an eye
   scanning a column can tell at a glance which squares are work and which
   were ruled out, without reading anything. --sunk-2 rather than a new
   token: it already carries both dark definitions. */
.gcell-na {
  display: block;
  min-height: 32px;
  background: var(--sunk-2);
  cursor: pointer;
}
.gcell-na:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.gcell-na[aria-busy="true"] { opacity: .5; }
.gcell-none:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

/* ---------------------------------------------------------------------------
   The cursor crosshair

   Which row and which column the cursor is in, said two ways at once. On a
   sheet this wide the focus ring alone is not enough: a 30px square lit
   somewhere in the middle of three hundred tells you where the cursor is
   only once you have already found it. The row wash and the lit column
   heading are how you find it.

   Wash, not a border. A border on the current row would move every other row
   by a pixel as the cursor travelled, and a sheet that shivers as you arrow
   down it is worse than no indicator.
   --------------------------------------------------------------------------- */

.grid tbody tr.cursor-row > td,
.grid tbody tr.cursor-row > th { background: var(--brand-wash); }

/* The sticky columns are opaque so cells slide under them, which means they
   do not inherit the row wash and would stay unlit. Named explicitly. */
.grid tbody tr.cursor-row > .grid-row,
.grid tbody tr.cursor-row > .grid-num { background: var(--brand-wash); }

.grid thead th.cursor-col {
  background: var(--brand-wash);
  color: var(--ink);
}

/* ---------------------------------------------------------------------------
   Compact density

   Fifty line items across ten companies is three hundred cells, and the
   spacing that makes a twelve-task board pleasant turns that into a
   scrolling exercise. This is the same screens, tighter.

   SPACING ONLY. No colour and no font-family changes, so none of this
   interacts with the theme rules or the two dark blocks — and nothing here
   makes text smaller than the type scale allows, because a dense screen
   nobody can read is not denser, it is useless.

   24px REMAINS THE FLOOR on anything you tap. WCAG 2.2 2.5.8 is not a
   comfort setting, so controls already at 24 stay there and only the ones
   above it come down: grid cells 32 -> 26, steps and dependency rows
   32 -> 26. The card footer chips do not move at all, because they are
   already at the limit.
   --------------------------------------------------------------------------- */

/* --- the board ------------------------------------------------------------
   THIS IS THE SCREEN PEOPLE JUDGE THE SETTING ON, and the first version of
   this block barely touched it: card padding and the footer gap, nothing
   else. Everything that actually drives how many cards fit — the gap between
   them, the gap inside them, the column padding, the title's line-height —
   was left alone. So pressing Compact on the board changed almost nothing,
   and it was reported as missing three times. That is the right conclusion
   to draw about a toggle whose effect you cannot see.

   A card goes from roughly 62px to roughly 40px: half again as many on the
   same screen. */
:root[data-density="compact"] .card {
  gap: .15rem;
  padding: .25rem .4rem;
  padding-left: .45rem;
  /* The lift is what makes a loose stack read as separate cards. Packed
     tight they have borders and gaps to do that, and the shadow only adds
     noise in every gap. */
  box-shadow: none;
}
:root[data-density="compact"] .card .foot { gap: .25rem; }
:root[data-density="compact"] .card .title { line-height: 1.25; }

:root[data-density="compact"] .cards { gap: .2rem; }
:root[data-density="compact"] .col { gap: .2rem; padding: .3rem; }
:root[data-density="compact"] .kanban { gap: .35rem; }
:root[data-density="compact"] .col-head { padding: .05rem .25rem 0; }

/* COLUMNS ARE NOT NARROWED, and that is a decision rather than an omission.

   Compact used to set `.kanban > .col { flex-basis: 150px }` to fit a sixth
   column on a laptop. It backfired on two of three viewports, because the
   thing a narrower column trades against is the TEXT, not the padding:
   squeeze the column and every title wraps onto a second line, and a second
   line costs more height than all the padding saved.

   Measured both times. On a phone: 71.59px comfortable against 68.89px
   compact, title going one line to two. On an iPad, worse — 82.69px against
   85.14px, so compact made the card TALLER while claiming to save space.
   Scoping it above 560px fixed the phone and left the iPad broken, which is
   the sign of a rule being chased from breakpoint to breakpoint rather than
   being wrong.

   Density here comes from padding, gaps and line-height only. Those shrink
   a card without touching how the text flows, so they behave the same at
   every width. */

/* NOTHING TAPPABLE SHRINKS. .tap and .chip in the card footer are already at
   the WCAG 2.2 2.5.8 floor of 24px, so they appear in none of the rules
   above and stay exactly as they are. Density is bought from padding and
   gaps, never from targets. */

/* 24px, which is the floor and not a preference: every square is a control
   and WCAG 2.2 2.5.8 governs it. Fifty rows at 24 is 1200px of sheet, so a
   laptop shows most of a close at once — which is the entire argument for
   replacing the spreadsheet rather than photographing it. */
:root[data-density="compact"] .gcell { min-height: 24px; min-width: 28px; }
:root[data-density="compact"] .gcell-none { min-height: 24px; }
:root[data-density="compact"] .gcell-na { min-height: 24px; }
:root[data-density="compact"] .grid-rowbtn { min-height: 24px; padding: 0 .3rem; }
:root[data-density="compact"] .grid .grid-corner,
:root[data-density="compact"] .grid .grid-row { min-width: 140px; max-width: 240px; }
:root[data-density="compact"] .grid { font-size: var(--t-2xs); }

/* The gutter narrows with everything else, and the line-item column follows
   it automatically because both read --gnum. Two digits still fit: a sheet
   that clipped its own row numbers at row 100 would be a poor advertisement
   for the idea. */
:root[data-density="compact"] .grid { --gnum: 1.7rem; }
:root[data-density="compact"] .grid .grid-num { padding: 0 .2rem; }
:root[data-density="compact"] .grid thead th { padding: .15rem .3rem; }

:root[data-density="compact"] .steps-row,
:root[data-density="compact"] .deps-row { min-height: 26px; padding: .1rem .35rem; }
:root[data-density="compact"] .steps { gap: .15rem; }
:root[data-density="compact"] .deps-list { gap: .15rem; }

/* --- the list, as a sheet --------------------------------------------------
   This is the view that answers "show me everything" on a project whose
   work is not yet a grid — seventy line items with no company columns is
   still seventy rows somebody has to read. Compact turns it from a web
   table into something closer to the spreadsheet it is replacing.

   The row floor is 24px and stays there: .rowopen is the row's primary
   control and WCAG 2.2 2.5.8 governs it. A compact row lands near 28px, so
   roughly a third more rows fit than at comfortable spacing. */
:root[data-density="compact"] .tbl td { padding: .1rem .4rem .1rem 0; }
:root[data-density="compact"] .tbl th { padding: 0 .4rem .15rem 0; }

/* Ruled in both directions, the same reasoning as the grid: horizontal
   rules alone read as a list, and the lattice is what lets an eye track
   along one row across six columns without losing its place. */
:root[data-density="compact"] .tbl th,
:root[data-density="compact"] .tbl td {
  border-right: 1px solid var(--line-soft);
}
:root[data-density="compact"] .tbl th:last-child,
:root[data-density="compact"] .tbl td:last-child { border-right: 0; }

/* Dates and refs line up digit over digit, which is most of what makes a
   column of numbers scannable rather than merely present. */
:root[data-density="compact"] .tbl { font-variant-numeric: tabular-nums; }

/* Group headings stop costing a blank line each. */
:root[data-density="compact"] .tbl .grouphead td { padding-top: .4rem; }

/* The target completion date, column B of the sheet this replaces.
   NOT sticky, deliberately: the line-item column is min-width 180 and
   max-width 320, so its rendered width is decided by content and any fixed
   left offset here would be wrong for most sheets — sticking it at a guess
   lands it on top of EG. The row header stays pinned, which is the column
   that must never scroll away; the date travels with the companies. */
.grid .grid-due {
  background: var(--ground);
  white-space: nowrap;
  padding: 0 .4rem;
  font-size: var(--t-2xs);
  font-variant-numeric: tabular-nums;
  color: var(--ink-3);
  text-align: right;
}
.grid thead .grid-due { z-index: 3; }

/* Who owns a column. Beside the company name, quieter than it: the column
   is what you are reading, the owner is a note about it. */
.grid-owner {
  margin-left: .3rem;
  font-weight: 700;
  color: var(--brand-ink);
}

/* A folder heading in the rail. Quieter than the section label above it —
   it groups projects, it is not a destination. */
.rail-folder {
  margin: .5rem 0 .15rem;
  padding: 0 .4rem;
  font-size: var(--t-2xs);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-4);
}

.gimport { display: flex; flex-direction: column; gap: .5rem; }
.gimport textarea {
  /* Monospace, because what goes in here is tab-separated columns and a
     proportional font makes a misaligned paste impossible to spot. */
  font-family: var(--mono);
  font-size: var(--t-xs);
  white-space: pre;
  overflow-x: auto;
}
.gimport .toolbar { flex-wrap: wrap; }
.gimport-out:empty { display: none; }

.grid-pick { display: flex; flex-direction: column; gap: .2rem; margin-bottom: .5rem; }
.grid-pick-row {
  display: flex;
  align-items: center;
  gap: .45rem;
  min-height: 32px;
  cursor: pointer;
}

/* Steps — the cells of a line item, seen from inside the drawer.
   No new colour tokens; --line, --sunk, --ink and --brand-ink all already
   carry their two dark definitions. */
.steps-up { display: flex; align-items: center; gap: .4rem; margin-bottom: .5rem; }

.steps-link {
  flex: 1;
  min-height: 24px;
  border: 0;
  background: transparent;
  padding: .2rem .3rem;
  border-radius: var(--r-xs);
  color: var(--brand-ink);
  font: inherit;
  font-size: var(--t-sm);
  text-align: left;
  cursor: pointer;
}
.steps-link:hover { background: var(--sunk); }

.steps { display: flex; flex-direction: column; gap: .25rem; }

.steps-row {
  display: flex;
  align-items: center;
  gap: .4rem;
  width: 100%;
  /* 32px, not 24: these are list rows a thumb aims at on a phone, and 2.5.8
     is the floor rather than the target. */
  min-height: 32px;
  padding: .25rem .4rem;
  border: 1px solid transparent;
  border-radius: var(--r-xs);
  background: transparent;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.steps-row:hover { background: var(--sunk); border-color: var(--line); }

/* The title takes the slack and truncates; the pill, ref and name must not
   shrink, or a long task title squeezes the status out of legibility. */
.steps-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.steps-new { display: flex; gap: .4rem; margin-top: .4rem; }
.steps-new .fld { flex: 1; min-width: 0; }

/* What has to happen first. Shares the steps row shape, because they are the
   same kind of thing on screen: another task, reachable from this one. */
.deps-list { display: flex; flex-direction: column; gap: .25rem; }
.deps-line { display: flex; align-items: center; gap: .25rem; }

.deps-row {
  display: flex;
  align-items: center;
  gap: .4rem;
  flex: 1;
  min-width: 0;
  min-height: 32px;
  padding: .25rem .4rem;
  border: 1px solid transparent;
  border-radius: var(--r-xs);
  background: transparent;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.deps-row:hover { background: var(--sunk); border-color: var(--line); }
.deps-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The headline, and the only part of this section that changes behaviour.
   --warn rather than --bad: being waited on is a normal state of work, not
   a fault, and colouring it like an error would train people past it. */
.deps-wait {
  margin: .4rem 0 .2rem;
  padding: .35rem .5rem;
  border-radius: var(--r-xs);
  background: var(--warn-wash);
  color: var(--warn);
  font-size: var(--t-sm);
}

.deps-new { display: flex; margin-top: .4rem; }
.deps-new .fld { flex: 1; min-width: 0; }

.shift { margin-top: .6rem; }
.shift-row { display: flex; gap: .4rem; align-items: center; }
.shift-row .fld { flex: 1; min-width: 0; }
.shift-out:empty { display: none; }
/* One line per date that would move. A list, not a count: "12 tasks will
   move" is agreed to without reading, and the point of asking is that
   somebody sees a particular ref in here and stops. */
.shift-line {
  display: flex;
  align-items: center;
  gap: .4rem;
  padding: .2rem .3rem;
  font-size: var(--t-sm);
}

/* ✎ and ×, sharing everything but what they mean on hover. 24px because
   WCAG 2.2 2.5.8 asks for that much target and these sit side by side. */
.act-edit,
.act-drop {
  flex: none;
  min-width: 24px;
  min-height: 24px;
  border: 0;
  border-radius: var(--r-xs);
  background: transparent;
  color: var(--ink-4);
  font-family: inherit;
  font-size: var(--t-md);
  line-height: 1;
  cursor: pointer;
}
/* Changing an entry is ordinary; removing one is not. Only the destructive
   half turns red. */
.act-edit:hover { background: var(--sunk); color: var(--ink-2); }
.act-drop:hover { background: var(--bad-wash); color: var(--bad); }

.act-edit-box {
  display: flex;
  flex-direction: column;
  gap: .4rem;
  margin-top: .4rem;
}

.act-new {
  display: flex;
  flex-direction: column;
  gap: .35rem;
  padding: .55rem;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
}
.act-kinds { display: flex; flex-wrap: wrap; gap: .25rem; }
.act-pick {
  min-height: 26px;
  padding: 0 .5rem;
  border: 1px solid var(--line);
  border-radius: 99px;
  background: transparent;
  color: var(--ink-3);
  font-family: inherit;
  font-size: var(--t-2xs);
  font-weight: 640;
  cursor: pointer;
}
.act-pick:hover { color: var(--ink); }
.act-pick.on {
  background: var(--brand-wash);
  border-color: var(--brand);
  color: var(--brand-ink);
}
.act-body-in { resize: vertical; min-height: 2.6rem; }

/* ==========================================================================
   Review — what came of the work
   ========================================================================== */

.rv-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .6rem;
  margin-bottom: 1rem;
}
.rv-counts { display: flex; flex-wrap: wrap; gap: .4rem; flex: 1; }
.rv-count {
  padding: .2rem .5rem;
  border: 1px solid var(--line);
  border-radius: 99px;
  color: var(--ink-2);
  font-size: var(--t-2xs);
  font-variant-numeric: tabular-nums;
}

.rv-h { font-size: var(--t-md); margin-bottom: .5rem; }
.rv-list { display: flex; flex-direction: column; gap: .5rem; }

.rv-card {
  padding: .6rem .7rem;
  border: 1px solid var(--line);
  border-left: 3px solid var(--good);
  border-radius: var(--r-md);
  background: var(--surface);
}
.rv-card-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .45rem;
  margin-bottom: .3rem;
}
.rv-card-title { flex: 1; min-width: 0; font-weight: 640; }
.rv-outcome { color: var(--ink-2); font-size: var(--t-md); text-wrap: pretty; }

/* The gap. Deliberately not styled as a warning: it is a to-do list for
   whoever wants the next review to be worth reading, not a fault. */
.rv-gap { margin-top: 1.2rem; }
.rv-gap > summary { color: var(--ink-3); font-size: var(--t-sm); cursor: pointer; }
.rv-thin {
  display: flex;
  align-items: baseline;
  gap: .45rem;
  padding: .2rem 0;
  font-size: var(--t-md);
}
.rv-thin-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.toolbar {
  display: flex;
  align-items: center;
  gap: .4rem;
  flex: none;
  flex-wrap: wrap;
  padding: .55rem 1.1rem;
}
.toolbar .spacer { flex: 1; }

.pane {
  flex: 1;
  min-height: 0;
  padding: .2rem 1.1rem 2.5rem;
  overflow: auto;
}

/* ==========================================================================
   Board
   ========================================================================== */

.kanban {
  display: flex;
  align-items: flex-start;
  gap: .6rem;
  padding-bottom: .6rem;
}
/* Shrinkable basis, not fixed columns. Six fixed tracks overflowed the
   viewport with no affordance and simply hid Done and Cancelled. */
.kanban > .col { flex: 1 1 180px; min-width: 164px; max-width: 320px; }

.col {
  display: flex;
  flex-direction: column;
  gap: .35rem;
  padding: var(--pad-col);
  border-radius: var(--r-lg);
  background: var(--sunk);
  transition-property: background-color, box-shadow;
  transition-duration: var(--fast);
}
.col.drop { background: var(--brand-wash); box-shadow: inset 0 0 0 2px var(--brand); }

.col-head {
  display: flex;
  align-items: center;
  gap: .4rem;
  padding: .15rem .3rem .1rem;
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.col-head .count {
  min-width: 1.1rem;
  padding: .05em .3em;
  border-radius: 99px;
  background: var(--sunk-2);
  font-size: var(--t-2xs);
  text-align: center;
}

.cards { display: flex; flex-direction: column; gap: .35rem; min-height: 30px; }

/* Concentric: column radius 13 minus its 6px padding gives the card 7.
   Parallel corners are the difference between "nested" and "stacked". */
.card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: .35rem;
  padding: .5rem .55rem;
  padding-left: .6rem;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--surface);
  box-shadow: var(--lift-1);
  cursor: pointer;
  transition-property: border-color, box-shadow, transform;
  transition-duration: var(--fast);
  transition-timing-function: ease-out;
}
.card:hover { border-color: var(--line-strong); box-shadow: var(--lift-2); }
.card:active { transform: scale(.995); }
.card.dragging { opacity: .35; }

/* Priority reads as a stripe, not a number. Shape carries state faster than
   text does when you are scanning forty cards. */
.card::before {
  content: "";
  position: absolute;
  left: 0;
  top: 7px;
  bottom: 7px;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: transparent;
}
.card.p3::before { background: var(--warn); }
.card.p4::before { background: var(--bad); }

/* A <button>, not a <div>, since the chips arrived.
   The card can no longer be role="button" — a button may not contain buttons —
   so the title is the focusable control that opens the task. The first block
   below is the reset that keeps it looking exactly like the text it replaced;
   the second is the original styling, unchanged. */
.card .title {
  appearance: none;
  border: 0;
  background: none;
  padding: 0;
  text-align: left;
  font-family: inherit;
  color: inherit;
  cursor: pointer;
  /* Fills the card so the target matches the text block a user aims at, rather
     than being a shrink-wrapped inline box. */
  display: block;
  width: 100%;

  font-size: var(--t-md);
  font-weight: 550;
  line-height: 1.35;
  overflow-wrap: anywhere;
  text-wrap: pretty;
}
.card .foot { display: flex; align-items: center; gap: .35rem; flex-wrap: wrap; }

.ref {
  font-family: var(--mono);
  font-size: var(--t-2xs);
  font-variant-numeric: tabular-nums;
  color: var(--ink-4);
  letter-spacing: -.02em;
}

/* How much has been said on a card. Quiet on purpose: it is a signal that
   there is something to read, not a number anybody acts on. No new colour —
   --ink-4, --sunk and --line all already have their two dark definitions. */
.said {
  font-size: var(--t-2xs);
  font-variant-numeric: tabular-nums;
  color: var(--ink-4);
  background: var(--sunk);
  border: 1px solid var(--line);
  border-radius: 99px;
  padding: 0 .3rem;
  line-height: 1.5;
  white-space: nowrap;
}

.avatar {
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  border-radius: 99px;
  background: var(--brand-wash);
  color: var(--brand-ink);
  font-size: var(--t-2xs);
  font-weight: 700;
  letter-spacing: -.02em;
}

/* The signed-in identity, in place of the development actor picker. */
.who {
  display: flex;
  align-items: center;
  gap: .45rem;
  min-width: 0;          /* lets .ellip actually truncate inside a flex row */
  max-width: 15rem;
  font-size: var(--t-xs);
}
/* Slightly larger than a card avatar: this one is a persistent identity
   indicator rather than a repeated inline marker. */
.who .avatar { width: 22px; height: 22px; flex: none; }
.who .ellip { min-width: 0; }

/* Below the rail breakpoint the header is tight, and a name is the first
   thing that can go: the avatar still says who you are, and Sign out is the
   control you actually came for. */
@media (max-width: 700px) {
  .who { max-width: none; }
  .who .ellip { display: none; }
}

.when {
  font-family: var(--mono);
  font-size: var(--t-2xs);
  font-variant-numeric: tabular-nums;
  color: var(--ink-3);
}
.when.late { color: var(--bad); font-weight: 700; }

/* --- card action chips ----------------------------------------------------
   The avatar and the due pill the card already draws, wrapped in real buttons
   so they can be tapped to change what they show. Plus one new glyph for
   status, which is the single-pointer alternative to dragging the card.

   Styled to be INVISIBLE at rest. The card is meant to look exactly as it did;
   these are affordances that surface under a cursor and are always there for a
   finger, not a new row of controls. Anything else would trade the tablet fix
   for the clutter the same review complained about.                          */

.card .foot .tap {
  display: inline-grid;
  place-items: center;
  border: 0;
  background: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  border-radius: 6px;

  /* WCAG 2.2 raised target size to a 24x24 minimum at AA (2.5.8), and the
     avatar is 18px. Sizing the BUTTON rather than the avatar keeps the mark the
     size it was designed to be while giving a fingertip something to land on.
     That is why this is a wrapper and not a class on the avatar itself. */
  min-width: 24px;
  min-height: 24px;
}

/* The action chip sits at the trailing edge, so the card's centre stays inert
   and a click in the middle still opens the task. See the comment in board.js:
   this was measured, not a preference. */
.card .foot .tap.end { margin-left: auto; }

.card .foot .tap:hover { background: var(--sunk); }

/* Matches the press feedback on .chip, so the two read as the same kind of
   control even though they are styled for different surroundings. */
.card .foot .tap:active { transform: scale(.94); }

/* Focus is handled by the global :focus-visible rule near the top of this file.
   Do not add a second outline here — two rules drifted apart is how a focus
   ring ends up invisible on exactly one control. */

.card .foot .tap .glyph {
  font-size: var(--t-2xs);
  color: var(--ink-4);
  line-height: 1;
}

/* --- label chips --------------------------------------------------------- */

.tag {
  display: inline-flex;
  align-items: center;
  gap: .25em;
  padding: .1em .42em;
  border-radius: var(--r-xs);
  font-size: var(--t-2xs);
  font-weight: 650;
  line-height: 1.5;
  white-space: nowrap;
  background: var(--sw-slate-bg);
  color: var(--sw-slate);
}
.tag.slate  { background: var(--sw-slate-bg);  color: var(--sw-slate); }
.tag.red    { background: var(--sw-red-bg);    color: var(--sw-red); }
.tag.amber  { background: var(--sw-amber-bg);  color: var(--sw-amber); }
.tag.green  { background: var(--sw-green-bg);  color: var(--sw-green); }
.tag.teal   { background: var(--sw-teal-bg);   color: var(--sw-teal); }
.tag.blue   { background: var(--sw-blue-bg);   color: var(--sw-blue); }
.tag.violet { background: var(--sw-violet-bg); color: var(--sw-violet); }
.tag.pink   { background: var(--sw-pink-bg);   color: var(--sw-pink); }

.dot.slate  { background: var(--sw-slate-dot); }
.dot.red    { background: var(--sw-red-dot); }
.dot.amber  { background: var(--sw-amber-dot); }
.dot.green  { background: var(--sw-green-dot); }
.dot.teal   { background: var(--sw-teal-dot); }
.dot.blue   { background: var(--sw-blue-dot); }
.dot.violet { background: var(--sw-violet-dot); }
.dot.pink   { background: var(--sw-pink-dot); }

/* --- status pills -------------------------------------------------------- */

.pill {
  display: inline-block;
  padding: .12em .45em;
  border-radius: var(--r-xs);
  font-size: var(--t-2xs);
  font-weight: 700;
  white-space: nowrap;
}
.st-backlog, .st-todo      { background: var(--sunk-2);   color: var(--ink-3); }
.st-in_progress            { background: var(--brand-wash); color: var(--brand-ink); }
.st-blocked                { background: var(--bad-wash);  color: var(--bad); }
.st-done                   { background: var(--good-wash); color: var(--good); }
.st-cancelled              { background: var(--sunk-2);    color: var(--ink-4); }

/* ==========================================================================
   List
   ========================================================================== */

.tbl { width: 100%; border-collapse: collapse; font-size: var(--t-md); }

/* The header stays while the rows go past.
   Seventy line items is five screens, and a column header that scrolls away
   leaves five of the six columns as unlabelled text — which is the moment a
   list stops being a table and becomes a wall. Sticky against .pane, which
   is the scroll container. Opaque, because rows pass underneath it. */
.tbl thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--ground);
}

.tbl th {
  padding: 0 .6rem .4rem 0;
  border-bottom: 1px solid var(--line);
  font-size: var(--t-2xs);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-4);
  text-align: left;
  white-space: nowrap;
}
.tbl td {
  padding: .4rem .6rem .4rem 0;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: middle;
}
.tbl th:last-child, .tbl td:last-child { padding-right: 0; }
.tbl tbody tr {
  cursor: pointer;
  transition-property: background-color;
  transition-duration: var(--fast);
}
.tbl tbody tr:hover { background: var(--raise); }
.tbl .grouphead td {
  padding-top: 1rem;
  border-bottom: 1px solid var(--line);
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.cellwrap { display: flex; align-items: center; gap: .4rem; min-width: 0; }
.ellip { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ==========================================================================
   Timeline
   ========================================================================== */

.gantt { min-width: 680px; }
.gantt-head {
  display: grid;
  align-items: end;
  padding-bottom: .3rem;
  border-bottom: 1px solid var(--line-strong);
  margin-bottom: .4rem;
}
.gantt-row {
  display: grid;
  align-items: center;
  min-height: 2rem;
  border-bottom: 1px solid var(--line-soft);
}
.gantt-row:hover { background: var(--raise); }
.gantt-lab { padding-right: .6rem; overflow: hidden; }
.gantt-lab .t {
  font-size: var(--t-sm);
  font-weight: 570;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tick {
  padding-left: .2rem;
  border-left: 1px solid var(--line-soft);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  color: var(--ink-4);
}
/* .bar, .cal-ev and .rowopen are <button> elements.
   A div with only a click handler cannot be reached from a keyboard, which
   left the list, timeline and calendar views readable but not operable without
   a mouse. These reset the button chrome ONLY — background and colour stay with
   the rules below and the b-<status> modifiers, because those carry meaning. */
.bar, .cal-ev, .rowopen {
  appearance: none;
  border: 0;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

/* The list row's title. Transparent by design: the row already looks clickable,
   so this adds a focus target, not a second visual control. */
.rowopen {
  padding: 0;
  background: none;
  color: inherit;
  font-size: inherit;
  font-weight: inherit;
  min-width: 0;   /* lets the .ellip child actually truncate inside a flex row */
}

.bar {
  display: flex;
  align-items: center;
  height: 1.2rem;
  padding: 0 .35rem;
  border-radius: var(--r-xs);
  font-size: var(--t-2xs);
  font-weight: 700;
  color: #fff;
  white-space: nowrap;
  overflow: hidden;
  cursor: pointer;
  transition-property: filter, transform;
  transition-duration: var(--fast);
}
.bar:hover { filter: brightness(1.08); }

/* Waiting on something unfinished. A striped left edge rather than a colour,
   because the bar's colour already means its status and overloading it would
   make "blocked" and "waiting for somebody else" indistinguishable. Drawn
   inside the bar so it cannot disturb the grid. */
.bar.waiting {
  background-image: repeating-linear-gradient(
    135deg,
    rgb(255 255 255 / .45) 0 3px,
    transparent 3px 7px
  );
  background-repeat: no-repeat;
  background-size: 14px 100%;
}

/* Lit because the bar being pointed at waits on this one. An outline rather
   than a fill: the bar keeps saying what its status is while it answers a
   different question. */
.bar.lit {
  outline: 2px solid var(--warn);
  outline-offset: 1px;
  filter: brightness(1.12);
}
.bar.b-done        { background: var(--good); }
.bar.b-in_progress { background: var(--brand); color: var(--on-brand); }
.bar.b-blocked     { background: var(--bad); }
/* Not-yet-started work reads as an outline rather than a fill. A mid-grey
   fill with white text measured about 2.2:1 — unreadable, and the shape now
   carries "not started" as well as the colour does. */
.bar.b-todo, .bar.b-backlog {
  background: var(--sunk-2);
  color: var(--ink-2);
  box-shadow: inset 0 0 0 1px var(--line-strong);
}
.bar.b-cancelled {
  background: transparent;
  color: var(--ink-4);
  box-shadow: inset 0 0 0 1px var(--line);
  text-decoration: line-through;
}
/* align-self: stretch is load-bearing. The row centres its items, and a
   border-only element has no intrinsic height — so without this the today
   marker collapses to nothing and never appears at all. */
.nowline {
  grid-row: 1 / -1;
  align-self: stretch;
  border-left: 2px solid var(--bad);
  opacity: .55;
}

/* ==========================================================================
   Calendar
   ========================================================================== */

.cal { display: flex; flex-direction: column; gap: .3rem; }
.cal-dow {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 3px;
}
.cal-dow span {
  padding: .1rem .35rem;
  font-size: var(--t-2xs);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-4);
}
.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 3px;
}
.cal-day {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-height: 96px;
  padding: 4px;
  border: 1px solid var(--line-soft);
  border-radius: var(--r-sm);
  background: var(--surface);
  transition-property: background-color, border-color;
  transition-duration: var(--fast);
}
.cal-day:hover { border-color: var(--line-strong); }
.cal-day.out { background: var(--ground); opacity: .6; }
.cal-day.today { border-color: var(--brand); box-shadow: inset 0 0 0 1px var(--brand); }
.cal-daynum {
  align-self: flex-start;
  padding: 0 .2rem;
  font-size: var(--t-xs);
  font-weight: 650;
  color: var(--ink-3);
}
.cal-day.today .cal-daynum { color: var(--brand); }
.cal-ev {
  display: flex;
  align-items: center;
  gap: .25rem;
  padding: 1px 4px;
  border-radius: var(--r-xs);
  background: var(--sunk);
  font-size: var(--t-2xs);
  font-weight: 550;
  line-height: 1.45;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
  transition-property: background-color;
  transition-duration: var(--fast);
}
.cal-ev:hover { background: var(--sunk-2); }
.cal-ev.done { opacity: .5; text-decoration: line-through; }
.cal-more { padding: 0 .25rem; font-size: var(--t-2xs); color: var(--ink-4); }

/* ==========================================================================
   Notes
   ========================================================================== */

.note-grid {
  columns: 260px;
  column-gap: .6rem;
}
.note {
  break-inside: avoid;
  display: flex;
  flex-direction: column;
  gap: .35rem;
  margin-bottom: .6rem;
  padding: .65rem .7rem;
  border: 1px solid var(--line);
  border-left: 3px solid var(--sw-slate-dot);
  border-radius: var(--r);
  background: var(--surface);
  box-shadow: var(--lift-1);
  cursor: pointer;
  transition-property: box-shadow, border-color, transform;
  transition-duration: var(--fast);
}
.note:hover { box-shadow: var(--lift-2); }
.note.slate  { border-left-color: var(--sw-slate-dot); }
.note.red    { border-left-color: var(--sw-red-dot); }
.note.amber  { border-left-color: var(--sw-amber-dot); }
.note.green  { border-left-color: var(--sw-green-dot); }
.note.teal   { border-left-color: var(--sw-teal-dot); }
.note.blue   { border-left-color: var(--sw-blue-dot); }
.note.violet { border-left-color: var(--sw-violet-dot); }
.note.pink   { border-left-color: var(--sw-pink-dot); }

.note .nt {
  font-size: var(--t-md);
  font-weight: 620;
  line-height: 1.35;
  text-wrap: pretty;
  overflow-wrap: anywhere;
}
.note .nb {
  font-size: var(--t-sm);
  color: var(--ink-2);
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  text-wrap: pretty;
}
.note .nf {
  display: flex;
  align-items: center;
  gap: .35rem;
  flex-wrap: wrap;
  padding-top: .1rem;
  color: var(--ink-4);
  font-size: var(--t-2xs);
}

/* Pushed to the far end so it reads as "actions for this card" rather than as
   another tag among the tags.
   Always visible, never hover-revealed: hover does not exist on the tablet this
   is mostly used from, and a control that only appears on hover is exactly how
   Route-to-project came to be unreachable there in the first place.
   Sizing and the 40px hit area come from .icon-btn. */
.note-more { margin-left: auto; }

/* --------------------------------------------------------------------------
   The generated prompt
   --------------------------------------------------------------------------
   Replaces a readonly <textarea> that was effectively unscrollable on a tablet.
   Three rules do the work, and each is load-bearing:

   max-height in vh, not a rem min-height. The old box was 22rem TALL MINIMUM,
   so on a short screen it filled the viewport and left nowhere to touch that
   would scroll the page instead of the box.

   overscroll-behavior: auto. Explicit rather than assumed: when this element's
   own scroll reaches its end, the gesture must continue to the page. `contain`
   here would recreate the exact trap being removed.

   pre-wrap plus anywhere. A prompt contains long unbroken lines (addresses,
   task refs); without wrapping this scrolls sideways, which on a touch device
   competes with the vertical gesture and makes both feel broken.
   -------------------------------------------------------------------------- */

.promptbox {
  margin: 0;
  max-height: 45vh;
  overflow: auto;
  overscroll-behavior: auto;
  padding: .7rem .8rem;
  background: var(--sunk);
  border: 1px solid var(--line);
  border-radius: var(--r);
  font-family: var(--mono);
  font-size: var(--t-sm);
  line-height: 1.55;
  color: var(--ink);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  /* Selectable, because the copy button's fallback selects it and because
     people reasonably drag-select text they intend to copy. */
  user-select: text;
}
.promptbox:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* --------------------------------------------------------------------------
   Unanswered — questions nobody came back to
   --------------------------------------------------------------------------
   Same shape as the note echo, and that repetition is deliberate: both say
   "here is a number of days, and here is the thing it belongs to". One learned
   pattern, two features.
   -------------------------------------------------------------------------- */

.q-slot { margin-top: 1.6rem; }

.q-head {
  display: flex;
  align-items: baseline;
  gap: .5rem;
  flex-wrap: wrap;
  margin-bottom: .6rem;
}
.q-count {
  font-size: 1.6rem;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
/* Two tones, one renderer. Unanswered is a nag (warn); Drift is an observation
   (info). Both are SEMANTIC colours, deliberately separate from the brand
   accent, so neither reads as a promoted feature. */
.q-count.warn { color: var(--warn); }
.q-count.info { color: var(--info); }
.q-lbl { color: var(--ink-2); font-size: var(--t-md); text-wrap: pretty; }

.q-list { display: flex; flex-direction: column; gap: .4rem; }

.q-row {
  display: flex;
  align-items: flex-start;
  gap: .7rem;
  width: 100%;
  min-height: 56px;
  padding: .55rem .7rem;
  text-align: left;
  font: inherit;
  background: var(--surface);
  border: 1px solid var(--line);
  /* The left rule carries the tone. Width as well as colour, so the two kinds
     stay distinguishable in greyscale and to a colour-blind reader. */
  border-left: 3px solid var(--line-strong);
  border-radius: var(--r);
  cursor: pointer;
  transition-property: border-color, box-shadow;
  transition-duration: var(--fast);
}
.q-row.warn { border-left-color: var(--warn); }
.q-row.info { border-left-color: var(--info); }
.q-row:hover { box-shadow: var(--lift-1); }
.q-row:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.q-age {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 2.8rem;
  color: var(--ink-3);
}
.q-age.warn { color: var(--warn); }
.q-age.info { color: var(--info); }
.q-days {
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
}
.q-unit {
  font-size: var(--t-2xs);
  text-transform: uppercase;
  letter-spacing: .05em;
}

.q-body { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.q-text {
  font-size: var(--t-md);
  color: var(--ink);
  text-wrap: pretty;
  /* Two lines then ellipsis. A question long enough to need three lines is one
     the reader will open rather than skim. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.q-src { font-size: var(--t-2xs); color: var(--ink-3); }
.q-more { margin-top: .5rem; font-size: var(--t-sm); }

@media (prefers-reduced-motion: reduce) {
  .q-row { transition: none; }
}

/* --------------------------------------------------------------------------
   The note echo — you already wrote this down
   --------------------------------------------------------------------------
   The AGE is the datum, so it leads: a big tabular number on the left, the note
   itself beside it. "A note from last Tuesday" is a search result; "164 days
   before this task existed" is a fact about yourself, and the layout has to put
   the number where the eye lands first or the point is lost.
   -------------------------------------------------------------------------- */

.echoes { display: flex; flex-direction: column; gap: .4rem; }

.echo-card {
  display: flex;
  align-items: flex-start;
  gap: .7rem;
  width: 100%;
  min-height: 56px;
  padding: .6rem .7rem;
  text-align: left;
  font: inherit;
  background: var(--brand-wash);
  border: 1px solid transparent;
  border-radius: var(--r);
  cursor: pointer;
  transition-property: border-color, background-color;
  transition-duration: var(--fast);
}
.echo-card:hover { border-color: var(--brand); }
.echo-card:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.echo-age {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 3.4rem;
  color: var(--brand-ink);
}
.echo-days {
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -.02em;
  /* The number sits in a fixed-width column beside prose; without this it
     jiggles the label under it every time the digit count changes. */
  font-variant-numeric: tabular-nums;
}
.echo-unit {
  font-size: var(--t-2xs);
  text-transform: uppercase;
  letter-spacing: .05em;
  text-align: center;
  line-height: 1.15;
}

.echo-body { display: flex; flex-direction: column; gap: .12rem; min-width: 0; }
.echo-title { font-size: var(--t-md); font-weight: 600; color: var(--ink); }
.echo-excerpt {
  font-size: var(--t-sm);
  color: var(--ink-2);
  /* Two lines then ellipsis: enough to recognise the thought, not enough to
     turn a drawer panel into a reading surface. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-wrap: pretty;
}
.echo-when { font-size: var(--t-2xs); color: var(--ink-3); }

/* The Link button. Pushed to the end and always visible — never hover-revealed,
   because the device this is used from has no hover, and because a panel whose
   only action is hidden is the panel that stayed trivia for two releases. */
.echo-link { flex: none; margin-left: auto; align-self: center; }

@media (prefers-reduced-motion: reduce) {
  .echo-card { transition: none; }
}

/* --------------------------------------------------------------------------
   Slip — how far each deadline has moved
   --------------------------------------------------------------------------
   The bar length is the datum, so this has to read at a distance: the headline
   number is large and nothing whatsoever is hover-revealed. Both are
   requirements rather than taste — it is used from a tablet, where hover does
   not exist, and it is meant to survive being put on a wall screen where nobody
   is holding a pointer.
   -------------------------------------------------------------------------- */

.slip-head {
  display: flex;
  align-items: baseline;
  gap: .6rem;
  flex-wrap: wrap;
  padding: .2rem 0 1rem;
}
.slip-total {
  font-size: clamp(2rem, 6vw, 3.2rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -.03em;
  color: var(--bad);
  /* The number changes as dates move; without this the label beside it jumps
     sideways every time it crosses a digit width. */
  font-variant-numeric: tabular-nums;
}
.slip-total-lbl { color: var(--ink-2); font-size: var(--t-md); text-wrap: pretty; }

.slip-list { display: flex; flex-direction: column; gap: .5rem; }

.slip-row {
  display: flex;
  flex-direction: column;
  gap: .3rem;
  width: 100%;
  /* 56px floor. The whole row is the target, not the bar — a 10px bar is not a
     tap target, and this view has no other way in. */
  min-height: 56px;
  padding: .6rem .75rem;
  text-align: left;
  font: inherit;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  cursor: pointer;
  transition-property: border-color, box-shadow;
  transition-duration: var(--fast);
}
.slip-row:hover { border-color: var(--line-strong); box-shadow: var(--lift-1); }
.slip-row:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.slip-top { display: flex; align-items: baseline; gap: .5rem; }
.slip-title {
  flex: 1;
  font-size: var(--t-md);
  font-weight: 560;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.slip-count {
  flex: none;
  font-size: var(--t-2xs);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

.slip-track {
  height: 10px;
  background: var(--sunk);
  border-radius: 99px;
  overflow: hidden;
}
.slip-bar {
  height: 100%;
  border-radius: 99px;
  /* Neutral by default: a task whose date never moved still draws a bar, and it
     must not be coloured as though something were wrong. */
  background: var(--line-strong);
}
/* Semantic, not decorative — the good/bad channel, deliberately separate from
   the brand accent. */
.slip-bar.late  { background: var(--bad); }
.slip-bar.early { background: var(--good); }

.slip-foot {
  display: flex;
  align-items: baseline;
  gap: .4rem;
  flex-wrap: wrap;
  font-size: var(--t-sm);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.slip-arrow { color: var(--ink-4); }
.slip-to { color: var(--ink-2); }
.slip-days { margin-left: auto; font-weight: 600; }
.slip-days.late  { color: var(--bad); }
.slip-days.early { color: var(--good); }

/* An assumed first date is marked in TEXT, not only by colour. The row makes a
   claim about history, and the reader has to see which part of it was guessed —
   greying it alone would read as merely less important. */
.slip-from.assumed { color: var(--ink-4); }
.slip-assumed {
  font-size: var(--t-2xs);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--warn);
}

.slip-note { margin-top: 1rem; font-size: var(--t-sm); text-wrap: pretty; }

@media (prefers-reduced-motion: reduce) {
  .slip-row { transition: none; }
}

.capture {
  display: flex;
  flex-direction: column;
  gap: .45rem;
  margin-bottom: .9rem;
  padding: .6rem;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--lift-1);
}
.capture textarea {
  min-height: 3.2rem;
  border: 0;
  padding: .2rem .3rem;
  font-family: var(--sans);
  font-size: var(--t-base);
  resize: none;
  background: transparent;
}
.capture textarea:focus { outline: none; box-shadow: none; }
.capture .row { display: flex; align-items: center; gap: .35rem; flex-wrap: wrap; }

.swatches { display: flex; gap: .25rem; }
.swatch {
  position: relative;
  width: 18px;
  height: 18px;
  padding: 0;
  border: 2px solid transparent;
  border-radius: 99px;
  cursor: pointer;
  transition-property: transform, border-color;
  transition-duration: var(--fast);
}
.swatch::after { content: ""; position: absolute; inset: -8px; }
.swatch:hover { transform: scale(1.14); }
.swatch.on { border-color: var(--ink); }

/* ==========================================================================
   Pages / documents
   ========================================================================== */

.doc { max-width: 46rem; }
.doc h2 {
  margin: 1.3rem 0 .35rem;
  font-size: var(--t-lg);
}
.blk { padding: .1rem 0; font-size: var(--t-base); text-wrap: pretty; }
.blk-heading { margin-top: .7rem; font-size: var(--t-lg); font-weight: 680; }
.blk-quote {
  padding-left: .65rem;
  border-left: 3px solid var(--line-strong);
  color: var(--ink-2);
  font-style: italic;
}
.blk-callout {
  margin: .4rem 0;
  padding: .55rem .7rem;
  border-left: 3px solid var(--warn);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  background: var(--warn-wash);
  color: var(--ink);
}
.blk-code {
  padding: .55rem .65rem;
  border-radius: var(--r-sm);
  background: var(--sunk);
  font-family: var(--mono);
  font-size: var(--t-sm);
  white-space: pre-wrap;
  overflow-x: auto;
  text-wrap: initial;
}
.blk-todo { display: flex; gap: .4rem; align-items: flex-start; }
.blk-todo .tick { flex: none; color: var(--ink-3); }
.blk-todo .struck { color: var(--ink-3); text-decoration: line-through; }
.blk-bullet { display: flex; gap: .45rem; align-items: baseline; }
.blk-bullet .dot-mark { flex: none; color: var(--ink-4); }

/* Author-supplied owner or date on a checklist line. A hint, not a task —
   styled quietly so it never reads as an assignment the board knows about. */
.hint {
  margin-left: .35rem;
  padding: .05em .35em;
  border-radius: var(--r-xs);
  background: var(--sunk);
  color: var(--ink-3);
  font-size: var(--t-2xs);
  white-space: nowrap;
}

/* Heading levels. Every heading rendered at one size before this, so a
   four-level document arrived completely flat. */
.blk-heading.h1 { font-size: var(--t-xl); font-weight: 700; margin-top: 1.1rem; }
.blk-heading.h2 { font-size: var(--t-lg); }
.blk-heading.h3 { font-size: var(--t-base); }
.blk-heading.h4 {
  font-size: var(--t-sm);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.blk-callout.tone-info    { border-left-color: var(--info); background: var(--info-wash); }
.blk-callout.tone-success { border-left-color: var(--good); background: var(--good-wash); }
.blk-callout.tone-danger  { border-left-color: var(--bad);  background: var(--bad-wash); }

/* --------------------------------------------------------------------------
   Inline spans
   -------------------------------------------------------------------------- */

.lnk {
  color: var(--brand);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
  transition-property: color;
  transition-duration: var(--fast);
  transition-timing-function: var(--ease);
}
.lnk:hover { color: var(--brand-hi); }

/* A link the allowlist refused. Dotted, not coloured — it must not look
   clickable, but it must not look like ordinary prose either, or nobody ever
   learns why the thing they pasted did nothing. */
.lnk-off {
  color: var(--ink-2);
  text-decoration: underline dotted var(--line-strong);
  text-underline-offset: 2px;
}
.lnk-why {
  margin-left: .15em;
  color: var(--warn);
  font-size: var(--t-sm);
  cursor: help;
}

code.ic {
  padding: .08em .3em;
  border-radius: var(--r-xs);
  background: var(--sunk);
  font-family: var(--mono);
  font-size: .92em;
}

/* --------------------------------------------------------------------------
   Schedule block — a page's tasks, as the tasks themselves
   -------------------------------------------------------------------------- */

.blk-sched {
  margin: .5rem 0;
  padding: .5rem .6rem .6rem;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--raise);
}
.sched-head {
  display: flex;
  align-items: baseline;
  gap: .5rem;
  margin-bottom: .45rem;
}
.sched-title { font-weight: 660; font-size: var(--t-md); color: var(--ink); }
.sched-head .muted { font-size: var(--t-sm); font-variant-numeric: tabular-nums; }

.chips { display: flex; flex-wrap: wrap; gap: .3rem; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  /* 26px, not the 20px this wanted to be: WCAG 2.2 SC 2.5.8 puts the floor at
     24x24 CSS px, and these are the primary way into a task from a page. */
  min-height: 26px;
  padding: .15rem .5rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--t-sm);
  text-align: left;
  cursor: pointer;
  transition-property: background-color, border-color, transform, box-shadow;
  transition-duration: var(--fast);
  transition-timing-function: var(--ease);
}
.chip:hover { border-color: var(--line-strong); box-shadow: var(--lift-1); }
.chip:active { transform: scale(.97); }
.chip.quiet { background: transparent; }
@media (prefers-reduced-motion: reduce) {
  .chip:active { transform: none; }
}

.task-chip .chip-ref {
  font-family: var(--mono);
  font-size: var(--t-2xs);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.task-chip .chip-name {
  max-width: 15rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.task-chip .chip-due {
  color: var(--ink-3);
  font-size: var(--t-2xs);
  font-variant-numeric: tabular-nums;
}
/* Status reads as a stripe rather than a fill, so a row of twelve chips does
   not become twelve coloured blocks with the titles fighting them. */
.task-chip { border-left-width: 3px; }
.task-chip.st-in_progress { border-left-color: var(--brand); }
.task-chip.st-blocked     { border-left-color: var(--bad); }
.task-chip.st-done        { border-left-color: var(--good); }
.task-chip.st-done .chip-name { color: var(--ink-3); text-decoration: line-through; }
.task-chip.st-cancelled   { border-left-color: var(--line-strong); }
.task-chip.st-cancelled .chip-name { color: var(--ink-3); }

/* --------------------------------------------------------------------------
   Archived projects — the filing cabinet at the foot of the rail
   -------------------------------------------------------------------------- */

.archived-sec > summary {
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.archived-sec > summary::-webkit-details-marker { display: none; }
/* A disclosure triangle that turns, drawn rather than inherited so it matches
   the rail's other glyphs at this size. */
.archived-sec > summary::before {
  content: '▸';
  display: inline-block;
  margin-right: .35rem;
  transition: transform var(--fast) var(--ease);
}
.archived-sec[open] > summary::before { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) {
  .archived-sec > summary::before { transition: none; }
}

.archived-row {
  display: flex;
  align-items: center;
  gap: .4rem;
  padding: .2rem .1rem .2rem 1rem;
}
.archived-row .nm {
  overflow: hidden;
  color: var(--ink-3);
  font-size: var(--t-sm);
  text-overflow: ellipsis;
  white-space: nowrap;
}
.archived-row .btn { margin-left: auto; flex: none; }

/* --------------------------------------------------------------------------
   Membership — adding people, and the ones not here yet
   -------------------------------------------------------------------------- */

.invite {
  margin-bottom: 1.1rem;
  padding: .7rem .8rem .8rem;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--raise);
}
.invite-row {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  align-items: center;
}
/* The address is the point of the row, so it takes the space. Below the
   breakpoint everything stacks rather than shrinking to unusable widths. */
.invite-row input[type="email"] { flex: 1 1 16rem; min-width: 0; }
.invite-row select { flex: 0 1 15rem; min-width: 0; }
.invite .muted { margin: .5rem 0 0; font-size: var(--t-sm); text-wrap: pretty; }

@media (max-width: 560px) {
  .invite-row { flex-direction: column; align-items: stretch; }
  .invite-row .btn { width: 100%; }
}

.fld.sm { padding: .25rem .4rem; font-size: var(--t-sm); }

/* Someone invited who has not signed in yet. Dashed, because they are a
   placeholder for a person rather than a person — an owner scanning this list
   needs to see at a glance which rows are real access and which are waiting on
   somebody else to act. */
.person.pending {
  border-bottom-style: dashed;
  opacity: .82;
}
.person.pending .nm { font-family: var(--mono); font-size: var(--t-sm); }

/* --------------------------------------------------------------------------
   AI helper — mode picker and what the prompt knows
   -------------------------------------------------------------------------- */

.modes {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem;
  margin: .9rem 0 .5rem;
}
.mode-chip {
  padding: .3rem .65rem;
  font-weight: 560;
}
.mode-chip.on {
  border-color: var(--brand);
  background: var(--brand-wash);
  color: var(--brand-ink);
}
/* The selected chip is not signalled by colour alone — a radiogroup whose only
   difference is a tint fails for anyone who cannot see the tint. */
.mode-chip.on::before { content: '✓'; margin-right: .1rem; font-size: var(--t-2xs); }

/* What the assembled prompt actually carries. The roster being real is the
   whole point of this screen, and a prompt containing it looks exactly like one
   that does not until an import comes back with nothing assigned. */
.knows {
  display: flex;
  flex-wrap: wrap;
  gap: .3rem;
  margin: .6rem 0;
}
.knows-bit {
  padding: .15em .5em;
  border-radius: var(--r-xs);
  background: var(--good-wash);
  color: var(--good);
  font-size: var(--t-2xs);
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   Page navigation
   -------------------------------------------------------------------------- */

.contents {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .3rem;
  margin-bottom: .9rem;
  padding-bottom: .7rem;
  border-bottom: 1px solid var(--line-soft);
}
.contents .lbl { margin: 0 .3rem 0 0; }

.page-sec { scroll-margin-top: 1rem; border-radius: var(--r); }
.page-head { display: flex; align-items: center; gap: .5rem; }
.page-head h2 { flex: none; }

/* "That one, there."
   --------------------------------------------------------------------------
   Applied by applyFlash() in core.js after a repaint, to whichever element
   carries the matching data-flash. Marks the row the user just acted on, which
   the repaint would otherwise make indistinguishable from every other row.

   Deliberately an OUTLINE and nothing else. An outline is painted outside the
   border box, so it cannot shift layout, and it does not touch the element's
   own background or box-shadow — which means a card keeps its lift and its
   surface for the whole animation instead of snapping back to them at the end.
   That snap is what ruled out animating background/box-shadow directly, and it
   is why this replaced the .page-sec-only `land` rule rather than extending it:
   one mechanism that works on a card, a rail row or a page section alike. */
.flash {
  outline: 2px solid transparent;
  outline-offset: 2px;
  animation: flash 1.4s var(--ease);
}
@keyframes flash {
  from { outline-color: var(--brand); }
  to   { outline-color: transparent; }
}
@media (prefers-reduced-motion: reduce) {
  /* Held rather than faded — the class is removed on a timer either way, so
     this still clears itself. Fading is the part that is motion; being told
     which row changed is information, and reduced motion does not mean less
     information. */
  .flash { animation: none; outline-color: var(--brand); }
}

/* --------------------------------------------------------------------------
   Backlinks — the pages that write about a task
   -------------------------------------------------------------------------- */

.refs { display: flex; flex-direction: column; gap: .3rem; }
.ref-card {
  display: flex;
  flex-direction: column;
  gap: .1rem;
  padding: .4rem .55rem;
  border: 1px solid var(--line);
  border-left: 3px solid var(--brand);
  border-radius: var(--r-sm);
  background: var(--raise);
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition-property: background-color, border-color;
  transition-duration: var(--fast);
  transition-timing-function: var(--ease);
}
.ref-card:hover { background: var(--sunk); }
.ref-title { font-weight: 620; font-size: var(--t-md); }
.ref-snip {
  overflow: hidden;
  color: var(--ink-3);
  font-size: var(--t-sm);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Import report — what the import left out
   -------------------------------------------------------------------------- */

.loss-head {
  margin: .5rem 0 .2rem;
  font-size: var(--t-sm);
  font-weight: 700;
}
.loss-list {
  margin: 0;
  padding-left: 1.1rem;
  font-size: var(--t-sm);
  line-height: 1.55;
  text-wrap: pretty;
}

.person {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .5rem 0;
  border-bottom: 1px solid var(--line-soft);
}
.person .nm { font-weight: 600; font-size: var(--t-md); }
.person .em { font-size: var(--t-sm); color: var(--ink-3); }
.kind { padding: .1em .4em; border-radius: var(--r-xs); font-size: var(--t-2xs); font-weight: 700; }
.kind.guest { background: var(--warn-wash); color: var(--warn); }
.kind.member { background: var(--sunk-2); color: var(--ink-3); }

/* ==========================================================================
   Overlays — drawer, menu, sheet, palette
   ========================================================================== */

.scrim {
  position: fixed;
  inset: 0;
  z-index: 40;
  background: rgba(8, 14, 11, .42);
  animation: fade var(--med) var(--ease);
}

.drawer {
  position: fixed;
  inset: 0 0 0 auto;
  z-index: 41;
  display: flex;
  flex-direction: column;
  gap: .7rem;
  width: min(440px, 100%);
  padding: 1rem 1.1rem;
  background: var(--surface);
  border-left: 1px solid var(--line);
  box-shadow: var(--lift-3);
  overflow-y: auto;
  animation: slide-in var(--med) var(--ease);
}
.drawer .fld { width: 100%; }
.drawer .grp { display: grid; gap: .25rem; }
.drawer .two { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; }
.drawer .acts {
  display: flex;
  gap: .5rem;
  margin-top: auto;
  padding-top: .8rem;
  border-top: 1px solid var(--line);
}

.menu {
  position: fixed;
  z-index: 60;
  min-width: 190px;
  padding: 4px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--surface);
  box-shadow: var(--lift-3);
  /* Enter is a touch longer and travels; exit is handled by removal. */
  animation: pop var(--fast) var(--ease);
}
.menu-item {
  display: flex;
  align-items: center;
  gap: .5rem;
  width: 100%;
  min-height: 28px;
  /* Concentric with the 9px menu and its 4px padding. */
  padding: 0 .45rem;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--ink-2);
  font-size: var(--t-md);
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
  transition-property: background-color, color;
  transition-duration: var(--fast);
}
.menu-item:hover { background: var(--sunk); color: var(--ink); }
.menu-item.bad { color: var(--bad); }
.menu-item.bad:hover { background: var(--bad-wash); }
.menu-item .glyph { width: 1rem; text-align: center; opacity: .8; }
.menu-item .hint { margin-left: auto; color: var(--ink-4); font-size: var(--t-2xs); }
.menu-sep { height: 1px; margin: 4px 2px; background: var(--line-soft); }
.menu-head {
  padding: .3rem .45rem .2rem;
  font-size: var(--t-2xs);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-4);
}

.sheet {
  position: fixed;
  z-index: 41;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(460px, calc(100vw - 2rem));
  max-height: calc(100vh - 4rem);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: .7rem;
  padding: 1.1rem;
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  background: var(--surface);
  box-shadow: var(--lift-3);
  animation: rise var(--med) var(--ease);
}
.sheet h3 { font-size: var(--t-lg); }
.sheet .acts { display: flex; gap: .5rem; justify-content: flex-end; }

.palette {
  position: fixed;
  z-index: 41;
  top: 14vh;
  left: 50%;
  transform: translateX(-50%);
  width: min(560px, calc(100vw - 2rem));
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  background: var(--surface);
  box-shadow: var(--lift-3);
  overflow: hidden;
  animation: rise var(--med) var(--ease);
}
.palette input {
  min-height: 46px;
  padding: 0 1rem;
  border: 0;
  border-bottom: 1px solid var(--line);
  background: transparent;
  font-size: var(--t-lg);
  color: var(--ink);
}
.palette input:focus { outline: none; }
.palette-list { max-height: 46vh; overflow-y: auto; padding: 5px; }
.palette-item {
  display: flex;
  align-items: center;
  gap: .6rem;
  width: 100%;
  min-height: 38px;
  /* Concentric with the 18px shell minus its 5px padding. */
  padding: 0 .6rem;
  border: 0;
  border-radius: 13px;
  background: transparent;
  color: var(--ink);
  font-size: var(--t-md);
  text-align: left;
  cursor: pointer;
}
.palette-item:hover, .palette-item.cursor { background: var(--sunk); }
.palette-item .where { margin-left: auto; color: var(--ink-4); font-size: var(--t-xs); white-space: nowrap; }
.palette-item .what {
  flex: none;
  width: 3.1rem;
  font-size: var(--t-2xs);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-4);
}
.palette-item .lbl-txt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.toast {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  z-index: 70;
  max-width: 24rem;
  padding: .55rem .8rem;
  border: 1px solid var(--line);
  border-left: 3px solid var(--good);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink);
  font-size: var(--t-md);
  font-weight: 550;
  box-shadow: var(--lift-3);
  animation: rise var(--med) var(--ease);
}
.toast.err { border-left-color: var(--bad); }

/* ==========================================================================
   Empty states
   ========================================================================== */

/* The manual prompt, folded away under Sarah. Quiet by design: it is the
   fallback, and a fallback competing for attention with the thing that
   replaced it is just clutter with a justification. */
.fallback {
  margin-top: 1rem;
  border-top: 1px solid var(--line);
  padding-top: .8rem;
}
.fallback > summary {
  cursor: pointer;
  color: var(--ink-3);
  font-size: var(--t-sm);
  /* A 24px target, per 2.5.8 — a summary is a control and gets tapped. */
  padding: .35rem 0;
  min-height: 24px;
}
.fallback > summary:hover { color: var(--ink); }

/* --- settings ------------------------------------------------------------
   Workspace-wide configuration. Deliberately plain: this screen is visited
   rarely, by one person, to do one careful thing. Anything decorative competes
   with the only content that matters.                                       */

.settings {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  /* Reading width. A credential field stretched across a desktop monitor puts
     the label and the control an eye-movement apart. */
  max-width: 44rem;
}

.card-lg {
  display: flex;
  flex-direction: column;
  gap: .6rem;
  padding: 1rem 1.1rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
}
.card-lg h3 { margin: 0; font-size: var(--t-lg); }
.card-lg p  { margin: 0; text-wrap: pretty; }

/* Status line: a pill, the masked hint, and who set it when. Wraps rather than
   truncating, because the thing it exists to say is what would be cut off. */
.kv {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
}

/* An advisory inside a card, as against the page-wide #banner. Same colour
   language, so the two read as one kind of statement at two scopes. */
.banner-inline {
  display: flex;
  gap: .5rem;
  padding: .6rem .7rem;
  border-radius: 8px;
  font-size: var(--t-sm);
  text-wrap: pretty;
}
.banner-inline.warn {
  background: var(--warn-wash);
  color: var(--warn);
  border: 1px solid var(--warn);
}

.empty {
  padding: 2.4rem 1rem;
  text-align: center;
  color: var(--ink-3);
  font-size: var(--t-md);
  text-wrap: pretty;
}

.onboard {
  max-width: 34rem;
  margin: 1.8rem auto;
  padding: 1.5rem 1.4rem;
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  background: var(--surface);
  box-shadow: var(--lift-1);
  text-align: center;
}
.onboard h3 { margin-bottom: .35rem; font-size: var(--t-lg); }
.onboard p { margin-bottom: 1rem; color: var(--ink-3); font-size: var(--t-md); }
.onboard .acts { display: flex; gap: .5rem; justify-content: center; flex-wrap: wrap; }

.steps { margin: .6rem 0 0; padding-left: 1.15rem; color: var(--ink-2); font-size: var(--t-md); }
.steps li { margin-bottom: .3rem; text-wrap: pretty; }

.scroll-x { overflow-x: auto; }

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

@keyframes fade { from { opacity: 0; } }
@keyframes slide-in {
  from { opacity: 0; transform: translateX(14px); }
}
@keyframes rise {
  from { opacity: 0; transform: translate(-50%, calc(-50% + 8px)); }
}
@keyframes pop {
  from { opacity: 0; transform: scale(.97); }
}

/* The palette and toast are not centre-translated, so they need their own
   entrance rather than inheriting the sheet's transform. */
.palette { animation-name: rise-x; }
@keyframes rise-x {
  from { opacity: 0; transform: translateX(-50%) translateY(8px); }
}
.toast { animation-name: rise-y; }
@keyframes rise-y {
  from { opacity: 0; transform: translateY(8px); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  .btn:active, .card:active, .swatch:hover { transform: none; }
}

/* ==========================================================================
   Material — liquid glass
   --------------------------------------------------------------------------
   Everything in this section is applied to surfaces that genuinely float
   ABOVE other content. That is the whole selection rule, and it is what keeps
   the effect from becoming a texture sprayed over the app:

     scrim, rail-scrim   the dimmed content itself
     menu, sheet, drawer overlays that sit on top of the pane
     toast               floats over everything
     rail (phones only)  where it becomes an overlay; on a desktop it is a
                         column in the grid with nothing behind it, so it
                         stays opaque and costs nothing

   The header and tab strip are deliberately NOT here. They are flex children
   above the pane, not over it — blurring them would sample the flat ground
   and buy a compositor layer in exchange for nothing.

   This section is last in the file on purpose. Several of these selectors
   restate a background or a shadow set earlier, and at equal specificity the
   later rule is the one that wins. See the note above the phone breakpoint;
   this file has been bitten by that ordering before.
   ========================================================================== */

/* The scrim stops being a flat wash and becomes the blurred content itself.
   This is the single change that makes an overlay read as sitting in front of
   something rather than on a grey sheet. */
.scrim,
.rail-scrim {
  background: var(--glass-scrim);
  -webkit-backdrop-filter: blur(6px) saturate(120%);
  backdrop-filter: blur(6px) saturate(120%);
}

/* Overlays that carry text, so all of them take --glass-deep. The inset
   hairline is the specular top edge; the outer shadow is unchanged from the
   token, because depth and material are separate jobs. */
.menu,
.sheet,
.drawer {
  background: var(--glass-deep);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  border-color: var(--glass-edge);
  box-shadow: inset 0 1px 0 var(--glass-line), var(--lift-3);
}

/* The toast sets background and the inset edge but NOT border-color: its left
   border is a semantic accent (--good, or --bad when it failed), and a
   blanket border-color here would quietly erase the one part of a toast that
   says whether the thing worked. */
.toast {
  background: var(--glass-deep);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  box-shadow: inset 0 1px 0 var(--glass-line), var(--lift-3);
}

/* --- when glass must stop being glass -------------------------------------
   Three separate conditions, one outcome. Reduced transparency is a stated
   accessibility preference on both major desktop platforms and on iOS, a
   contrast preference is a stronger version of the same request, and a
   browser without backdrop-filter would otherwise render a plain translucent
   panel with unblurred text showing through — strictly worse than opaque.

   These blocks reference tokens rather than defining colours, so the opaque
   fallback is still whatever the current theme surface is. Redefining a
   colour inside a media query is the one thing this stylesheet forbids. */
/* --------------------------------------------------------------------------
   Ask Sarah
   --------------------------------------------------------------------------
   The one control in the app that floats over everything, so it is the one
   that most earns the material: it belongs to no panel, it sits above
   whatever view is open, and what is behind it changes as you scroll. That is
   this stylesheet's whole test for glass — a surface that genuinely hovers,
   not a texture sprayed over the app.

   It is a pill with a word in it rather than a bare icon, because a lone
   sparkle in a corner is a puzzle. Sarah spent her whole life on a tab
   labelled "Sarah" that only drafted plans; the point of this control is that
   she can change things, and a control you have to discover cannot say so.
   -------------------------------------------------------------------------- */

.ask {
  position: fixed;
  z-index: 35;
  right: calc(1rem + env(safe-area-inset-right, 0px));
  bottom: calc(1rem + env(safe-area-inset-bottom, 0px));

  display: flex;
  align-items: center;
  gap: .45rem;
  min-height: 44px;
  padding: 0 1rem 0 .85rem;
  border: 1px solid var(--glass-line);
  border-radius: 99px;

  background: var(--glass-deep);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));

  color: var(--ink);
  font-family: inherit;
  font-size: var(--t-md);
  font-weight: 640;
  box-shadow: var(--lift-3);
  cursor: pointer;
}
.ask:hover { border-color: var(--line-strong); }
.ask:active { transform: translateY(1px); }
/* The brand goes on the glyph, not the pill: a fully tinted pill competes with
   the primary button of every dialog it floats over. */
.ask-glyph { color: var(--brand); font-size: var(--t-lg); line-height: 1; }

.ask-sheet { width: min(34rem, 100%); }
.ask-sub { color: var(--ink-2); font-size: var(--t-sm); }
.ask-box { width: 100%; min-height: 4.5rem; resize: vertical; }
.ask-hint { color: var(--ink-3); font-size: var(--t-xs); }

.ask-eg { display: flex; flex-wrap: wrap; gap: .35rem; }
.ask-chip {
  padding: .3rem .55rem;
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink-3);
  font-family: inherit;
  font-size: var(--t-2xs);
  text-align: left;
  cursor: pointer;
}
.ask-chip:hover { color: var(--ink); border-style: solid; }

.ask-out:empty { display: none; }
.ask-summary { font-weight: 640; }
.ask-note { color: var(--ink-3); font-size: var(--t-xs); }
.ask-cost {
  color: var(--ink-3);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  font-variant-numeric: tabular-nums;
}
.ask-list {
  display: flex;
  flex-direction: column;
  gap: .3rem;
  margin: .5rem 0 0;
  padding-left: 1.1rem;
  font-size: var(--t-md);
}
/* Recording is a state the control has to show. The only other feedback is
   your own voice, and in a quiet room there is none. */
.ask-mic.on { border-color: var(--bad); color: var(--bad); }

@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
  .menu,
  .sheet,
  .drawer,
  .toast,
  .ask,
  .rail {
    background: var(--surface);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    box-shadow: var(--lift-3);
  }
  .scrim,
  .rail-scrim {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .menu,
  .sheet,
  .drawer,
  .toast,
  .ask,
  .rail {
    background: var(--surface);
    box-shadow: var(--lift-3);
  }
}

/* ==========================================================================
   Mobile — the failures that only appear on a real handset
   ========================================================================== */

/* `height: 100%` resolves against a viewport that changes size as mobile
   Safari slides its toolbars in and out, so the shell ends up either short of
   the bottom or running underneath it. dvh tracks that change. Added as an
   extra declaration rather than an edit: a browser that does not understand
   dvh drops this line and keeps the 100% defined earlier. */
body { height: 100dvh; }

.sheet { max-height: calc(100dvh - 4rem); }

/* iOS Safari zooms the page in when a focused field carries text smaller than
   16px, and it does not zoom back out afterwards. The fields here are 13px,
   which is correct under a pointer and wrong under a thumb — so the size is
   raised only where the pointer is coarse. This is not a matter of taste: at
   13px, tapping the search box silently magnifies the entire app and strands
   the reader there. */
@media (pointer: coarse) {
  .fld,
  .fld.sm,
  select.fld,
  textarea.fld { font-size: 16px; }
}

/* --------------------------------------------------------------------------
   The top bar, on a phone
   --------------------------------------------------------------------------
   Measured, not guessed. At 390px the bar wanted 405, so the DOCUMENT scrolled
   sideways — the whole app slid under the thumb. The flexible spacer had been
   squeezed to 0px, and the search control had gone from its 156px basis down
   to 20px: too narrow to read, too narrow to hit, and under the 24px target
   minimum. Its label had nowhere to go, so it wrapped and spilled out of a bar
   with a fixed 48px height, printing "j… to…" over the board behind it.

   None of that is a styling accident. The bar was carrying desktop furniture —
   a wordmark, a full search field, a keyboard hint — and flexbox resolved the
   overflow by crushing whichever item was allowed to shrink. So the fix is to
   take the furniture out at this width rather than shrink it further:

     the wordmark                          the mark already says which app
     the search label and its Ctrl K hint  there is no Ctrl key on a phone
     the "Acting as" caption               the control is self-evident

   The breakpoint is the one .who already uses, so the bar collapses in one
   step instead of two. The widest screen still below it is a 768px tablet,
   which lands around 680px of content and fits.
   -------------------------------------------------------------------------- */
@media (max-width: 700px) {
  .chrome {
    gap: .45rem;
    padding-left: calc(.6rem + env(safe-area-inset-left, 0px));
    padding-right: calc(.6rem + env(safe-area-inset-right, 0px));
  }

  /* Nothing in the bar may wrap. Wrapping is what put header text on top of
     the board: the bar's height is fixed, so a second line has nowhere to go
     but out of it. */
  .chrome > * { white-space: nowrap; }

  .brand > span:not(.mark) { display: none; }

  /* A square icon button rather than a field. Hiding the label alone would NOT
     have fixed the sliver — the button would have kept shrinking, because a
     flex item shrinks below its width unless told otherwise. The explicit
     width with flex:none is the part that actually holds. */
  .searchbar {
    width: 36px;
    min-width: 36px;
    flex: none;
    justify-content: center;
    padding: 0;
  }
  .searchbar .grow,
  .searchbar .kbd { display: none; }

  .who .lbl { display: none; }
  /* min-width:0 so the select truncates itself instead of forcing the row
     wider than the screen, which is how it came to overhang the right edge. */
  .who { min-width: 0; }
  .who .fld {
    min-width: 0;
    max-width: 7rem;
  }
}

/* The tab strip is now the primary way around the app wherever there is no
   pointer — the rail is behind a button and the keyboard shortcuts are not
   reachable — so the tabs get the extra 6px. Keyed to the pointer rather than
   to width, because an iPad is roomy and still operated by a thumb. 34px
   already cleared the 24px floor; this is about comfort, not compliance. */
@media (pointer: coarse) {
  .tab { min-height: 40px; }
}

/* Safe areas. index.html asks for viewport-fit=cover so the page can paint
   into the notch and home-indicator regions — which also makes it the job of
   this stylesheet to keep content out of them. The insets go on the fixed
   edges rather than on a wrapper, so the glass still reaches the physical
   edge of the display and only the content is pushed clear. Insetting a
   wrapper instead would letterbox the app and defeat the cover mode.

   Every env() carries a 0px fallback. Without one the whole calc() is invalid
   on any browser that does not implement the variable, and the padding
   collapses to nothing. */
.head {
  padding-top: calc(.8rem + env(safe-area-inset-top, 0px));
  padding-left: calc(1.1rem + env(safe-area-inset-left, 0px));
  padding-right: calc(1.1rem + env(safe-area-inset-right, 0px));
}
.tabs {
  padding-left: calc(1.1rem + env(safe-area-inset-left, 0px));
  padding-right: calc(1.1rem + env(safe-area-inset-right, 0px));
}
.pane {
  padding-left: calc(1.1rem + env(safe-area-inset-left, 0px));
  padding-right: calc(1.1rem + env(safe-area-inset-right, 0px));
  padding-bottom: calc(2.5rem + env(safe-area-inset-bottom, 0px));
}
.drawer {
  padding-top: calc(1rem + env(safe-area-inset-top, 0px));
  padding-right: calc(1.1rem + env(safe-area-inset-right, 0px));
  padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
}
.toast {
  right: calc(1rem + env(safe-area-inset-right, 0px));
  bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
}

/* A scroll that reaches the end of an overlay stops there instead of handing
   its momentum to the page underneath. Without this, flicking to the bottom
   of an open task drawer on iOS scrolls the board behind it. */
.rail,
.drawer,
.sheet,
.menu,
.pane { overscroll-behavior: contain; }

/* Removes the synthetic wait before a tap on a control counts as a click.
   Scoped to controls rather than set on the body, because the blanket version
   also takes away pinch-to-zoom — an accessibility feature, not a nuisance. */
button,
summary,
[role="button"] { touch-action: manipulation; }

@media (max-width: 700px) {
  /* Keeps the tighter mobile header while still clearing the status bar. */
  .head { padding-top: calc(.6rem + env(safe-area-inset-top, 0px)); }

  /* Here the rail IS an overlay — fixed, above the stage, with the board
     showing through. This is the one place it earns the material. */
  .rail {
    padding-top: calc(.85rem + env(safe-area-inset-top, 0px));
    padding-bottom: calc(.85rem + env(safe-area-inset-bottom, 0px));
    padding-left: calc(.6rem + env(safe-area-inset-left, 0px));
    background: var(--glass-deep);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
    backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
    border-right-color: var(--glass-edge);
    box-shadow: inset 0 1px 0 var(--glass-line), var(--lift-3);
  }
}

@media (max-width: 560px) {
  /* Six columns across a 390px screen gives each one 164px and a scrollbar
     with no sense of position — Done and Cancelled are simply somewhere off
     to the right. Snapping moves one column at a time, and 78vw is chosen so
     the edge of the next column stays visible: that sliver is the only thing
     telling a thumb there is more board further along. */
  .kanban {
    scroll-snap-type: x mandatory;
    scroll-padding-left: 1.1rem;
  }
  .kanban > .col {
    flex: 0 0 78vw;
    max-width: 78vw;
    scroll-snap-align: start;
  }
}

/* ==========================================================================
   Touch targets — WCAG 2.2 SC 2.5.8
   --------------------------------------------------------------------------
   Every one of these was found by measurement rather than by reading the file,
   and every one is a control whose HEIGHT was the height of one line of text.
   That is the pattern: a button wrapped around a label inherits the label's
   box, looks perfectly fine, and is 18 pixels tall.

   24x24 CSS pixels is the AA minimum. These set a floor rather than a size, so
   nothing already comfortable changes.
   ========================================================================== */

/* The board card's title, which is the card's primary control. */
.card .title {
  min-height: 24px;
  display: flex;
  align-items: center;
}

/* The list row's title. Flex so the floor does not strand the text at the top
   of a box that is now taller than it is; min-width:0 above still lets .ellip
   truncate inside the flex row. */
.rowopen {
  min-height: 24px;
  display: flex;
  align-items: center;
}

/* --- the timeline bar, and why only its height moves ----------------------
   A Gantt bar is 11 pixels wide when the task is one day long, and widening it
   to 24 would be a lie: the bar's horizontal extent IS the duration, drawn to
   the same scale as the axis above it. That is the "essential" exception in
   2.5.8 — a presentation required by the information being conveyed — and it
   is the textbook case for it.

   Nothing about the HEIGHT is essential, so the height meets the minimum.
   Do not "fix" the width; widening it would misreport every short task. */
.bar { height: 24px; }

/* --- the colour swatches, and the spacing exception -----------------------
   These are 18px circles, and they stay 18px: the swatch IS the colour, and a
   24px circle in a row of eight is a different control. They already carry
   `::after { inset: -8px }`, an invisible 34x34 hit area, which is the right
   way to enlarge a target without enlarging the mark.

   What was actually wrong was the SPACING. 2.5.8 lets a target be under 24px
   when the distance between adjacent target CENTRES is at least 24, and at an
   18px swatch with a 4px gap that distance was 22 — so the hit areas of
   neighbouring swatches overlapped and the nearest-neighbour ambiguity was
   real, not theoretical. 6px makes it exactly 24.

   Measured, not guessed: elementFromPoint at the corners of a 24x24 box
   centred on a swatch was returning the swatch NEXT to it. */
.swatches { gap: 6px; }

/* The hit area has to TILE, not merely be large.

   Two mistakes in one rule, and the second only showed up once the first was
   fixed:

   At `inset: -8px` the area reached far enough to overlap its neighbour by
   10px, and the later sibling paints on top — so a tap near the right edge of
   one swatch selected the NEXT colour along. Nominally a 34px target;
   effectively an off-centre one, which is harder to notice than a small target
   because nothing looks wrong.

   The size itself is not 18 + 2*inset. box-sizing is border-box for
   everything (see the reset above), the swatch carries a 2px border, and an
   absolutely positioned child resolves `inset` against its containing block s
   PADDING box — which is 14px, not 18. So the arithmetic is 14 + 2*5 = 24.

   With the 6px gap above, centres sit 24px apart and the areas meet edge to
   edge without overlapping. Measured with elementFromPoint at the corners of a
   24x24 box, not calculated and hoped for. */
.swatch::after { inset: -5px; }

/* On a touch screen the swatch simply IS 24px.
   
   The invisible-extender approach above is right for a mouse and turned out to
   be too clever for a thumb: the area has to be large enough, centred on the
   mark, and not overlap the next one, and getting all three right depends on
   the border box, the padding box and the flex gap agreeing. Two attempts at
   the arithmetic produced a target that still resolved to the neighbouring
   colour under elementFromPoint.
   
   A 24px circle needs none of that reasoning to be correct. The 18px mark is
   kept wherever the pointer is fine, because there the extender genuinely
   works and the smaller dot is better to look at; where the pointer is coarse,
   legibility of the target beats daintiness of the mark. */
@media (pointer: coarse) {
  .swatch { width: 24px; height: 24px; }
  /* Redundant now, and actively harmful: at 24px wide with a 6px gap, an
     extender would reach into the next swatch again. */
  .swatch::after { inset: 0; }
}

/* ==========================================================================
   The capture box
   --------------------------------------------------------------------------
   It was two rows tall with resize disabled, which is the smallest control on
   the screen it is the entire point of. Capture has to be cheaper than
   remembering, and a box that looks like a search field invites a fragment
   rather than a thought.
   ========================================================================== */
.capture textarea {
  min-height: 5.5rem;

  /* Bounded, because notes.js grows it to fit the content and an unbounded
     box would push the list of existing notes off the screen entirely on a
     long paste. Past this it scrolls, which is the right trade. */
  max-height: 40vh;

  /* Draggable, in the one axis that helps. Horizontal resize inside a flex
     column does nothing useful and lets somebody make the box narrower than
     the card containing it. */
  resize: vertical;
}

/* ==========================================================================
   Sales — making the pipeline usable
   --------------------------------------------------------------------------
   Appended as one block at the end of the file. Nothing above this line is
   modified; every rule here is new, and every colour in it is a var(--token)
   reference. There is no glass in this section on purpose — see the material
   notes near the top. A translucent panel here would have to be added to the
   two escape-hatch blocks and the @supports block as well, and the board has
   nothing behind it worth blurring.
   ========================================================================== */

/* The four ways in, above the board.

   Four plain buttons rather than one "Add…" menu. On a phone these are the
   entire vocabulary of the screen, and putting four verbs behind a fifth tap
   is how a screen that can be written to goes on feeling read-only. */
.crm-acts {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  margin-bottom: .9rem;
}

.crm-deal-head {
  display: flex;
  align-items: flex-start;
  gap: .25rem;
}
/* The title takes what is left after the two controls, and breaks rather than
   pushing them off the card: a 15rem column has no room to absorb one long
   unbroken company name. */
.crm-deal-head .crm-deal-title {
  flex: 1;
  min-width: 0;
  overflow-wrap: anywhere;
}

/* The drag handle.

   40px square. WCAG 2.5.8 asks for 24, and 24 is the wrong answer for a
   target you are expected to press, hold AND move — a handle that small is
   one the finger slips off, and slipping off mid-drag reads as the app
   dropping the card. The negative margin claws the extra size back out of the
   card's own padding so the card does not grow to accommodate it. */
.crm-grip {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  margin: -.3rem -.25rem -.3rem 0;
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink-4);
  line-height: 1;
  cursor: grab;

  /* Unconditional, and this is the load-bearing declaration in the section.

     Left alone, the browser claims the gesture for scrolling the instant a
     finger moves and cancels the pointer stream — which happens BEFORE the
     drag threshold in crm.js can be crossed, so the drag could never start on
     the one class of device it exists for. Applying it only once a drag is
     live is therefore not an option: there would be no live drag to apply it
     to.

     Scoped to the 40px handle rather than to the card, which is the other
     half of the trade: a flick anywhere else on a card still scrolls the
     column it sits in, so the board has not become undraggable-through in
     exchange. */
  touch-action: none;
}
.crm-grip:hover { background: var(--sunk-2); color: var(--ink-2); }
.crm-grip:active { cursor: grabbing; }

.crm-more {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  margin: -.2rem -.25rem -.2rem 0;
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink-4);
  line-height: 1;
  cursor: pointer;
}
.crm-more:hover { background: var(--sunk-2); color: var(--ink-2); }

/* The card in flight.

   pointer-events is not cosmetic here. The drop column is found with
   elementFromPoint, and the card is directly under the pointer for the whole
   gesture — without this it is the only thing elementFromPoint ever returns,
   and every drop lands nowhere. */
.crm-deal.dragging {
  position: relative;
  z-index: 3;
  opacity: .82;
  box-shadow: var(--lift-3);
  pointer-events: none;
  touch-action: none;
}

/* Where it would land. A ring plus a wash rather than a solid fill, so the
   cards already in the column stay readable while something hovers over
   them — the question being answered is "is this the right column", which
   needs the column's contents to still be visible. */
.crm-col.drop {
  outline: 2px dashed var(--brand);
  outline-offset: -2px;
  background: var(--brand-wash);
}

/* An empty column has no cards and therefore no height, which makes the first
   deal in a stage impossible to drop. This is the target, not decoration. */
.crm-cards { min-height: 44px; }

/* --- forms ---------------------------------------------------------------
   sheet() gives its body a grid with a .6rem gap, so each field below is one
   row of that grid and needs no margins of its own. */

.crm-field { display: grid; gap: .2rem; }
.crm-field .fld { width: 100%; }
.crm-lbl {
  color: var(--ink-3);
  font-size: var(--t-2xs);
  text-transform: uppercase;
  letter-spacing: .06em;
}

.crm-pair { display: flex; gap: .4rem; align-items: start; }
.crm-pair > * { flex: 1; min-width: 0; }
/* A currency is three characters. Giving it half the row would crowd the
   amount, which is the field actually being typed into. */
.crm-pair > .crm-cur { flex: 0 0 6.5rem; }

/* The parsed amount, read back before anything is saved.

   Monospaced and tabular because it is being compared against what was just
   typed a few pixels above it, and a proportional font makes "12,500.50" and
   "1,250.05" look the same length. */
.crm-echo {
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: var(--t-sm);
  font-variant-numeric: tabular-nums;
}
.crm-echo.bad { color: var(--bad); }

.crm-note {
  margin: 0;
  color: var(--ink-3);
  font-size: var(--t-xs);
  text-wrap: pretty;
}

/* --- the import report ---------------------------------------------------
   Sunk rather than raised. It is the result of the action, not another
   control, and a panel that looks like a card invites a click it has no
   answer for. */

.crm-report {
  display: grid;
  gap: .35rem;
  padding: .55rem .7rem;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--sunk);
}
.crm-report-n {
  margin: 0;
  font-size: var(--t-base);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-wrap: pretty;
}
/* Every failed row, scrollable. The list is capped in height because an
   import of two thousand rows can fail two thousand times, and a dialog that
   grows past the viewport takes its own buttons off the bottom of the
   screen. */
.crm-report ul {
  margin: 0;
  padding-left: 1.1rem;
  max-height: 9rem;
  overflow-y: auto;
}
.crm-report li {
  color: var(--ink-2);
  font-size: var(--t-xs);
}

/* --- companies ----------------------------------------------------------- */

.crm-co {
  display: flex;
  align-items: baseline;
  gap: .5rem;
  padding: .35rem 0;
  border-bottom: 1px solid var(--line-soft);
}
.crm-co-name { flex: 1; min-width: 0; font-weight: 640; }
.crm-co-domain {
  color: var(--ink-3);
  font-family: var(--mono);
  font-size: var(--t-2xs);
}
/* The count is why this list is drawn at all — a company with zero contacts
   is usually a domain an importer picked out of an email address — so it must
   not wrap away onto a second line at 390px. */
.crm-co-n {
  color: var(--ink-3);
  font-size: var(--t-2xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* --- 390px ---------------------------------------------------------------
   Layout only. No colour appears inside this query, deliberately: a colour
   declared at one width is a colour exactly one viewport ever renders, and it
   will be the one nobody looks at in the other theme. */
@media (max-width: 560px) {
  .crm-acts .btn { flex: 1 1 9rem; }
  .crm-pair { flex-wrap: wrap; }
  .crm-pair > .crm-cur { flex: 1 1 100%; }
}
