/* Uproad Hub — base styles. Self-hosted fonts, ink-on-paper defaults,
   and the small utility vocabulary from the brand system. Mobile-first. */

/* Libre Caslon Display was retired with "The Lit Workbench" (2026-08). A
   cyclorama has no serif in it, and the one-thing-at-a-time moment is now
   carried by scale and light instead of by a second voice. The woff2 is
   deleted; --up-font-display now aliases the sans so nothing breaks. */
@font-face {
  font-family: "Work Sans";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/fonts/WorkSans-var.woff2") format("woff2");
}
@font-face {
  font-family: "Work Sans";
  font-style: italic;
  font-weight: 100 900;
  font-display: swap;
  src: url("/fonts/WorkSans-var-italic.woff2") format("woff2");
}

/* Base — "most layouts are ink on paper" */
* { box-sizing: border-box; }
body {
  background: var(--up-bg);
  color: var(--up-fg);
  font-family: var(--up-font-body);
  font-size: var(--up-text-body);
  font-weight: var(--up-weight-regular);
  line-height: 1.55;
  margin: 0;
}

/* Accessibility: text size (Settings). Whole-page zoom rather than a
   font-size token rewrite — every font-size in this file is a literal
   px value, so scaling the root font-size alone wouldn't cascade. */
body[data-font-scale="small"] { zoom: 0.9; }
body[data-font-scale="large"] { zoom: 1.15; }
body[data-font-scale="xlarge"] { zoom: 1.3; }

/* Page titles and card headings. These name a room you're about to scan, so
   they're set in the body sans — tight, semibold, quiet. The display serif is
   reserved for the moments (wordmark, deck card, home hero, portal); when it
   was on every card heading it was the loudest thing on a page full of work.
   Sizes came down with it: a 34px title above a 13px table was a poster. */
/* 2026-08-08, the clean pass. Headings went 300-and-huge because "a cyc is
   vast and a semibold grotesque sits on it like a sticker" — but there is no
   cyc any more, and a 42px hairline headline above a 13px table is a poster
   sitting on top of the work rather than a label for it. Medium weight at a
   smaller size reads as INFORMATION, which is what a heading is for. This is
   the one place the old world's argument survives in reverse: with the wash
   gone, the type no longer has anything vast to hold its own against. */
h1, h2 {
  font-family: var(--up-font-title);
  font-weight: var(--up-weight-medium);
  font-style: normal;
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin: 0 0 var(--up-space-md);
  text-wrap: balance;
}
h1 { font-size: clamp(24px, 2.4vw, 30px); }
h2 { font-size: 19px; font-weight: var(--up-weight-medium); }

/* Page header stack. Every room opened with three stacked lines — copper
   eyebrow, title, subtitle — each with its own margin, so ~100px of masthead
   sat above the actual work. They're one block now: the eyebrow tucks under
   the title's own leading and the subtitle follows immediately. */
.up-kicker { margin-bottom: 2px; }
.content > h1 { margin-bottom: 4px; }
.content > h1 + p.up-caption { margin-top: 0; margin-bottom: var(--up-space-md); }

/* Prose gets a measure. Everything else — tables, boards, calendars — still
   uses the full width of the shell. */
.content > p,
.content > .up-caption,
.card > p.up-caption { max-width: var(--up-measure); }
h3 {
  font-family: var(--up-font-body);
  font-weight: var(--up-weight-semibold);
  font-size: 18px;
  margin: 0 0 var(--up-space-sm);
}

/* Links are text. Copper used to colour every one of them, which is how a
   table of five project names ended up as five copper underlines — the
   loudest thing on the page was the fact that they were clickable. They now
   read in the foreground with a faint underline that firms up on hover, so
   the NAME leads and copper is free to mean one thing: current. */
a {
  color: inherit;
  text-decoration-color: color-mix(in srgb, currentColor 30%, transparent);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: text-decoration-color 0.15s ease;
}
a:hover { text-decoration-color: currentColor; }

/* Utility classes — the brand's small vocabulary.

   .up-caption is SECONDARY PROSE: hints, empty states, explanatory lines,
   metadata. It used to be 11px letterspaced capitals, which is a LABEL
   device — and with ~290 uses across the app that meant most of the
   product's sentences were shouted. Sentence case at 13px reads in one
   pass and drops the "editorial mood board" texture.

   .up-label is the old treatment, kept for the handful of things that
   genuinely are labels: eyebrows, column headers, group heads. If it isn't
   naming a region or a column, it wants .up-caption. */
.up-caption {
  font-size: 13px;
  font-weight: var(--up-weight-regular);
  letter-spacing: 0;
  text-transform: none;
  line-height: 1.55;
  color: var(--up-muted);
}
.up-label {
  font-size: var(--up-text-caption);
  font-weight: var(--up-weight-semibold);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--up-muted);
}
.up-kicker {
  font-size: var(--up-text-caption);
  font-weight: var(--up-weight-bold);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  /* The readable copper, not the brand fill — this is the app's smallest
     copper text and it measured 3.66:1 on cream. */
  color: var(--up-copper-text);
}
.up-dark-ground {
  background: var(--up-ground);
  color: var(--up-paper);
}
.up-dark-ground .up-kicker { color: var(--up-accent); }

/* Top bar */
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--up-space-md);
  padding: var(--up-space-md);
  flex-wrap: wrap;
  /* A hairline, not a 3px accent bar. Every system UI separates chrome from
     content with the thinnest line it can draw; a coloured rule across the
     full width is a decoration wearing a structural job. The org accent still
     appears, as a dot next to the company name. */
  border-bottom: 1px solid var(--cyc-rim);
}
/* App topbar: sticky frosted glass, so content frosts beneath it as you scroll
   and it picks up the ambient grade like the sidebar. The copper accent
   bottom-border rides in as an inline style. (The logged-out topbar isn't
   inside .main-col, so it stays solid.) */
.main-col > .topbar {
  position: sticky; top: 0; z-index: 30;
  background: var(--up-chrome-glass);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
.topbar-right {
  display: flex;
  align-items: center;
  gap: var(--up-space-md);
  flex-wrap: wrap;
}
.topbar-left {
  display: flex;
  align-items: center;
  gap: var(--up-space-md);
}
/* These carry icons, so the glyph sizes the box and the box came out 16x16 and
   28x18 — under the 24x24 floor (WCAG 2.5.8). Centring inside a 24px minimum
   grows the target without moving the icon. */
.topbar-link {
  color: var(--up-paper);
  font-size: 14px;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 24px; min-height: 24px;
}
.topbar-brand,
.sidebar-brand {
  display: flex;
  align-items: center;
  gap: var(--up-space-sm);
  color: var(--up-paper);
  text-decoration: none;
}
.topbar-mark { height: 28px; width: auto; }
/* A company's uploaded logo (migration 067), standing where its typeset name
   would be. Bounded rather than sized: any aspect ratio scales to fit the
   brand row, and the rail version fits the collapsed 32px square. */
.sidebar-logo { max-height: 34px; max-width: 168px; width: auto; object-fit: contain; }
.sidebar-rail-logo { max-width: 26px; max-height: 26px; object-fit: contain; }
/* Ops preview sits on the same near-black the chrome uses, so a white or
   transparent logo is judged against where it will actually live. */
.logo-preview {
  background: var(--up-ground, #100F0D);
  border: 1px solid var(--up-line);
  border-radius: var(--up-radius-sm);
  padding: 14px; margin: 0 0 var(--up-space-sm);
  display: flex; align-items: center; justify-content: center;
}
.logo-preview img { max-height: 56px; max-width: 220px; object-fit: contain; }
.topbar-org-label,
.topbar-mark-divided {
  padding-right: var(--up-space-sm);
  border-right: 1px solid var(--up-neutral-500);
}
.topbar-org-label {
  font-family: var(--up-font-title);
  font-weight: var(--up-weight-semibold);
  font-size: 15px;
  letter-spacing: -0.01em;
}
.topbar-wordmark {
  font-family: var(--up-font-display);
  font-size: 20px;
  letter-spacing: 0.01em;
}
.topbar-version-badge {
  font-family: var(--up-font-body, inherit);
  font-size: 9px;
  font-weight: var(--up-weight-semibold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--up-neutral-400);
  margin-left: var(--up-space-xs);
  vertical-align: super;
}
.topbar-wordmark-group { display: flex; flex-direction: column; line-height: 1.15; }
.topbar-wordmark-accent { color: var(--up-copper); }
.topbar-tagline {
  font-size: 9px;
  font-weight: var(--up-weight-semibold);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--up-neutral-300);
}

/* Sidebar toggle (hamburger). Desktop: hides/shows the rail, state
   persisted per-user. Mobile: opens/closes the drawer, not persisted —
   see nav.js, which picks the behavior based on viewport width. */
.sidebar-toggle {
  background: none;
  border: none;
  color: var(--up-paper);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  padding: var(--up-space-xs);
  border-radius: var(--up-radius-sm);
}
.sidebar-toggle:hover { background: var(--up-surface); color: var(--up-copper); }

/* Topbar create. The "+ Create" label is gone — the glyph is the whole
   affordance — so it sits as a round copper button on the topbar's icon
   rhythm, next to the hamburger. */
/* A copper FILL carrying a glyph is exactly the case decisions.md 2026-08-08
   wrote the rule for, and the rule had never been applied here: paper on
   copper measured 1.96:1 on night and 2.77:1 on day, i.e. the "+" that opens
   Create has been effectively invisible on every screen in the app since it
   was built. The fill takes the strong pair, whose foreground flips with the
   theme, instead of always-paper. */
.qc-trigger {
  width: 30px; height: 30px; padding: 0;
  border: none; border-radius: 50%;
  background: var(--up-strong-bg); color: var(--up-strong-fg);
  display: inline-flex; align-items: center; justify-content: center;
  font: inherit; font-size: 20px; line-height: 1; cursor: pointer;
  transition: transform 0.16s ease, filter 0.16s ease;
}
.qc-trigger:hover { filter: brightness(1.08); transform: rotate(90deg); }
.qc-trigger:active { transform: rotate(90deg) scale(0.94); }

/* Sidebar — a persistent left rail on desktop (position: sticky, so it
   rides along with page scroll instead of scrolling away), an off-canvas
   drawer on mobile (see the mobile pass below). Same dark ground as the
   old topbar/tabs it replaces. */
.sidebar {
  width: 240px;
  flex: none;
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  overflow-x: hidden;
  overflow-y: auto;
  /* Frosted glass: translucent near-black over the shell's grade, so the nav
     picks up the warm/cool light instead of reading as a flat black slab. */
  background: var(--up-chrome-glass);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border-right: 0;
  box-shadow: none;
  /* impeccable-disable-next-line layout-transition: this is a sidebar collapsing on an occasional click, not a per-frame animation */
  transition: width 0.22s ease-in-out, border-color 0.22s ease-in-out;
}
/* The head, the nav and every label move on the SAME clock as the panel, or
   the edge arrives somewhere the contents haven't got to yet. Labels fade
   faster than the panel closes (0.12s of a 0.22s move) so they are gone before
   the edge reaches them rather than being sliced in half on the way past;
   visibility waits out the full move so it only leaves the a11y tree at rest. */
@media (min-width: 701px) {
  /* impeccable-disable-next-line layout-transition: same once-a-click collapse as the panel above */
  .sidebar-head, .sidebar-nav { transition: width 0.22s ease-in-out; }
  .sidebar-user-name, .sidebar-head-actions,
  .nav-label, .nav-section-kicker, .menu-newws {
    transition: opacity 0.12s ease-in-out, visibility 0s linear 0s;
  }
  .app-shell.sidebar-collapsed .sidebar-user-name,
  .app-shell.sidebar-collapsed .sidebar-head-actions,
  .app-shell.sidebar-collapsed .nav-label,
  .app-shell.sidebar-collapsed .nav-section-kicker,
  .app-shell.sidebar-collapsed .menu-newws {
    transition: opacity 0.12s ease-in-out, visibility 0s linear 0.22s;
  }
}
@media (prefers-reduced-motion: reduce) {
  .sidebar, .sidebar-head, .sidebar-nav,
  .sidebar-user-name, .sidebar-head-actions,
  .nav-label, .nav-section-kicker, .menu-newws { transition: none; }
}
/* Collapsed is a desktop-only, persisted preference (users.sidebar_collapsed)
   — scoped to min-width so it never fights the mobile drawer's own
   open/close state (.sidebar-open below), which is transient and viewport-
   driven instead.

   THE PANEL NARROWS; NOTHING MOVES (2026-08-10, Mark: "make it look like it
   is just going down to the small bar, rather than changing positions").
   It used to re-lay-out on collapse, and it was doing four separate things
   at once, all of them measured:

     · the avatar was REPLACED by a different element — a 32px text monogram
       (.sidebar-brand-collapsed-icon) standing in for the real 26px avatar,
       centred 6px left and 2px up from where the avatar had been
     · every nav icon slid 10px right, because .nav-link re-justified to
       `center` while the expanded state left-aligned it
     · the whole nav column jumped ~7px UP, because hiding .nav-label
       collapsed each row's line box from 35.7px to 30px
     · .sidebar-head's padding went from 16px to `16px 0`

   All four are gone. The two states now share ONE ICON COLUMN whose centre
   sits 30px from the sidebar's left edge — which is also the centre of the
   60px rail, so the same column reads as "indented" when open and "centred"
   when closed without a single value changing. Collapsing is now a width
   animation and a fade: the labels dissolve as the closing edge passes over
   them, and every icon stays exactly where your eye left it.

   The 30px column is why .sidebar-head is padded 10px on the left rather than
   16px, and why .nav-link is padded 14px rather than 4px — see both. Change
   one without the other and the avatar stops sitting above the nav icons. */
@media (min-width: 701px) {
  .app-shell.sidebar-collapsed .sidebar,
  .app-shell.sidebar-collapsed .sidebar-head,
  .app-shell.sidebar-collapsed .sidebar-nav { width: var(--sidebar-rail-width, 60px); }
  /* Everything that is a LABEL fades; nothing is display:none, because a
     display change is instant and would pop rather than pass behind the edge.
     pointer-events off so a faded label can't still be clicked or tabbed to,
     and visibility rides the same transition (delayed to the end) so it
     leaves the accessibility tree once it has actually gone. */
  .app-shell.sidebar-collapsed .sidebar-user-name,
  .app-shell.sidebar-collapsed .sidebar-head-actions,
  .app-shell.sidebar-collapsed .nav-label,
  .app-shell.sidebar-collapsed .nav-section-kicker,
  .app-shell.sidebar-collapsed .menu-newws {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
  }
}
/* .sidebar-brand-collapsed-icon is GONE (2026-08-10). It was a second identity
   element — plain text initials in a 32px tile — that existed only to stand in
   for the avatar while the avatar was hidden. Two elements for one thing is
   what made the collapse read as a swap: a photo became a monogram, at a
   different size, in a different place. The avatar simply stays now. */
.sidebar-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--up-space-sm);
  /* 10px on the left, not 16: the 26px avatar carries 7px of its own button
     padding, so 10 + 7 + 13 lands its centre on the shared 30px icon column
     that the nav icons below it use. This is the one asymmetric padding in
     the chrome and it buys the avatar sitting square above the nav. */
  padding: var(--up-space-md) var(--up-space-md) var(--up-space-md) 10px;
  width: 240px;
  flex-shrink: 0;
}
/* The only way out of the mobile drawer, and it measured 23x21 — under the
   24x24 floor (WCAG 2.5.8) on the one surface that is touch-only. It is
   `display: none` until the mobile breakpoint turns it on, which is why it
   never got looked at on a desktop. */
.sidebar-close {
  display: none;
  background: none;
  border: none;
  color: var(--up-paper);
  font-size: 16px;
  cursor: pointer;
  padding: 2px 6px;
  min-width: 24px; min-height: 24px;
}
.sidebar-nav {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  gap: var(--up-space-md);
  padding: 0 var(--up-space-sm) var(--up-space-lg);
  width: 240px;
  flex-shrink: 0;
}
.nav-section { display: flex; flex-direction: column; }
.nav-help-bottom { margin-top: auto; }
.nav-section-kicker { padding: var(--up-space-xs) var(--up-space-sm); }
/* The More shelf (V3 consolidation): everything that isn't a daily door
   collapses behind one disclosure so the rail stays six rows. Chevron
   replaces the default marker; flips when open. */
.nav-more summary { cursor: pointer; user-select: none; display: flex; align-items: center; gap: 7px; }
.nav-more summary::-webkit-details-marker { display: none; }
.nav-more summary { list-style: none; }
.nav-more summary::after {
  content: ""; width: 6px; height: 6px; flex: none;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg); transition: transform 0.15s ease;
}
.nav-more details[open] > summary::after { transform: rotate(45deg); }
.nav-item-list { display: flex; flex-direction: column; }
.nav-item {
  display: flex;
  align-items: center;
  gap: 6px;
  border-radius: var(--up-radius-sm);
}
.nav-item:hover { background: var(--up-surface); }
/* Navigation orients; it should not compete. The active row gets one quiet
   neutral fill, never an accent rule that makes the sidebar look like a
   component demo. Copper stays reserved for work that commits. */
/* 9% -> 5.5% with the soft pass. At 9% of a near-black foreground the active
   row was a solid grey lozenge — the highest-contrast object in the sidebar,
   shouting a fact the semibold label already states. The fill only has to be
   findable at a glance, not read from across the room. */
.nav-item-active {
  background: color-mix(in srgb, var(--up-fg) 5.5%, transparent);
}
.nav-item-active .nav-link { color: var(--up-fg); font-weight: var(--up-weight-semibold); }
.nav-item.dragging { opacity: 0.4; }
.nav-grip {
  cursor: grab;
  color: var(--up-neutral-400);
  font-size: 12px;
  padding-left: var(--up-space-sm);
  flex: none;
}
.nav-link {
  flex: 1;
  min-width: 0;
  /* 14px on the left puts the 16px icon's centre on the shared 30px column
     (8px of .sidebar-nav padding + 14 + 8), which is also the centre of the
     60px rail — so the icon does not move when the panel closes. It was 4px,
     which put the icon at 20px and made the collapse re-justify to reach 30.
     min-height holds the row at 36px whether or not the label is drawn:
     without it, fading the label collapsed the line box from 21.7px to the
     icon's 16px and dragged the whole column up ~7px mid-collapse. */
  padding: 7px var(--up-space-sm) 7px 14px;
  min-height: 22px;
  /* The icon never gives up a pixel. A faded label still OCCUPIES layout —
     that is the price of fading rather than display:none'ing it — so in the
     44px rail the icon and a 35px label were both flex-shrinking to fit 22px
     of content box, and the icon arrived at literally 0 wide. All the shrink
     goes to the label, which the link then clips. */
  align-items: center;
  gap: 8px;
  color: var(--up-fg);
  text-decoration: none;
  font-size: 14px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.nav-link > svg { flex: none; }
.nav-label { flex: 0 1 auto; min-width: 0; overflow: hidden; }
.nav-item:hover .nav-link { color: var(--up-fg); }
.nav-item-active:hover .nav-link { color: var(--up-fg); }
.sidebar .menu-newws { padding: 0 var(--up-space-sm) 0 var(--up-space-lg); }
.sidebar-backdrop { display: none; }

/* Dropdown backdrop (nav.js): a full-screen tap target that sits behind
   any open .menu (user menu, quick create, gear transfer), same idiom as
   .sidebar-backdrop above. A document-level pointerdown listener alone
   turned out not to reliably close dropdowns on every mobile browser —
   this covers the full viewport itself, so an outside tap has a direct,
   unambiguous element to land on instead of depending on the tap event
   bubbling all the way up. Sits above ordinary content but below the
   open menu panel itself (z-index 20, see .menu-panel). */
.menu-backdrop { display: none; }
.menu-backdrop.menu-backdrop-visible {
  display: block;
  position: fixed;
  inset: 0;
  z-index: 15;
}

/* Content */
.content {
  /* Explicit width: 100% — .content is a flex item of .main-col (the
     sidebar shell's column), and a flex item's cross-axis auto margins
     would otherwise suppress the default stretch-to-fill behavior.
     Capped at 1200px (was 720px and centered, then briefly uncapped
     entirely — both wrong: 720px starved wide-monitor layouts, no cap
     read as bare edge-to-edge bars with nothing to fill them). margin: 0
     instead of 0 auto keeps it flush against the sidebar rather than
     floating centered with empty gutters on both sides. Individual
     text-heavy elements (the doc editor/body, the auth form, admin
     .stack forms) keep their own tighter max-width regardless, so prose
     doesn't stretch edge to edge just because the page shell is wider. */
  width: 100%;
  max-width: 1200px;
  margin: 0;
  padding: var(--up-space-xl) var(--up-space-md) var(--up-space-2xl);
  /* The content STAGE re-themes here (the chrome — sidebar + topbar — stays
     Navy, so its paper logo/nav never break). Dark theme = brand Ink, the
     edit-bay ground; light theme (below) = warm print-stock cream. Every
     panel/table/comment reads its surface + text through these tokens, so
     re-pointing them here re-themes the whole content subtree at once. */
  --up-fg: var(--up-paper);
  /* Kept in step with :root through the soft pass — surface up, line down. */
  --up-surface:   rgba(244, 242, 238, 0.07);
  --up-surface-2: rgba(244, 242, 238, 0.115);
  --up-line:      rgba(244, 242, 238, 0.07);
  --up-border:    var(--up-line);
  /* Warm, because the room is. The cobalt build's #B6BDDA is a COOL grey, and
     a cool grey on a warm-key ground is both a contrast failure and a grading
     error — it measured 3.87:1 on the warm wash before this was corrected.
     Also worth keeping from that episode: this token was once lifted again to
     compensate for the wash seam, which was treating a symptom. When a token
     seems to need "one more lift", check what moved underneath it first. */
  --up-muted:     #C2BAAE;
  --up-cool:      #8FA8C4;
  --up-danger:    #FF7F6D;
  --up-due:       #F3CB63;
  color: var(--up-fg);
  /* Surfaces are transparent to the wash: a lit region lets the cyc through
     rather than covering it, which is why nothing here is a solid fill. */
  --up-glass:      var(--up-surface);
  --up-glass-2:    var(--up-surface-2);
  --up-glass-edge: transparent;
  --up-glass-hi:   var(--cyc-rim);
  --up-glass-shadow: none;
}
/* THE HORIZON. Fixed to the viewport, because a cyclorama's horizon does not
   move when the actors do — this is the one line the whole metaphor rests on,
   and it is the reason the stage reads as a room rather than as a page. */
.content::after {
  content: ""; position: fixed; left: 0; right: 0; bottom: 0; height: 44vh;
  background: var(--cyc-horizon);
  pointer-events: none; z-index: 0;
}
.content > * { position: relative; z-index: 1; }
body[data-theme$="light"] .content {
  --up-fg: var(--up-ink);
  --up-surface:   rgba(255, 253, 249, 0.88);
  --up-surface-2: rgba(255, 253, 249, 1);
  --up-line:      rgba(23, 19, 16, 0.065);
  --up-border:    var(--up-line);
  /* 2026-08-07: 46% -> 58%. Measured while building the quiet themes: a
     .up-caption on a card in light mode composited to 3.24:1, under the
     4.5 small-text floor, and .up-caption is the most-used text class in
     the app. Nothing to do with those themes — a pre-existing hole the
     measurement happened to walk into. Warmed with the regrade; the value is
     the same distance off the ground, it is just no longer blue. */
  --up-muted:     #665D53;
  --up-cool:      #3A5670;
  --up-danger:    #A8301F;
  --up-due:       #6B4600;
  --up-glass:      var(--up-surface);
  --up-glass-2:    var(--up-surface-2);
  --up-glass-edge: transparent;
  --up-glass-hi:   var(--cyc-rim);
  --up-glass-shadow: none;
}
/* Cards are panels that follow the content stage's theme (set on .content):
   a lifted dark charcoal in dark mode, near-white on cream in light mode.
   They read background/text/border/nested-surface through the stage tokens,
   so nothing inside them (tables, stage tracks, comment bubbles, inputs)
   needs per-theme rules. Copper stays copper throughout. */
/* A region on a cyc is not a panel with an edge — it is a place where more
   light lands. No border anywhere: the whole ring is an inset rim, so the
   shape is described by luminance rather than by a drawn line. */
.card {
  border: 0;
  background: var(--up-glass);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  color: var(--up-fg);
  border-radius: var(--up-radius);
  /* A little more air with the soft pass. The panel's edge got quieter, so the
     margin inside it has to do more of the work of saying where it starts. */
  padding: 22px 26px;
  margin-top: var(--up-space-md);
  box-shadow: inset 0 0 0 1px var(--up-line);
}
/* Not every card is the same weight. One holding a single sentence was
   getting the same 24px of air and the same drop shadow as one holding nine
   rows of work, which is why a page of them read as a grid of identical
   slabs. A card with nothing but a heading and a line of prose sits closer
   to the page: less padding, no lift. */
.card:not(:has(table, ul, ol, form, .card, li)) {
  padding: 16px var(--up-space-md);
  box-shadow: inset 0 0 0 1px var(--up-line);
}
/* 2026-08: cleaner frost — more blur, less saturation, so cards read as calm
   glass rather than a color-boosted pane (Superhuman-clean). */
.card {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
.card .up-caption { margin-bottom: var(--up-space-md); }

/* `backdrop-filter` makes every card its own stacking context, so a date
   picker's popover — z-index 60 and all — is sealed inside its card and the
   NEXT card paints straight over it. Raising the card that holds the open
   picker lifts the whole context, popover included. Scoped to the open state
   so cards don't otherwise reorder among themselves. */
.card:has(.dp.dp-open) { position: relative; z-index: 20; }

/* A card-scoped error: the card itself gets a danger outline, the
   heading gets a small warning badge, and the message renders as a
   highlighted callout right where the problem is — instead of a generic
   banner at the top of the page. */
/* What a delete would take with it — shown before you type the name, not
   after. Reads as a receipt, so the zero counts are simply absent. */
.del-counts {
  list-style: none; padding: 0;
  margin: var(--up-space-md) 0; display: flex; flex-wrap: wrap; gap: 6px 8px;
}
.del-counts li {
  font-size: 12.5px; padding: 5px 11px; border-radius: 999px;
  border: 1px solid var(--up-line); color: var(--up-muted);
}
.del-counts li b { color: var(--up-fg); font-weight: 600; font-variant-numeric: tabular-nums; }

.card-danger {
  border-color: var(--up-danger);
  box-shadow: 0 0 0 1px var(--up-danger);
}
.danger-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  margin-left: var(--up-space-xs);
  border-radius: 50%;
  background: var(--up-danger);
  color: var(--up-ink);
  font-size: 12px;
  font-weight: var(--up-weight-bold);
  vertical-align: middle;
  cursor: default;
}
.section-alert {
  background: color-mix(in srgb, var(--up-danger) 14%, transparent);
  border: 1px solid var(--up-danger);
  border-radius: var(--up-radius-sm);
  color: var(--up-danger);
  font-size: 14px;
  font-weight: var(--up-weight-medium);
  padding: var(--up-space-sm) var(--up-space-md);
  margin: var(--up-space-md) 0;
}

/* Palette swatches (Phase 0 placeholder page) */
.swatches {
  display: flex;
  flex-wrap: wrap;
  gap: var(--up-space-lg);
}
.swatch {
  display: flex;
  align-items: center;
  gap: var(--up-space-sm);
  font-size: 14px;
}
.chip {
  display: inline-block;
  width: 28px;
  height: 28px;
  border-radius: 50%;
}
.chip-outline { border: 1px solid var(--up-border); }

.specimen-display {
  font-family: var(--up-font-display);
  font-size: var(--up-text-subhead);
  margin: 0 0 var(--up-space-sm);
}
.specimen-body { margin: 0; }

/* Forms */
.stack { display: flex; flex-direction: column; gap: var(--up-space-md); max-width: 380px; }
.stack label {
  display: flex;
  flex-direction: column;
  gap: var(--up-space-xs);
  font-size: 14px;
  font-weight: var(--up-weight-medium);
}
input:not([type="checkbox"]):not([type="radio"]), select, textarea {
  font: inherit;
  font-size: 14px;
  color: var(--up-fg);
  background: color-mix(in srgb, var(--up-surface) 55%, transparent);
  border: 1px solid var(--up-line);
  border-radius: var(--up-radius-sm);
  padding: 9px 12px;
  /* A text input carries an intrinsic width from its `size` (~20 characters),
     and as a flex/grid item its default min-width: auto means it will refuse
     to shrink below that — which pushed the whole page sideways on a 320px
     phone. Let fields shrink to their container and never exceed it. */
  min-width: 0;
  max-width: 100%;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}
input::placeholder, textarea::placeholder { color: var(--up-muted); opacity: 1; }
input:not([type="checkbox"]):not([type="radio"]):hover, select:hover, textarea:hover {
  border-color: color-mix(in srgb, var(--up-fg) 19%, var(--up-line));
}
input:not([type="checkbox"]):not([type="radio"]):focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--up-copper);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--up-copper) 22%, transparent);
  background: var(--up-surface);
}
textarea { resize: vertical; }
/* Custom select caret — native chevron removed so the control matches our
   fields instead of the OS. */
select {
  -webkit-appearance: none; appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23948E80' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 10px center;
  padding-right: 30px;
}

/* --- Custom date picker (date-picker.js) --- */
/* Type-first date control: the field IS the picker, the calendar is a button
   beside it. Nothing has to be opened before you can type a date. */
.dp { position: relative; display: inline-flex; flex-direction: column; gap: 4px; }
.dp-native { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; margin: 0; padding: 0; border: 0; }
.dp-field {
  display: inline-flex; align-items: center; gap: 2px;
  background: color-mix(in srgb, var(--up-surface) 55%, transparent);
  border: 1px solid var(--up-line); border-radius: var(--up-radius-sm);
  padding: 0 4px 0 0; min-width: 190px;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.dp-field:hover { border-color: color-mix(in srgb, var(--up-fg) 28%, var(--up-line)); }
.dp-field:focus-within, .dp.dp-open .dp-field {
  border-color: var(--up-copper); box-shadow: 0 0 0 3px color-mix(in srgb, var(--up-copper) 22%, transparent);
}
/* Beats the global input styling — this one lives inside the field's border. */
.dp-field input.dp-input,
.dp-field input.dp-input:hover,
.dp-field input.dp-input:focus {
  flex: 1; min-width: 0; font: inherit; font-size: 14px; color: var(--up-fg);
  font-variant-numeric: tabular-nums; letter-spacing: 0.01em;
  background: none; border: none; border-radius: 0; box-shadow: none; outline: none;
  padding: 9px 4px 9px 12px; margin: 0;
}
.dp-field input.dp-input::placeholder { color: var(--up-muted); letter-spacing: 0.08em; }
.dp-field input.dp-input.dp-bad { color: var(--up-danger); }
.dp-x, .dp-cal-btn {
  flex: none; background: none; border: none; cursor: pointer; color: var(--up-muted);
  display: inline-flex; align-items: center; justify-content: center;
  padding: 5px; border-radius: 6px; line-height: 1;
}
.dp-x { font-size: 16px; }
/* The display above beats the [hidden] attribute, so say it explicitly —
   otherwise the clear × sits there on an empty field with nothing to clear.
   (Same trap as .cm-form[hidden] on the create modal.) */
.dp-x[hidden] { display: none; }
.dp-x:hover { color: var(--up-danger); background: color-mix(in srgb, var(--up-danger) 12%, transparent); }
.dp-cal-btn:hover, .dp.dp-open .dp-cal-btn { color: var(--up-copper); background: color-mix(in srgb, var(--up-copper) 14%, transparent); }
.dp-x:focus-visible, .dp-cal-btn:focus-visible { outline: 2px solid var(--up-copper); outline-offset: 1px; }
/* The live read-back of what you're typing. Reserves its line so the field
   doesn't jump as it appears. */
.dp-echo { font-size: 11.5px; line-height: 1.35; min-height: 1.35em; color: transparent; padding-left: 2px; }
.dp-echo-ok { color: var(--up-copper); font-variant-numeric: tabular-nums; }
.dp-echo-wait { color: var(--up-muted); }

.dp-pop {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 60; width: 268px;
  background: var(--up-surface); color: var(--up-fg);
  border: 1px solid var(--up-line); border-radius: var(--up-radius);
  box-shadow: var(--up-glass-shadow, 0 14px 34px rgba(0, 0, 0, 0.45));
  -webkit-backdrop-filter: none; backdrop-filter: none;
  padding: 12px;
}
.dp-pop { top: calc(100% - 14px); }
.dp-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.dp-head button { background: none; border: none; color: var(--up-fg); cursor: pointer; font-size: 18px; line-height: 1; padding: 2px 10px; border-radius: 6px; }
.dp-head button:hover { background: color-mix(in srgb, var(--up-fg) 10%, transparent); color: var(--up-copper); }
.dp-title { font-weight: 600; font-size: 14px; }
.dp-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.dp-dow { font-size: 11px; color: var(--up-muted); text-align: center; padding: 2px 0 6px; font-weight: 700; }
.dp-day { aspect-ratio: 1; border: none; background: none; color: var(--up-fg); border-radius: 7px; cursor: pointer; font-size: 13px; padding: 0; transition: background 0.12s ease; }
.dp-day:hover { background: color-mix(in srgb, var(--up-copper) 22%, transparent); }
.dp-day.dp-other { color: var(--up-muted); opacity: 0.45; }
.dp-day.dp-today { box-shadow: inset 0 0 0 1px var(--up-copper); }
/* The Copper Rule: a copper fill carrying text takes the strong pair, whose
   foreground flips with the theme. --up-paper measured 2.29:1 night / 2.87:1
   day here — the selected day was the least legible cell in the picker. */
.dp-day.dp-sel { background: var(--up-strong-bg); color: var(--up-strong-fg); }
.dp-day.dp-sel:hover { background: var(--up-strong-bg); }
.dp-foot { display: flex; justify-content: space-between; margin-top: 10px; }
.dp-foot button { background: none; border: none; color: var(--up-copper); cursor: pointer; font-size: 12px; font-weight: 600; padding: 2px; }
.dp-foot button:hover { text-decoration: underline; }

/* --- Create modal (create-modal.js) --- */
.create-modal {
  /* Scoped theme tokens so the fields inside follow the modal's own light/dark
     surface (it lives in the top layer, outside .content). */
  --up-fg: var(--up-paper);
  --up-surface:   color-mix(in srgb, var(--up-paper) 8%, var(--up-ink));
  --up-surface-2: color-mix(in srgb, var(--up-paper) 12%, var(--up-ink));
  --up-line:      color-mix(in srgb, var(--up-paper) 15%, var(--up-ink));
  --up-muted:     color-mix(in srgb, var(--up-paper) 55%, var(--up-ink));
  --up-danger:    #E2725B;
  --up-due:       #D8A44B;
  border: 1px solid var(--up-line);
  border-radius: var(--up-radius-lg);
  padding: 0;
  width: 520px; max-width: calc(100vw - 32px);
  background: color-mix(in srgb, var(--up-paper) 4%, var(--up-ink));
  color: var(--up-fg);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
  /* A <dialog> is overflow:auto by default, which clipped the date picker's
     popover (it hangs below its field). The panel is short by construction —
     one form, a handful of fields — so let it spill instead. */
  overflow: visible;
}
body[data-theme$="light"] .create-modal {
  --up-fg: var(--up-ink);
  --up-surface:   #FBFAF7;
  --up-surface-2: color-mix(in srgb, var(--up-ink) 5%, var(--up-cream));
  --up-line:      color-mix(in srgb, var(--up-ink) 14%, var(--up-cream));
  /* 2026-08-07: 46% -> 58%. Measured while building the quiet themes: a
     .up-caption on a card in light mode composited to 3.24:1, under the
     4.5 small-text floor, and .up-caption is the most-used text class in
     the app. Nothing to do with those themes — a pre-existing hole the
     measurement happened to walk into. Dark measured 4.85 and is left
     alone. */
  --up-muted:     color-mix(in srgb, var(--up-ink) 66%, var(--up-cream));
  --up-danger:    #A63A28;
  --up-due:       #8A5B14; /* deepened for cream — the dark-theme amber measures 1.9:1 here */
  background: #F7F5F1;
}
.create-modal::backdrop { background: rgba(8, 7, 6, 0.55); -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); }
.cm-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--up-line); }
.cm-tabs { display: flex; gap: 4px; }
.cm-tab { background: none; border: none; font: inherit; font-weight: 600; font-size: 14px; color: var(--up-muted); padding: 6px 12px; border-radius: var(--up-radius-sm); cursor: pointer; }
.cm-tab:hover { color: var(--up-fg); }
.cm-tab.cm-tab-active { color: var(--up-fg); background: color-mix(in srgb, var(--up-fg) 9%, transparent); }
.cm-close { background: none; border: none; color: var(--up-muted); font-size: 17px; line-height: 1; cursor: pointer; padding: 5px 7px; border-radius: var(--up-radius-sm); }
.cm-close:hover { color: var(--up-fg); background: color-mix(in srgb, var(--up-fg) 9%, transparent); }
.cm-form { padding: 18px; display: flex; flex-direction: column; gap: 18px; }
.cm-form[hidden] { display: none; }
.cm-title {
  font-family: var(--up-font-display); font-size: 26px; line-height: 1.15;
  border: none; background: none; padding: 4px 2px; color: var(--up-fg); border-radius: 0;
}
.cm-title:hover, .cm-title:focus { border: none; background: none; box-shadow: none; outline: none; }
.cm-title::placeholder { color: color-mix(in srgb, var(--up-fg) 38%, transparent); }
.cm-fields { display: flex; flex-wrap: wrap; gap: 14px; }
.cm-field { display: flex; flex-direction: column; gap: 5px; flex: 1 1 140px; min-width: 0; }
/* [hidden] must beat the flex above (same gotcha as .mp-row / .cm-form). */
.cm-field[hidden] { display: none; }
/* Craft chips (Phase 28b) — the multi-craft studio's one-tap "what kind of
   work is this?" row. Radio-feel buttons, copper when active. */
.cm-crafts { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: var(--up-space-sm); }
.cm-craft {
  border: 1px solid var(--up-line); background: transparent; color: var(--up-muted);
  border-radius: 999px; padding: 4px 12px; font-size: 13px; cursor: pointer;
}
.cm-craft:hover { color: var(--up-text); border-color: var(--up-copper); }
.cm-craft-active { color: var(--up-copper); border-color: var(--up-copper); font-weight: var(--up-weight-semibold); }
.cm-field > span { font-size: 12px; font-weight: var(--up-weight-semibold); letter-spacing: 0; color: var(--up-muted); }
.cm-check { display: inline-flex; align-items: center; gap: 8px; font-size: 14px; color: var(--up-fg); }
.cm-foot { display: flex; justify-content: flex-end; gap: 10px; align-items: center; border-top: 1px solid var(--up-line); padding-top: 16px; }

/* One workflow choice everywhere: type a name, then choose a recipe. The
   native select beneath it is the no-JS fallback; this compact list replaces
   the old craft chips, dropdown and deck-specific button pile. */
.workflow-picker { width: 100%; }
.workflow-picker-js { display: flex; flex-direction: column; gap: 8px; }
.workflow-picker-js[hidden] { display: none; }
.workflow-picker-search {
  width: 100%; font: inherit; color: var(--up-fg); background: var(--up-surface);
  border: 1px solid var(--up-line); border-radius: var(--up-radius-sm); padding: 10px 12px;
}
.workflow-picker-search::placeholder { color: var(--up-muted); }
.workflow-picker-search:focus { border-color: var(--up-copper); outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--up-copper) 18%, transparent); }
.workflow-picker-list { display: flex; flex-direction: column; gap: 4px; max-height: 180px; overflow-y: auto; }
.workflow-picker-choice {
  width: 100%; display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  text-align: left; font: inherit; font-size: 14px; color: var(--up-fg); background: transparent;
  border: 1px solid transparent; border-radius: var(--up-radius-xs); padding: 8px 10px; cursor: pointer;
}
.workflow-picker-choice:hover, .workflow-picker-choice:focus-visible { background: color-mix(in srgb, var(--up-copper) 10%, transparent); border-color: color-mix(in srgb, var(--up-copper) 42%, transparent); outline: none; }
.workflow-picker-choice.is-selected { color: var(--up-copper); background: color-mix(in srgb, var(--up-copper) 9%, transparent); border-color: color-mix(in srgb, var(--up-copper) 45%, transparent); }
.workflow-picker-choice small { flex: none; color: var(--up-muted); font-size: 12px; }

/* --- Generic form modal (form-modal.js) — the reusable version of the create
   modal for the app's many inline add/action forms. --- */
.form-modal {
  /* Scoped theme tokens: the dialog lives in the top layer, outside .content,
     so the fields inside follow the modal's own dark/light surface. */
  --up-fg: var(--up-paper);
  --up-surface:   color-mix(in srgb, var(--up-paper) 8%, var(--up-ink));
  --up-surface-2: color-mix(in srgb, var(--up-paper) 12%, var(--up-ink));
  --up-line:      color-mix(in srgb, var(--up-paper) 15%, var(--up-ink));
  --up-muted:     color-mix(in srgb, var(--up-paper) 55%, var(--up-ink));
  --up-danger:    #E2725B;
  --up-due:       #D8A44B;
  border: 1px solid var(--up-line);
  border-radius: var(--up-radius-lg);
  padding: 0;
  width: 460px; max-width: calc(100vw - 32px);
  max-height: min(88vh, 720px);
  background: color-mix(in srgb, var(--up-paper) 4%, var(--up-ink));
  color: var(--up-fg);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
  /* Column so the header stays put and only the body scrolls. */
  flex-direction: column;
}
.form-modal[open] { display: flex; }
.form-modal-wide { width: 600px; }
body[data-theme$="light"] .form-modal {
  --up-fg: var(--up-ink);
  --up-surface:   #FBFAF7;
  --up-surface-2: color-mix(in srgb, var(--up-ink) 5%, var(--up-cream));
  --up-line:      color-mix(in srgb, var(--up-ink) 14%, var(--up-cream));
  /* 2026-08-07: 46% -> 58%. Measured while building the quiet themes: a
     .up-caption on a card in light mode composited to 3.24:1, under the
     4.5 small-text floor, and .up-caption is the most-used text class in
     the app. Nothing to do with those themes — a pre-existing hole the
     measurement happened to walk into. Dark measured 4.85 and is left
     alone. */
  --up-muted:     color-mix(in srgb, var(--up-ink) 66%, var(--up-cream));
  --up-danger:    #A63A28;
  --up-due:       #8A5B14; /* deepened for cream — the dark-theme amber measures 1.9:1 here */
  background: #F7F5F1;
}
.form-modal::backdrop { background: rgba(8, 7, 6, 0.55); -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); }
.fm-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 18px; border-bottom: 1px solid var(--up-line); flex: 0 0 auto; }
.fm-title { margin: 0; font-family: var(--up-font-title); font-size: 17px; letter-spacing: -0.01em; line-height: 1.2; font-weight: 600; color: var(--up-fg); }
.fm-close { background: none; border: none; color: var(--up-muted); font-size: 17px; line-height: 1; cursor: pointer; padding: 5px 7px; border-radius: var(--up-radius-sm); flex: 0 0 auto; }
.fm-close:hover { color: var(--up-fg); background: color-mix(in srgb, var(--up-fg) 9%, transparent); }
.fm-body { padding: 18px; overflow: auto; }
/* Forms inside sit flush — the modal supplies the padding. */
.fm-body > form { margin: 0; }
.fm-body .btn, .fm-body .btn-small { margin-top: 4px; }

/* Circular "+" trigger — matches the look the old <details> summary had, for
   the bare add buttons that formModal() renders with triggerClass="fm-plus". */
.fm-plus {
  width: 28px; height: 28px; padding: 0;
  border-radius: 50%;
  border: 1px solid var(--up-line);
  background: none;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 18px; line-height: 1;
  color: var(--up-muted); cursor: pointer;
  transition: transform 0.15s, color 0.15s, border-color 0.15s;
}
.fm-plus:hover { color: var(--up-copper); border-color: var(--up-copper); transform: rotate(90deg); }
/* Keep triggers from stretching in a flex section header. */
.section-head > .js-modal-open { flex: 0 0 auto; }

/* --- Member picker (assignees) --- */
.member-picker { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.mp-search { margin-bottom: 4px; }

/* The quick-add that used to live on the General tasks card, now sitting
   under My Tasks — separated from the list so it doesn't read as a row. */
/* Nav count pill — a signal on the row itself, so the Call Board can say
   "something here needs you" without a dashboard card saying it. */
.nav-badge {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 17px; height: 17px; padding: 0 5px; margin-left: 6px;
  /* The Copper Rule — same defect as .bell-count, which was fixed while this
     one wasn't. --up-paper measured 2.29:1 night / 2.87:1 day, on the pill
     whose entire job is to be countable at a glance. */
  border-radius: 9px; background: var(--up-strong-bg); color: var(--up-strong-fg);
  font-size: 11px; font-weight: var(--up-weight-bold); line-height: 1;
}

/* Project create: one row per date (kind + value), repeatable. */
.date-rows { display: flex; flex-direction: column; gap: var(--up-space-sm); width: 100%; }
.date-row { display: flex; align-items: center; gap: var(--up-space-sm); flex-wrap: wrap; }
.date-row select { flex: 0 0 auto; }
.js-add-date { align-self: flex-start; margin-top: 4px; }
/* "Everyone" — the whole-team shortcut on a multi picker. */
.mp-all {
  align-self: flex-start; background: none; border: none;
  color: var(--up-copper); font: inherit; font-size: 13px;
  padding: 2px 6px; margin-bottom: 2px; cursor: pointer; border-radius: var(--up-radius-sm);
}
.mp-all:hover { background: color-mix(in srgb, var(--up-copper) 12%, transparent); }

/* Kit row actions sit side by side rather than stacking. */
.kit-actions { display: flex; gap: var(--up-space-sm); flex-wrap: wrap; align-items: center; }

.home-task-add { margin-top: var(--up-space-md); padding-top: var(--up-space-md); border-top: 1px solid var(--up-line); }
.mp-list { display: flex; flex-direction: column; gap: 2px; }
/* `.stack label` stacks its parts in a column (it's built for field + input);
   a picker row is a horizontal strip, so it has to say so louder — and the
   hidden rows need display:none or [hidden] loses to display:flex. */
.member-picker .mp-row {
  display: flex; flex-direction: row; align-items: center; gap: var(--up-space-sm);
  padding: 5px 8px; border-radius: var(--up-radius-sm);
  cursor: pointer; font-size: 14px;
  transition: background 0.12s ease;
}
.member-picker .mp-row[hidden] { display: none; }
.mp-row:hover { background: color-mix(in srgb, var(--up-fg) 8%, transparent); }
.mp-row input { flex: none; margin: 0; accent-color: var(--up-copper); }
.mp-row .avatar { flex: none; }
.mp-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The chosen row reads as chosen even in a list of five — the native
   radio/checkbox alone is too quiet at this size. */
.mp-row:has(input:checked) { background: color-mix(in srgb, var(--up-copper) 16%, transparent); }
.mp-row:has(input:focus-visible) { outline: 2px solid var(--up-copper); outline-offset: 1px; }
.mp-toggle {
  align-self: flex-start; background: none; border: none;
  color: var(--up-copper); font: inherit; font-size: 13px;
  padding: 4px 8px; cursor: pointer; border-radius: var(--up-radius-sm);
}
.mp-toggle:hover { background: color-mix(in srgb, var(--up-copper) 12%, transparent); }
/* In the create modal the picker is a column of rows, not a one-line field —
   give it the full width instead of the compact third a select would take. */
.cm-field-wide { flex: 1 1 100%; }

/* --- Mobile bottom nav (Instagram/HoneyBook style) --- */
.bottom-nav { display: none; }
.bnav-item {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  flex: 1; color: var(--up-muted); text-decoration: none;
  font-size: 11.5px; font-weight: 600; letter-spacing: 0.02em;
  background: none; border: none; cursor: pointer; padding: 6px 4px;
}
.bnav-item.bnav-active { color: var(--up-copper); }
.bnav-ic svg { width: 23px; height: 23px; display: block; }
.bnav-plus {
  display: inline-flex; align-items: center; justify-content: center;
  width: 46px; height: 46px; margin-top: -14px; border-radius: 50%;
  /* The Copper Rule: a copper FILL carrying a glyph takes the strong pair,
     whose foreground flips with the theme. Paper on copper measured 2.29:1 —
     this is the mobile twin of .qc-trigger and had the identical defect. */
  background: var(--up-strong-bg); color: var(--up-strong-fg);
  font-size: 28px; line-height: 1; font-weight: 400;
  box-shadow: 0 6px 18px color-mix(in srgb, var(--up-copper) 45%, transparent);
}
@media (max-width: 700px) {
  .bottom-nav {
    display: flex; align-items: center; justify-content: space-around;
    position: fixed; z-index: 200;
    left: 12px; right: 12px;
    bottom: calc(env(safe-area-inset-bottom, 0px) + 8px);
    padding: 8px 6px;
    border-radius: 26px;
    /* Liquid glass — a floating frosted pill (Instagram / iOS-26 style). Content
       blurs behind it ON PURPOSE, so there's nothing to "fix": the see-through
       IS the look, and it matches the app's glass panels. */
    background: color-mix(in srgb, var(--up-ground) 32%, transparent);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border: 1px solid color-mix(in srgb, var(--up-paper) 13%, transparent);
    box-shadow: 0 -1px 0 color-mix(in srgb, var(--up-paper) 10%, transparent),
      inset 0 1px 0 color-mix(in srgb, var(--up-paper) 16%, transparent);
  }
  /* On the cream stage that same 32% composites to a pale warm grey, and the
     labels are --up-muted — which outside .content is the LIGHT grey meant to
     sit on dark chrome. Light grey on light grey: the pill was legible only as
     a smudge. It holds near-opaque in light theme like the sidebar and topbar
     do, which also puts it back in the same family as the rest of the chrome.
     Dark theme keeps the sheer 32% glass untouched — that look was the point. */
  body[data-theme$="light"] .bottom-nav {
    background: color-mix(in srgb, var(--up-ground) 90%, transparent);
    border-color: color-mix(in srgb, var(--up-paper) 16%, transparent);
    box-shadow: 0 -1px 0 color-mix(in srgb, var(--up-paper) 12%, transparent),
      inset 0 1px 0 color-mix(in srgb, var(--up-paper) 14%, transparent);
  }
}

/* Count badge on a bottom-nav icon (Tasks = overdue, More = unread). */
.bnav-ic { position: relative; display: inline-flex; }
.bnav-badge {
  position: absolute; top: -5px; right: -11px;
  min-width: 15px; height: 15px; padding: 0 4px; box-sizing: border-box;
  /* Same fill pair as .bell-count — --up-paper measured 1.96:1 here too. */
  border-radius: 8px; background: var(--up-strong-bg); color: var(--up-strong-fg);
  font-size: 9px; font-weight: 700; line-height: 15px; text-align: center;
}

/* The account/utility list that only shows inside the mobile "More" sheet —
   the topbar's contents, flattened for the drawer. Hidden on desktop, where
   the topbar carries them. */
.sidebar-mobile-only { display: none; }
@media (max-width: 700px) { .sidebar-mobile-only { display: block; } }
.mnav { display: flex; flex-direction: column; gap: 2px; }
.mnav-link {
  display: block; width: 100%; text-align: left;
  padding: 10px 12px; border-radius: var(--up-radius-sm);
  color: var(--up-paper); text-decoration: none; font: inherit; font-size: 15px;
  background: none; border: none; cursor: pointer;
}
.mnav-link:hover, .mnav-link:active { background: color-mix(in srgb, var(--up-paper) 8%, transparent); }
.mnav-link .bnav-badge { position: static; display: inline-block; margin-left: 6px; }
.mnav-search { padding: 4px 8px 8px; }
.mnav-search input { width: 100%; }
/* Company switching collapses behind one row — a multi-workspace user was
   reading a wall of near-identical "Switch to X" lines in a sheet meant to
   be scanned. Same chevron language as the sidebar's More shelf. */
.mnav-switch > summary { list-style: none; display: flex; align-items: center; gap: 8px; }
.mnav-switch > summary::-webkit-details-marker { display: none; }
.mnav-switch > summary::after {
  content: ""; width: 6px; height: 6px; margin-left: auto; flex: none;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg); transition: transform 0.15s ease;
}
.mnav-switch[open] > summary::after { transform: rotate(45deg); }
.mnav-switch > summary .up-caption { opacity: 0.7; }
.mnav-sub { padding-left: 26px; font-size: 14px; }

/* Theme lives in the account menu now, not the topbar. */
.menu-theme {
  display: block; width: 100%; text-align: left;
  padding: 6px 10px; border-radius: var(--up-radius-sm);
  font: inherit; font-size: 14px; color: inherit;
  background: none; border: none; cursor: pointer;
}
.menu-theme:hover { background: var(--up-surface); }

/* The one control that commits carries the white-day gradient. Nothing else
   in the app is allowed it — that is the whole point of reserving day. */
.btn {
  font: inherit;
  font-weight: var(--up-weight-medium);
  background: var(--up-strong-grad);
  color: var(--up-strong-fg);
  border: none;
  border-radius: var(--up-radius-sm);
  padding: 11px 22px;
  cursor: pointer;
  align-self: flex-start;
  /* A 28px blur thrown 12px down is a stage light, not a button. Pulled in to
     a contact shadow: the control still lifts off the paper, it just stops
     announcing that it does. */
  box-shadow: 0 4px 10px -6px var(--up-strong-lift);
}
.btn-ghost {
  font: inherit;
  font-size: 14px;
  background: none;
  border: 1px solid var(--up-neutral-400);
  border-radius: var(--up-radius-sm);
  color: inherit;
  padding: 5px 12px;
  cursor: pointer;
}
.btn-small { font-size: 12px; padding: 3px 10px; }
.switch-org { display: inline-flex; align-items: center; gap: 5px; }
.switch-org svg { flex: none; }
/* Company switcher — just the exchange icon in the topbar. */
.company-control {
  display: inline-flex; align-items: center;
  color: var(--up-paper); cursor: pointer;
  padding: 5px; border-radius: var(--up-radius-sm); list-style: none;
}
.company-control::-webkit-details-marker { display: none; }
.company-control:hover { background: color-mix(in srgb, var(--up-paper) 8%, transparent); color: var(--up-copper); }
.company-control svg { flex: none; }
.inline-form { display: inline; }

.form-error { color: var(--up-danger); font-weight: var(--up-weight-medium); }
.form-notice { color: var(--up-copper); font-weight: var(--up-weight-medium); }

.auth-wrap { max-width: 380px; margin: var(--up-space-2xl) auto 0; }

/* Tables */
.table { width: 100%; border-collapse: collapse; margin: var(--up-space-lg) 0; }
/* Inside a panel the table's own bottom margin stacks on the panel's padding,
   which left ~46px of empty page under the last row of every list in the app.
   Visible now that the panel is quiet enough to see its own shape. */
.card > .table:last-child, .card > .tablewrap:last-child > .table:last-child { margin-bottom: 0; }
/* Column headers are one of the few places caps still earn their keep —
   they name a column rather than say anything. Tracking eased off 0.14em,
   which was set for display sizes, not for 11px chrome. */
.table th {
  text-align: left;
  font-size: var(--up-text-caption);
  font-weight: var(--up-weight-semibold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--up-muted);
  padding: var(--up-space-sm);
  border-bottom: 1px solid var(--up-line);
}
/* 6px vertical, not 8: these people run ten to twenty jobs at once and want
   to see all of them without scrolling. Air is hospitality on a landing page
   and friction on a workbench. */
.table td {
  padding: 8px var(--up-space-sm);
  /* Hairline rather than a full pixel. Apple's separator is 0.33pt and reads as
     a crease in the surface; a solid 1px reads as a drawn line, and that is
     most of why a web table looks heavier than the same list on a phone. */
  border-bottom: 0.5px solid var(--up-border);
  font-size: 14px;
  vertical-align: middle;
}
/* In a table the row IS the target, so its link drops the underline and
   carries a little weight instead; the row lights up under the cursor. */
/* No rule under the last row — iOS ends a group at the card's edge, not with a
   line hanging in space under the final item. */
.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr { transition: background 0.12s ease; }
.table tbody tr:hover { background: color-mix(in srgb, var(--up-fg) 4%, transparent); }
.table td a { text-decoration: none; font-weight: var(--up-weight-medium); }
.table td a:hover { text-decoration: underline; text-decoration-color: color-mix(in srgb, currentColor 45%, transparent); }
.row-inactive td { color: var(--up-muted); }
.row-inactive .avatar { opacity: 0.6; }
.task-done { text-decoration: line-through; color: var(--up-muted); }

/* Owner/assignee avatars standing in for a name — hover for who it is
   (avatarHtml sets title=), click through to their profile (avatarLink). */
.avatar-link { display: inline-block; line-height: 0; border-radius: 50%; }
.avatar-link:focus-visible { outline: 2px solid var(--up-copper); outline-offset: 1px; }
.owner-line { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.owner-line .avatar { vertical-align: middle; }

/* Multiple assignees on one project: overlap into a facepile instead of a
   row — a ring in the card's own background separates the circles, and
   whichever one you're hovering pops in front so its title tooltip and
   click target aren't half-covered by its neighbor. */
.avatar-stack { display: flex; }
.avatar-stack .avatar-link {
  margin-left: -10px;
  position: relative;
  z-index: 1;
  border-radius: 50%;
  box-shadow: 0 0 0 2px var(--up-surface);
}
.avatar-stack .avatar-link:first-child { margin-left: 0; }
.avatar-stack .avatar-link:hover,
.avatar-stack .avatar-link:focus-visible { z-index: 2; }

/* Sidebar shell — see the "Sidebar" block further down for the aside
   itself. .app-shell is the flex row (sidebar + main column); the topbar
   above now only holds the collapse/drawer toggle and the account cluster. */
/* The whole app is LIT here — a warm copper key-light from the top-right and a
   cool navy shadow from the bottom-left (the teal-orange grade as ambient room
   light), behind BOTH the content stage and the glass chrome. Fixed attachment
   so the panels and navs feel like they float in the light. */
/* The shell IS the cyclorama: one wash carrying the cobalt floor and the rose
   above it, fixed to the viewport so the light does not scroll with the work.
   Both themes read the same token, because they are two hours of one backdrop
   rather than two designs. */
/* Three shells, one cyc. The wash used to live on .app-shell alone, which
   meant the two pages a CLIENT ever sees — the portal (bare-doc) and the
   review page (cyc-plain, the signed-out shell) — were the only surfaces in
   the app still standing on a flat fill. They are the studio's shop window
   and they were the last rooms with the lights off. */
.app-shell, .bare-doc, .cyc-plain {
  min-height: 100vh;
  background: var(--cyc-wash);
  background-attachment: fixed;
  background-size: 160% 160%, 140% 140%;
  /* The static position matches the keyframes' resting value, so a reduced-
     motion or quiet-pair page — where the animation is off — gets the same
     covered frame rather than the old 122%. */
  background-position: 50% 100%, 78% 4%;
  /* impeccable-disable-next-line: a 42s drift on a fixed background is the
     world's own motion, not an interface animation — it is the light moving
     across the cyc, and it is what the whole metaphor is for. */
  animation: cyc-drift 42s ease-in-out infinite;
}
.app-shell { display: flex; align-items: stretch; }
/* THE VERTICAL POSITION MAY NEVER EXCEED 100%, and this is not a taste call.
   The wash layer is `no-repeat` and 160% of the viewport tall, so its offset is
   `(H - Ih) * P` and its bottom edge lands at `H + (Ih - H)(1 - P)`. With
   Ih > H, any P above 100% pulls that edge UP INTO the viewport and there is
   simply no image below it — the page falls through to bare canvas. At the
   122% these keyframes used to hold, on a 1128px viewport, the wash ran out
   149px above the bottom of the screen and drew a hard line straight across
   every page in the app, moving as the drift moved. It survived a 96-screen
   sweep because a contrast sweep measures colours, not seams.
   Making the layer BIGGER makes it worse, not better: `(Ih - H)` grows, so the
   edge climbs further up the screen. Only P <= 100% is safe.
   The "light from below the horizon" effect does not come from here anyway —
   it comes from the radial gradient's own focal point, `at 50% 122%`, which is
   measured INSIDE the image and is untouched. */
@keyframes cyc-drift {
  0%   { background-position: 50% 100%, 78% 4%; }
  50%  { background-position: 46% 96%, 84% 8%; }
  100% { background-position: 50% 100%, 78% 4%; }
}
@media (prefers-reduced-motion: reduce) {
  .app-shell, .bare-doc, .cyc-plain { animation: none; }
}
/* Chrome glass tokens (global — the navs live outside .content). A translucent
   near-black that frosts the grade behind it, with a lit edge. */
/* Chrome does not frost over the stage any more — it IS part of the stage,
   lit a little less. There is no bezel, because a cyclorama has no seam
   between the wall and the floor, and the old "chrome darker than content"
   bezel rule died with the workbench. */
/* FLAT, as of the 2026-08-10 soft pass. This was a vertical fade — 4.5% paper
   at the top running out to 1.2% at the bottom — so the sidebar was brighter
   at the head and dissolved into the stage at the foot, and the eye kept
   looking for an edge that was only there for the first 200px. A fade is a
   soft-looking device that produces a hard question. One honest step instead:
   chrome sits a hair off the stage, all the way down, and stops. */
/* And it steps DOWN, not up. This was a paper lift, which put the sidebar at
   roughly the same value as a card — so on night the chrome read as one more
   panel, and the darkest thing on screen was the stage the work sits on. The
   ramp has to run the same direction in both themes or the rule isn't a rule:
   chrome recedes, stage is neutral, panels come forward. On night that is
   #101216 / #14171C / #212429; on day it is #F3F0EA / #F7F4EF / #FFFDF9. */
body { --up-chrome-glass: rgba(0, 0, 0, 0.20);
       --up-chrome-edge: transparent;
       --up-chrome-hi: transparent; }
/* On day, the step goes the OTHER WAY, and that is the point of the soft pass.
   The old value was white-over-cream, so the sidebar was lighter than the work
   — the brightest thing on screen was the navigation. Now that the ground is
   paper rather than beige, the three surfaces can finally run in the order
   they mean: chrome (#F3F0EA) below the stage (#F7F4EF) below the panels
   (#FFFDF9). The work is the lit thing; the chrome is the room it is in.
   That is the two-grounds idea the workbench world wrote down and never
   executed — it failed there because all three of its values were near-black
   and inside a ten-value band, which is a problem this end of the ramp
   doesn't have. */
body[data-theme$="light"] { --up-chrome-glass: rgba(23, 19, 16, 0.022);
                           --up-chrome-edge: transparent; }
/* The content stage sits transparent over the shell's ambient grade (below),
   so the same lit background shows through the content area AND behind the
   glass chrome. */
.main-col { flex: 1; min-width: 0; display: flex; flex-direction: column; background: transparent; }
/* An item's own emoji (migration 091). Sized in em so it tracks whatever it
   sits beside — a 27px h1 on the project page, a 12.5px calendar block, a
   13px table row — without a size rule per surface.

   line-height: 1 and the vertical nudge because emoji carry their own generous
   metrics: left alone they push a table row taller than its neighbours, and a
   list where every third line is 2px taller than the others reads as broken
   before you work out why. */
.item-icon {
  display: inline-block;
  margin-right: 0.4em;
  font-size: 1.05em;
  line-height: 1;
  vertical-align: -0.08em;
  font-style: normal;
  /* Never let an emoji font override the surrounding weight or spacing. */
  font-weight: 400;
  letter-spacing: 0;
}
/* On the deck's big Caslon card the emoji would otherwise dwarf the words. */
.now-card h2 .item-icon, .now-run-title .item-icon { font-size: 0.72em; vertical-align: 0.04em; }
/* In a calendar block the title wraps, so the icon must not be dragged onto a
   line of its own by a long first word. */
.ev-title .item-icon { margin-right: 0.3em; }

/* Copper, in the two jobs it can't do with one value.
//
   Brand copper (#B5651D) is a FILL colour. As small TEXT it measures 3.6-4.2:1
   depending on the ground — under the 4.5 floor — which this app rediscovered
   three separate times in one afternoon: the `.badge`, the calendar's weekday
   label, and the month view's date disc. The deck had already solved it
   privately with --n-copper-br; these are the same idea at body level so
   nothing has to solve it a fourth time.

     --up-copper-text  copper you can READ. Lifts on ink, deepens on cream.

   RETIRED 2026-08-09: --up-copper-fill, "copper you can put PAPER TEXT ON,
   always dark in both themes, because the text on it is the fixed brand
   paper." Its premise was false — --up-paper is not fixed, the day themes
   re-point it from the paper to the ink — so its one consumer measured
   7.38:1 at night and 2.24:1 by day. A copper FILL carrying text does not
   need a third copper: it takes --up-strong-bg/--up-strong-fg, the pair
   whose foreground already flips. That is The Copper Rule, and it is now the
   only answer to this question in the app. Do not reintroduce a fill token
   that hardcodes what its foreground will be. */
/* Re-pointed off copper with the Cyclorama redesign. Declared on BODY, not
   :root, which matters: --up-accent is itself re-declared on body for the day
   themes, and a var() reference resolves against the value at the element
   that declares it. The same alias written on :root would have frozen the
   night value — the bug that put near-white nav text on a daytime sidebar. */
/* The readable accent has to differ from the fill in BOTH directions, or it
   is not doing its job: aliasing it straight to --up-accent left night-mode
   .deal-value at 4.36:1 on a card plate. Lifted on night, deepened on day. */
body { --up-copper-text: #E9A45E; }
/* On the day cyc the accent is a FILL value: as 11px kicker text it measured
   4.19:1 against the lighter plates. The readable variant deepens, exactly the
   split the old palette made between --up-copper and --up-copper-text. */
body[data-theme$="light"] { --up-copper-text: #7A3F10; }

/* Film grain over the whole app — low, constant, pointer-transparent, so
   surfaces read as lit stock rather than flat fills. Overlay blend works on
   both the dark and light stages. */
/* The film grain was removed with the workbench. A cyclorama is seamless by
   definition, and grain is a texture laid over every glyph on the screen —
   the opposite of what this world is for. */

/* Bell */
.bell { position: relative; display: inline-flex; align-items: center; }
.bell-count {
  position: absolute;
  top: -7px; right: -9px;
  /* The accent is a FILL, so the text on it is the fill's own foreground —
     --up-paper measured 1.96:1 on the night rose and 2.77:1 on the day one. */
  background: var(--up-strong-bg);
  color: var(--up-strong-fg);
  font-size: 10px;
  font-weight: var(--up-weight-bold);
  border-radius: 8px;
  padding: 0 5px;
  line-height: 14px;
}

/* Avatar: a real image if the user set an avatar link, otherwise their
   initials on the brand accent — same fallback in the topbar and on the
   profile page, sized via inline style at each call site. */
.avatar {
  border-radius: 50%;
  object-fit: cover;
  flex: none;
}
/* The initials fallback used to be a solid copper disc. One is fine; a
   twenty-row table is twenty copper dots, and it was the single biggest
   remaining copper mass in the app. A tinted fill with a copper ring still
   reads as ours and lets the names lead. A real uploaded photo is unaffected. */
.avatar-initials {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--up-copper) 20%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--up-copper) 42%, transparent);
  color: var(--up-fg);
  font-weight: var(--up-weight-semibold);
  letter-spacing: 0.02em;
}

/* The account menu lives in the SIDEBAR HEAD now (.sidebar-user, further down)
   — it was briefly a 32px round gear in the topbar, and that fixed square is
   why the name rendered at zero width when the trigger became a full row: the
   element kept its .user-menu class and the old size rule went on winning.
   Nothing here should set the trigger's box; .sidebar-user owns that. */
.user-menu > summary::-webkit-details-marker { display: none; }
/* .menu-list defaults to left:0 (right-expanding) for Studio/Ops, which
   sit on the left of the topbar. This trigger sits at the far right
   instead, so its panel needs to expand leftward or it runs off-screen. */
/* Both of these triggers sit at the FAR RIGHT of the topbar, so their panels
   have to expand leftward or they hang off the edge of the page — which the
   company switcher did, clipping every row's last word and the "+ New
   company" link with it. */
.user-menu .menu-panel,
.switch-menu .menu-panel { left: auto; right: 0; }
.menu-logout-form { padding: var(--up-space-xs) 10px 0; }
.menu-logout-form .btn-ghost { width: 100%; }

.profile-header {
  display: flex;
  align-items: center;
  gap: var(--up-space-md);
  margin-bottom: var(--up-space-lg);
}
.profile-header p { margin: 0; }

/* Projects: one row per project with a compact dot tracker instead of a
   horizontal-scrolling kanban — the whole lifecycle stays in view. */
.stage-track-list { display: flex; flex-direction: column; gap: var(--up-space-sm); }
.stage-track-row {
  border: 1px solid var(--up-border);
  background: var(--up-surface);
  border-radius: var(--up-radius);
  padding: var(--up-space-sm) var(--up-space-md);
}
.stage-track-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--up-space-sm);
  flex-wrap: wrap;
  margin-bottom: var(--up-space-sm);
}
.stage-track-head a { font-weight: var(--up-weight-medium); text-decoration: none; color: var(--up-fg); }
.stage-track-head a:hover { color: var(--up-copper); }
.stage-track { display: flex; align-items: center; gap: var(--up-space-xs); }
.stage-track-dots { display: flex; align-items: center; flex: 1; min-width: 0; }
.stage-dot {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--up-surface);
  border: 2px solid var(--up-line);
  flex: 0 0 auto;
}
.stage-dot-past { background: var(--up-copper); border-color: var(--up-copper); }
.stage-dot-current {
  width: 15px;
  height: 15px;
  background: var(--up-copper);
  border-color: var(--up-copper);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--up-copper) 25%, transparent);
}
.stage-dot-line { flex: 1 1 auto; height: 2px; min-width: 8px; background: var(--up-line); }
.stage-dot-line-past { background: var(--up-copper); }
.stage-track-label {
  font-size: 12px;
  font-weight: var(--up-weight-medium);
  color: var(--up-muted);
  white-space: nowrap;
  min-width: 88px;
  text-align: right;
}
.stage-track-nav {
  background: none;
  border: none;
  color: var(--up-muted);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 0 var(--up-space-xs);
}
.stage-track-nav:hover { color: var(--up-copper); }
.stage-track-nav-spacer { display: inline-block; width: 22px; }

/* Project list (routes/workspaces.js) only — not the CRM deal pipeline,
   which shares .stage-track-row's base look but keeps its own inline
   prev/next stage buttons. The whole card is a link into the project
   now (moving stage happens on the project's own page, not from this
   list), so the border/background respond to hover the way an ordinary
   link-card would. */
.stage-track-row-link {
  display: block;
  color: inherit;
  text-decoration: none;
  transition: border-color 0.15s ease;
}
.stage-track-row-link:hover { border-color: var(--up-copper); }
.stage-track-row-link .stage-track-head span:first-child { font-weight: var(--up-weight-medium); }

/* Project stage strip */
.stage-strip {
  display: flex;
  align-items: center;
  gap: var(--up-space-xs);
  flex-wrap: wrap;
  margin: var(--up-space-md) 0;
}
.stage-pill {
  font-size: 12px;
  font-weight: var(--up-weight-medium);
  padding: 3px 10px;
  border: 1px solid var(--up-line);
  border-radius: 12px;
  color: var(--up-muted);
}
/* Stages already cleared read as done, not as current — copper on six pills
   at once is the same dilution the nav had. Only .stage-current wears it. */
.stage-past { border-color: color-mix(in srgb, var(--up-fg) 22%, transparent); color: var(--up-fg); }
.stage-current {
  background: var(--up-strong-bg);
  border-color: var(--up-strong-bg);
  color: var(--up-strong-fg);
}
/* --up-muted, not --up-line. This arrow sits BETWEEN two stage names and says
   which way the work moves, so it is information, not a divider — and it was
   drawn in the 9%-alpha line colour, which measured 1.2:1 and made it
   effectively invisible. --up-line is for edges; nothing readable belongs in
   it. Quieter than the stage names either side, which is all it needed. */
.stage-sep { color: var(--up-muted); font-size: 12px; }

/* Composed project status line (routes/projects.js statusLine) — phase ·
   deliverables · release · deadline · guest, dot-separated, above the stage
   strip. One editorial read of where the project stands. */
.project-status {
  display: flex; flex-wrap: wrap; align-items: center;
  margin: var(--up-space-sm) 0 2px;
  font-size: 13px; color: var(--up-muted); line-height: 1.5;
}
.project-status .ps-seg { display: inline-flex; align-items: center; gap: 5px; }
.project-status .ps-seg:not(:last-child)::after {
  content: "·"; margin: 0 10px; color: var(--up-line); font-weight: 700;
}
/* Current phase reads as the "you are here" mark. It's a stage NAME — data,
   not a label — so it stays in the case the studio typed it in. */
.ps-phase {
  font-size: 13px; font-weight: var(--up-weight-semibold); letter-spacing: 0;
  color: var(--up-copper);
}
/* Release ▶ echoes the Schedule's Releases lens accent. */
.ps-release { color: var(--up-copper); }
/* Deadline pressure on the status line, matching the due cells. */
.ps-soon { color: var(--up-due); font-weight: 600; }
.ps-late { color: var(--up-danger); font-weight: 600; }
/* A deliverable waiting on review is the one thing that wants attention. */
/* "Needs your attention" is not "you are here" — it gets the attention hue,
   not the brand one. */
.ps-alert { color: var(--up-due); font-weight: 600; }
a.ps-guest { color: var(--up-muted); text-decoration: none; }
a.ps-guest:hover { color: var(--up-copper); }

/* "Add to this project" — the quiet row standing in for empty sections
   (simplicity arc: a card you haven't used doesn't render). */
.add-bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  margin-top: var(--up-space-lg);
}
.add-bar > .up-caption { margin-right: 4px; }

/* Signals tile instead of stacking. Each one is a couple of rows — deadlines
   this week, what's with the client, who's gone quiet — and full-width cards
   turned a company's status into a long scroll of mostly-empty banners.
   `align-items: start` so a one-row card doesn't stretch to match a six-row
   neighbour, and `auto-fit` means the count follows the window rather than a
   breakpoint guess. */
/* minmax(0, 1fr), not the implicit `1fr`. A bare `1fr` is minmax(AUTO, 1fr),
   and an auto minimum refuses to shrink below the widest card's min-content —
   which on a phone was 561px of nowrap table inside a 359px column, so every
   card in the stack blew out and the whole page scrolled sideways. The cards
   need min-width: 0 for the same reason: a grid item's default min-width is
   auto, so the inner table's overflow-x: auto never got the chance to work. */
.signal-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--up-space-lg); margin-top: var(--up-space-lg); }
.signal-grid > .card { margin-top: 0; min-width: 0; }
@media (min-width: 1000px) {
  /* 420px is a floor with a reason: these cards hold three-column tables
     (name · state · when), and at ~330px the badge broke onto three lines and
     "updated 4 days ago" onto two. This lands on two comfortable columns at
     every laptop width instead of three cramped ones. */
  .signal-grid { grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); align-items: start; }
  /* The first-run card is the whole page when it shows — it doesn't tile. */
  .signal-grid > .first-run { grid-column: 1 / -1; }
}
/* Nothing in a tiled card should wrap mid-phrase: the state badge and the
   "updated N days ago" stamp are single ideas. */
.signal-grid .badge, .signal-grid .table td:last-child { white-space: nowrap; }

/* Two-column page (the project page today). One column below 1100px — the
   rail's cards simply follow the main ones, which is the order the markup is
   already in, so nothing needs a second layout to be correct.

   Above it, the work runs down the middle (tasks, deliverables, notes,
   activity) and the project's standing facts — who's on it, the release, the
   shoot days, archiving — sit in a rail beside it. Full-width cards holding
   two avatars are what made a wide window feel like wasted paper.

   `align-items: start` so a short rail doesn't stretch its cards to the height
   of a long task table, and the rail's first card loses the top margin every
   .card carries so both columns start on the same line. */
/* Narrow: the two wrappers dissolve (`display: contents`) so every card is a
   direct flex item again and can be ordered individually. Without that, moving
   the rail's cards into their own element would have dumped "Add a shoot day"
   and the assignees below the whole activity feed on a phone — the columns are
   a desktop idea and shouldn't cost the phone anything. Order restored to
   roughly what it was: the work, the project's facts, then the long feed and
   the destructive controls last. */
.page-cols { display: flex; flex-direction: column; }
.page-main, .page-side { display: contents; }
.card-activity { order: 1; }
.card-manage { order: 2; }
@media (min-width: 1100px) {
  .page-cols {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 330px;
    gap: var(--up-space-lg);
    align-items: start;
  }
  .page-main, .page-side { display: block; }
  .card-activity, .card-manage { order: 0; }
  .page-side > .card:first-child,
  .page-main > .card:first-child { margin-top: 0; }
  /* In a 330px rail the add chips wrapped one-per-line at random widths. Give
     them the whole rail each and they read as a short menu instead of debris. */
  .page-side .add-bar { margin-top: var(--up-space-lg); flex-direction: column; align-items: stretch; }
  .page-side .add-bar > .up-caption { margin: 0 0 2px; }
  .page-side .add-bar .js-modal-open,
  .page-side .add-bar > form > button { width: 100%; }
}

/* Inline stacked forms */
.stack-inline {
  display: flex;
  gap: var(--up-space-sm);
  flex-wrap: wrap;
  align-items: center;
  margin-top: var(--up-space-md);
}
.stack-row { display: flex; gap: var(--up-space-lg); align-items: center; flex-wrap: wrap; }
.admin-tools { margin: var(--up-space-md) 0; }
.admin-tools summary { cursor: pointer; }

.view-toggle { font-size: 14px; }
.view-toggle a { text-decoration: none; color: var(--up-muted); }
.view-toggle a.toggle-active { color: var(--up-fg); font-weight: var(--up-weight-semibold); }

/* Segmented control — a real control instead of underlined links separated by
   middots. Used for the Calendar's view and lens switches; the ON segment
   takes the same copper "you are here" fill the sidebar and stage strip use,
   so the app has one way of saying where you are. */
.seg-row { display: flex; flex-wrap: wrap; gap: var(--up-space-md); margin: var(--up-space-md) 0 var(--up-space-lg); }
.seg {
  display: inline-flex; gap: 2px; padding: 3px;
  border: 1px solid var(--up-glass-edge); border-radius: 999px;
  background: var(--up-glass); backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.seg-item {
  padding: 7px 15px; border-radius: 999px; text-decoration: none;
  font-size: 13px; font-weight: var(--up-weight-semibold);
  letter-spacing: 0; color: var(--up-muted);
  transition: color 0.15s, background 0.15s;
}
.seg-item:hover { color: var(--up-fg); }
/* A view toggle picks a lens, it doesn't mark current work — so no copper.
   iOS draws the selection as a raised THUMB sitting in the track rather than a
   tinted slot: a solid fill a step lighter than the track, a hairline, and one
   soft shadow. That reads as a physical control you slid, which is most of why
   Apple's segmented controls feel like objects and web ones feel like tabs. */
.seg-item.is-on {
  color: var(--up-fg);
  background: var(--up-surface);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.16), 0 0 0 0.5px color-mix(in srgb, var(--up-fg) 12%, transparent);
}

/* Todos */
.todos { list-style: none; padding: 0; margin: 0 0 var(--up-space-md); }
.todo {
  display: flex;
  align-items: center;
  gap: var(--up-space-sm);
  padding: var(--up-space-xs) 0;
  border-bottom: 1px solid var(--up-border);
}
.todo-title { flex: 1; }
.todo-done .todo-title { text-decoration: line-through; color: var(--up-muted); }
/* The tick is quiet at rest and copper under the pointer, and that is a
   softness fix with a semantic one inside it. Both the done marker and the
   "mark this done" button render the same ✓ glyph, so at full copper an
   UNCHECKED task wore a copper tick — eight rows of them down the left edge of
   a list, which is both the accent's least rare appearance in the app and a
   row that looks like it has already been completed. Neutral at rest says
   "press me"; copper on hover says "this is the one you're about to commit". */
.todo-check {
  width: 22px; height: 22px;
  border: 1px solid color-mix(in srgb, var(--up-fg) 22%, transparent);
  border-radius: var(--up-radius-sm);
  background: var(--up-surface);
  cursor: pointer;
  color: var(--up-muted);
  font-weight: var(--up-weight-medium);
  transition: color 0.15s ease, border-color 0.15s ease;
}
button.todo-check:hover, button.todo-check:focus-visible {
  color: var(--up-copper);
  border-color: color-mix(in srgb, var(--up-copper) 55%, transparent);
}

/* Home: My Tasks list — same checkbox-row idiom as .todo, plus a project/
   company caption line and a trailing due date (Home's tasks always
   belong to a project, so unlike .todo there's always a caption to show
   and never a delete action — that stays on the task's own page). */
.home-task-list { list-style: none; padding: 0; margin: 0; }
.home-task-row {
  display: flex;
  align-items: center;
  gap: var(--up-space-sm);
  padding: var(--up-space-xs) 0;
  border-bottom: 1px solid var(--up-border);
}
.home-task-main { flex: 1; min-width: 0; }
.home-task-done .home-task-main a { text-decoration: line-through; color: var(--up-muted); }
.home-task-overdue { color: var(--up-danger, #b5433a); font-weight: var(--up-weight-semibold); }

/* Home: My Projects tiles — a grid of small cards instead of a table, so
   Home reads as a dashboard glance rather than another list view (the
   project's own page still has the full detail/table treatment). Icon is
   a plain monogram square in the project's company accent color — cheap
   per-project visual distinction with no image asset needed. */
.project-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--up-space-sm);
}
.project-tile {
  display: flex;
  align-items: center;
  gap: var(--up-space-sm);
  padding: var(--up-space-sm);
  border: 1px solid var(--up-glass-edge);
  border-radius: var(--up-radius);
  background: var(--up-glass-2);
  box-shadow: inset 0 1px 0 var(--up-glass-hi);
  text-decoration: none;
  color: inherit;
  min-width: 0;
}
.project-tile-icon {
  flex: 0 0 auto;
  width: 32px; height: 32px;
  border-radius: var(--up-radius-sm);
  display: flex; align-items: center; justify-content: center;
  color: var(--up-paper);
  font-weight: var(--up-weight-bold);
}
.project-tile-body { min-width: 0; display: flex; flex-direction: column; }
.project-tile-name {
  font-weight: var(--up-weight-semibold);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Feeds */
.feed { list-style: none; padding: 0; margin: 0; }
.feed li { padding: var(--up-space-sm) 0; border-bottom: 1px solid var(--up-border); font-size: 14px; }
.feed .up-caption { display: block; }
.notif-unread { font-weight: var(--up-weight-medium); }
.notif-unread::before { content: '●'; color: var(--up-copper); margin-right: 6px; }
.notif-read { color: var(--up-muted); }
.sendback-note { font-style: italic; }

/* Comments: avatar, bubble, time — texting-app minimal, no name line (the
   avatar carries identity). Reactions and delete tuck into the same small
   caption-height row under the bubble so a comment with nothing on it
   stays this quiet. */
.comment-list {
  list-style: none; padding: 0; margin: 0 0 var(--up-space-md);
  display: flex; flex-direction: column; gap: var(--up-space-md);
}
.comment { display: flex; gap: var(--up-space-sm); align-items: flex-start; }
.comment-main { min-width: 0; max-width: 80%; }
.comment-bubble {
  background: var(--up-surface-2);
  border-radius: var(--up-radius);
  padding: 8px 12px;
  white-space: pre-wrap;
  overflow-wrap: break-word;
}
.comment-foot {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 4px;
}
.comment-time { font-size: 12px; color: var(--up-muted); }

.reaction-pill {
  background: var(--up-surface);
  border: 1px solid var(--up-line);
  border-radius: 999px;
  padding: 1px 7px;
  font-size: 12px;
  line-height: 1.6;
  color: var(--up-fg);
  cursor: pointer;
}
.reaction-pill.reaction-mine { border-color: var(--up-copper); background: color-mix(in srgb, var(--up-copper) 22%, var(--up-surface)); }

.reaction-picker { display: inline-block; }
.reaction-picker > summary {
  list-style: none;
  cursor: pointer;
  color: var(--up-muted);
  font-size: 12px;
  padding: 0 4px;
  border: 1px solid var(--up-line);
  border-radius: 999px;
}
.reaction-picker > summary::-webkit-details-marker { display: none; }
.reaction-picker > summary:hover { color: var(--up-copper); border-color: var(--up-copper); }
.reaction-picker[open] > summary { color: var(--up-copper); border-color: var(--up-copper); }
/* Author `display` on a details child outranks the UA's collapse-when-closed
   rule (see .menu-panel above) — state it explicitly or options leak through. */
.reaction-picker:not([open]) > .reaction-options { display: none; }
.reaction-picker .reaction-options { display: inline-flex; gap: 3px; margin-left: 4px; }
.reaction-options .reaction-pill { font-size: 15px; padding: 0 5px; }


/* Threaded replies: one level deep (lib/comments.js threadRootId — a
   reply's parent is always the top-level comment, never another reply),
   so this only ever needs to indent once, not recursively. .reply-toggle
   reuses .reaction-picker's exact details-as-button idiom, just with its
   own small reply form instead of emoji options. */
.reply-toggle > summary { padding: 0 6px; }
.reply-toggle .comment-form { margin-left: 4px; }
.comment-replies {
  list-style: none;
  margin: var(--up-space-sm) 0 0;
  padding: 0 0 0 var(--up-space-lg);
  display: flex;
  flex-direction: column;
  gap: var(--up-space-sm);
  border-left: 2px solid var(--up-line);
}
.comment-reply .comment-bubble { padding: 6px 10px; font-size: 14px; }

.comment-delete-form { margin-left: auto; }
.comment-delete {
  background: none;
  border: none;
  color: var(--up-muted);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  padding: 0 2px;
}
.comment-delete:hover { color: var(--up-danger); }
.mention { color: var(--up-copper); font-weight: var(--up-weight-medium); }
.comment-form input { flex: 1; }

/* Schedule */
/* ============================================================================
   The Calendar. Rebuilt 2026-08-07 against a reference Mark supplied — an
   hour-gridded week planner with pastel blocks, tick boxes and progress.

   What was taken from it: the period as a serif headline, small-caps weekday
   over a big day number, soft tinted blocks instead of bordered chips, a real
   tick box, duration and progress inside the block.

   What was NOT: the hour grid. Only calls carry a time in this app — tasks,
   deadlines, releases and shoot days are dates — so events are stacked in day
   columns rather than positioned against an axis that would be inventing
   information. See the note in routes/schedule.js.

   The reference marks today in violet. Copper does that job here, and only
   that job (see tokens.css: "Copper now only ever means CURRENT"), so bringing
   in a second "you are here" hue would break a rule the whole app follows.
   ============================================================================ */

/* The period, as the headline. "Calendar" as an <h1> was the page announcing
   its own name to someone who just clicked Calendar to get here. */
.cal-top {
  display: flex; align-items: center; gap: var(--up-space-md);
  flex-wrap: wrap; margin-bottom: var(--up-space-lg);
}
.cal-period { margin: 0; display: flex; align-items: baseline; gap: 10px; line-height: 1; }
.cal-month { font-family: var(--up-font-display); font-size: 34px; font-weight: 400; letter-spacing: -0.01em; }
.cal-year { font-size: 20px; font-weight: var(--up-weight-medium); color: var(--up-muted); font-variant-numeric: tabular-nums; }
.cal-step { display: flex; align-items: center; gap: 2px; }
.cal-arrow, .cal-today-btn {
  display: inline-flex; align-items: center; justify-content: center;
  height: 30px; border-radius: 999px; text-decoration: none;
  color: var(--up-muted); transition: color 0.15s, background 0.15s;
}
.cal-arrow { width: 30px; font-size: 19px; line-height: 1; }
.cal-today-btn { padding: 0 12px; font-size: 12.5px; font-weight: var(--up-weight-medium); }
.cal-arrow:hover, .cal-today-btn:hover { color: var(--up-fg); background: color-mix(in srgb, var(--up-fg) 8%, transparent); }
.cal-views { margin-left: auto; }
.cal-lens { margin: 0 0 var(--up-space-lg); }

/* --- the week ------------------------------------------------------------
   Seven equal columns. minmax(0, 1fr) rather than a bare 1fr because an auto
   minimum won't shrink below the widest event title — the same trap that had
   the signal cards pushing phone pages sideways. */
/* The grid's lines are drawn by a 1px GAP over a coloured background, so there
   is no border property here even though the result reads as ruled. They use
   --cal-rule rather than --up-line: at 0.09 the rule was invisible and the week
   read as one undivided slab, which is the opposite of what a calendar is for.
   This is the documented no-border exception — see the token. */
.wk {
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 1px; background: var(--cal-rule);
  box-shadow: inset 0 0 0 1px var(--cal-rule); border-radius: var(--up-radius);
  overflow: hidden;
}
/* --up-surface, NOT --up-bg. --up-bg is the CHROME's colour and doesn't
   re-theme; the content stage re-points --up-surface/--up-surface-2/--up-line
   per theme on .content (see the token block up top). Using the chrome colour
   here painted seven near-black columns onto a cream page in light mode and
   made every day number invisible — the same class of mistake the /now deck
   made when it hardcoded one dark palette.
   Min-height so a week reads as a week; content grows it past this. */
/* --cal-cell, declared per theme just below. The day view wants a bright PAPER
   cell, which is what makes the Google-ish reference read the way it does — but
   only on the day themes. On night the same token is a 10% white lift over the
   wash, which composited to a mid grey-blue big enough to knock today's copper
   label under 4.5:1: a dark theme should not ape a white calendar. */
/* OPAQUE ON NIGHT TOO, as of the 2026-08-10 soft pass, and this was a real
   compounding bug rather than a taste call. The grid is drawn as 1px GAPS over
   a `--cal-rule` background, so a translucent cell composites over the RULE
   layer and inherits its lift before adding its own — two lifts, and the
   calendar came out at `#3A3C40` while every other panel in the app sat at
   `#242629`. Seven columns of the brightest surface in the product, for a
   view that is mostly empty most of the time. The day themes never had this
   because their cell is already opaque; night now states its value the same
   way, at the panel step. */
.wk, .cal { --cal-cell: #242629; }
body[data-theme$="light"] .wk, body[data-theme$="light"] .cal { --cal-cell: var(--up-surface-2); }
.wk-col { background: var(--cal-cell); min-width: 0; padding: 12px 10px 18px; min-height: 220px; }
/* Weekends recede rather than disappear — a shoot on a Saturday is still a
   shoot, it just isn't where the eye should land first. Halved with the ruled
   grid: once the columns are actually divided, the shading only has to whisper. */
.wk-col.is-weekend { background: color-mix(in srgb, var(--up-fg) 1.5%, var(--cal-cell)); }
.wk-head { display: flex; flex-direction: column; align-items: center; gap: 3px; margin-bottom: 14px; }
.wk-dow {
  font-size: 10.5px; font-weight: var(--up-weight-semibold);
  letter-spacing: 0.14em; text-transform: uppercase;
  /* The smallest type in the app, so it can't just take --up-muted: that is
     tuned for 13px body captions and measured 4.34:1 here. Mixed off the
     foreground instead, which tracks every theme. */
  /* 2026-08: 66% -> 88%. On the day cyc the old mix measured 3.57:1, and
     78% still only reached 3.53 against the calendar's own lighter plate. */
  color: color-mix(in srgb, var(--up-fg) 88%, transparent);
}
.wk-num {
  font-family: var(--up-font-display); font-size: 24px; line-height: 1.35;
  min-width: 1.6em; text-align: center; border-radius: 999px;
  font-variant-numeric: lining-nums;
}
/* Today: a filled copper disc. The number goes to the paper colour rather than
   inheriting, so it reads on the fill in both themes. */
/* Today's weekday label is NOT copper. The filled disc under it already says
   today, unmistakably, and colouring the label too was redundancy that cost
   contrast: on the brightened cell it measured 4.14-4.31:1. When a state is
   already marked once, the second marker is the one to spend. */
.wk-col.is-today .wk-dow { color: var(--up-fg); }
/* Today's disc is a copper FILL with a numeral on it, so it takes the strong
   pair too. Paper on copper measured 2.29:1 — the date you are most likely to
   look for was the least legible thing on the calendar. */
.wk-col.is-today .wk-num { background: var(--up-strong-bg); color: var(--up-strong-fg); }
.wk-body { display: flex; flex-direction: column; gap: 6px; }

/* --- one event ------------------------------------------------------------
   A soft tint of the item's own company accent rather than a bordered chip.
   The tint is low enough (10%) that six of them in a column still read as one
   surface, and the left rule carries the accent at full strength so companies
   stay tellable apart at a glance. */
/* --up-radius-xs, and this is the element that made that token necessary. The
   small token is 999px because controls are pills, and a pill is only ever
   correct for ONE line: an event title in a 66px-wide day column wraps to
   five, the browser clamps the radius to half the box, and the chip renders as
   a literal ellipse with the company rule bent into an arc down its side. The
   panel radius (26px) is barely better at this width — still nearly half the
   box. A dense grid needs its own step. */
.ev {
  position: relative; display: flex; align-items: flex-start; gap: 6px;
  padding: 7px 8px 7px 10px; border-radius: var(--up-radius-xs);
  /* Mixed over the PANEL colour, not the page. The page is near-black in dark
     mode, so a tint of it is just black again — the first version of this was
     invisible for any company whose accent is one of the desaturated defaults.
     Over a lifted surface the same mix reads as a tinted card in both themes. */
  background: var(--up-surface-2);
  box-shadow: inset 2px 0 0 var(--ev-accent, var(--up-copper));
  font-size: 12.5px; min-width: 0;
  transition: background 0.15s;
}
.ev-main { flex: 1 1 auto; min-width: 0; display: block; text-decoration: none; color: var(--up-fg); }
/* `overflow-wrap: anywhere` lets the browser break inside a word even when the
   word would have fitted on the next line, which in a 66px day column rendered
   "Chen Wedding Film" as "Chen / Weddi / ng Film". `break-word` only breaks a
   word that cannot fit on a line of its own, which is the actual intent — the
   guard is against one absurdly long title blowing out the column, not against
   ordinary words. */
.ev-title {
  display: block; font-weight: var(--up-weight-semibold); line-height: 1.3;
  overflow-wrap: break-word; hyphens: auto;
}
.ev-mark { font-style: normal; }
/* NOT --up-muted. Muted is tuned against the plain stage; on a tinted card it
   measured 3.1–3.5:1 at 11.5px, which fails 4.5:1 for small text — and this
   line is where the duration and the call time live, so it's the part you
   squint at. Mixed straight off the foreground instead, which tracks the theme
   and clears 4.5 on every tint. */
.ev-meta {
  display: block; margin-top: 2px; font-size: 11.5px;
  color: color-mix(in srgb, var(--up-fg) 78%, transparent);
  font-variant-numeric: tabular-nums;
}
.ev.is-done .ev-title { text-decoration: line-through; color: var(--up-muted); }

/* A release is the loudest thing on any day it lands on. */
/* impeccable-disable-next-line side-tab: the rule is DATA, not an accent — it says which company owns the release (see the note below) */
.ev-release { box-shadow: inset 3px 0 0 var(--ev-accent, var(--up-copper)); }
/* TWO signals in two channels, both real: the FILL says what kind of thing
   this is, the left RULE says which company it belongs to.
//
   The fill had been the company accent alone, and that turned out to be a bad
   default — createOrg hands every new company the same desaturated #6A655A, so
   a fresh account got seven columns of grey and none of the point. Kind is also
   what the reference colours by, and it's the question you're actually asking
   when you scan a week: is that a shoot, a deadline, or a thing to do?

   These five are a category palette, not brand colours — deliberately kept out
   of tokens.css so nothing outside the calendar can start meaning things by
   them. Copper is absent on purpose: it means CURRENT, and today's disc is
   already wearing it two inches above. */
.wk, .cal {
  --ev-tint-task:    var(--up-fg);
  --ev-tint-project: var(--up-due);
  --ev-tint-shoot:   #4FA398;
  --ev-tint-release: #9A7BC8;
  /* Was #5B8FD6, a saturated cobalt-family blue — the one hue this app is
     deliberately out of, and the most colourful thing on a calendar screen.
     The grade's own slate does the same categorical job without reintroducing
     it. Teal and violet stay: a category palette needs hue separation, and
     warming all five would destroy the signal it exists to carry. */
  --ev-tint-call:    #7C93AC;
}
body[data-theme$="light"] .wk, body[data-theme$="light"] .cal {
  /* Deepened for cream, the same correction --up-due already carries: the
     dark-theme values are lifted to read on ink and go milky on paper. */
  --ev-tint-shoot:   #2E7D74;
  --ev-tint-release: #6B54A6;
  --ev-tint-call:    #3A5670;
}
/* THE CHIP BASE. --cal-chip rather than --up-surface-2 directly, because the
   day themes want an OPAQUE paper under the tint: mixing a hue into a 92%
   translucent white let the grey wash show through and the chips came out
   muddy mauve rather than pastel. On night the base stays translucent, because
   a pastel block on a dark ground would glow. Same formula, two grounds. */
.wk, .cal { --cal-chip: var(--up-surface-2); }
body[data-theme$="light"] .wk, body[data-theme$="light"] .cal { --cal-chip: #FFFDF9; }
.ev-task    { background: color-mix(in srgb, var(--ev-tint-task) 10%, var(--cal-chip)); }
.ev-project { background: color-mix(in srgb, var(--ev-tint-project) 38%, var(--cal-chip)); }
.ev-shoot   { background: color-mix(in srgb, var(--ev-tint-shoot) 38%, var(--cal-chip)); }
.ev-release { background: color-mix(in srgb, var(--ev-tint-release) 40%, var(--cal-chip)); }
.ev-call    { background: color-mix(in srgb, var(--ev-tint-call) 38%, var(--cal-chip)); }
.ev-task:hover    { background: color-mix(in srgb, var(--ev-tint-task) 18%, var(--cal-chip)); }
.ev-project:hover { background: color-mix(in srgb, var(--ev-tint-project) 52%, var(--cal-chip)); }
.ev-shoot:hover   { background: color-mix(in srgb, var(--ev-tint-shoot) 52%, var(--cal-chip)); }
.ev-release:hover { background: color-mix(in srgb, var(--ev-tint-release) 54%, var(--cal-chip)); }
.ev-call:hover    { background: color-mix(in srgb, var(--ev-tint-call) 52%, var(--cal-chip)); }
/* A deadline's progress bar wears its own hue rather than the company's. */
.ev-project .ev-prog-bar { background: var(--ev-tint-project); }

/* The tick box. A real form posting to the same /t/:id/done every other
   surface uses, so the estimate lock applies here too — this is not a second,
   softer way to finish something. */
.ev-tick-form { flex: 0 0 auto; line-height: 0; }
.ev-tick {
  width: 18px; height: 18px; padding: 0; margin-top: 1px;
  border-radius: 999px; cursor: pointer; background: transparent;
  border: 1.5px solid color-mix(in srgb, var(--up-fg) 35%, transparent);
  color: var(--up-surface); font-size: 11px; line-height: 1;
  transition: background 0.12s, border-color 0.12s;
}
.ev-tick:hover { border-color: var(--up-fg); background: color-mix(in srgb, var(--up-fg) 12%, transparent); }
.ev-tick.is-done { background: var(--up-fg); border-color: var(--up-fg); color: var(--up-surface); }

/* Progress: a project deadline says how close it actually is, not just when it
   lands. Same done/total the Workflows table shows. */
.ev-prog { display: flex; align-items: center; gap: 6px; margin-top: 6px; width: 100%; }
.ev-prog-track {
  flex: 1 1 auto; height: 4px; border-radius: 2px; overflow: hidden;
  background: color-mix(in srgb, var(--up-fg) 14%, transparent);
}
.ev-prog-bar { display: block; height: 100%; background: var(--ev-accent, var(--up-copper)); border-radius: 2px; }
/* Same correction as .ev-meta — this sits on the amber deadline tint, where
   --up-muted measured 2.1:1. */
.ev-prog-n { font-size: 11px; color: color-mix(in srgb, var(--up-fg) 78%, transparent); font-variant-numeric: tabular-nums; }
/* The card is a row until progress needs a line of its own underneath. */
.ev:has(.ev-prog) { flex-wrap: wrap; }

/* --- the month -----------------------------------------------------------
   Same language, less of it: the cells are dense so events go compact (title
   and tick only, no meta or progress). */
.cal { width: 100%; border-collapse: separate; border-spacing: 1px; table-layout: fixed;
  background: var(--up-line); border: 1px solid var(--up-line); border-radius: var(--up-radius); }
.cal th {
  font-size: 10.5px; font-weight: var(--up-weight-semibold);
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--up-muted);
  padding: 8px var(--up-space-xs); text-align: center; background: var(--up-surface);
}
.cal td { background: var(--up-surface); vertical-align: top; padding: 6px; height: 88px; }
@media (min-width: 1000px) { .cal td { height: 118px; } }
.cal-empty { background: color-mix(in srgb, var(--up-fg) 3%, var(--up-surface)); }
.cal td > .ev { margin-bottom: 4px; }
.cal-day {
  font-family: var(--up-font-display); font-size: 15px; line-height: 1.5;
  color: var(--up-muted); margin-bottom: 4px; text-align: center;
  border-radius: 999px; font-variant-numeric: lining-nums;
}
/* Today is the disc, not an outline around the whole cell — the outline read
   as an error state, and it fought every event inside it. */
.cal-today { background: var(--up-surface); }
.cal-today .cal-day {
  /* The strong pair, which is what .wk-num.is-today — the SAME idea in the
     week view — already uses. The two views now agree on what today looks
     like, which they didn't before.

     What was here was --up-copper-fill + --up-paper, and it failed for a
     reason worth keeping: that token was documented as "copper you can put
     PAPER TEXT ON... because the text on it is the fixed brand paper", and
     --up-paper is NOT fixed — the day themes re-point it to the ink. So the
     pairing measured 7.38:1 at night and 2.24:1 by day, and only ever got
     looked at in the dark. A token whose premise is "the other one holds
     still" breaks the moment the other one moves. */
  background: var(--up-strong-bg); color: var(--up-strong-fg);
  width: 1.7em; margin-left: auto; margin-right: auto;
}
.cal-nav { display: flex; align-items: center; gap: var(--up-space-md); }
.cal-nav h2 { margin: 0; }

/* Phones: seven columns of anything is unreadable, so the week becomes a
   stack of days and the month keeps its grid but loses the event text (the
   dot is "something is here", which is all a 44px cell can honestly say). */
@media (max-width: 760px) {
  .cal-top { gap: var(--up-space-sm); }
  .cal-month { font-size: 26px; }
  .cal-views { margin-left: 0; width: 100%; }
  .wk { grid-template-columns: minmax(0, 1fr); }
  /* min-height: 0 matters more than it looks. The desktop floor keeps a week
     reading as a week across seven columns; stacked on a phone it turned four
     quiet days into four screens of nothing between Monday and the thing you
     opened the app to find. An empty day now collapses to its heading. */
  .wk-col { padding: 8px 10px 12px; min-height: 0; }
  .wk-head { flex-direction: row; align-items: baseline; gap: 8px; justify-content: flex-start; margin-bottom: 0; }
  .wk-body:not(:empty) { margin-top: 8px; }
  .wk-num { font-size: 19px; min-width: 1.5em; text-align: center; }
  .cal td { height: 54px; padding: 3px; }
  /* A 44px cell can honestly say "something is here" and what KIND — no more.
     The dot takes the kind tint at full strength rather than the company
     accent, which is the same grey for every new company and would have made
     a month of dots say nothing at all. */
  .cal td > .ev {
    padding: 0; border-radius: 999px; box-shadow: none;
    height: 6px; width: 6px; margin: 0 auto 3px;
    background: var(--up-muted);
  }
  .cal td > .ev-task    { background: color-mix(in srgb, var(--ev-tint-task) 55%, transparent); }
  .cal td > .ev-project { background: var(--ev-tint-project); }
  .cal td > .ev-shoot   { background: var(--ev-tint-shoot); }
  .cal td > .ev-release { background: var(--ev-tint-release); }
  .cal td > .ev-call    { background: var(--ev-tint-call); }
  .cal td > .ev * { display: none; }
  .cal-day { font-size: 12px; }
}

/* Shoot-days card: one row per day, note muted, delete button flush right. */
.plain-list { list-style: none; padding: 0; margin: 0 0 var(--up-space-md); }
.plain-list > li { padding: var(--up-space-xs) 0; border-bottom: 1px solid var(--up-line); }
.plain-list > li:last-child { border-bottom: 0; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--up-space-sm); }
.call-card h2 a { text-decoration: none; color: var(--up-fg); }
.badge-muted { color: var(--up-muted); border-color: var(--up-line); }
.badge-danger { color: var(--up-danger); border-color: var(--up-danger); }
.role-picks { border: 1px solid var(--up-border); border-radius: var(--up-radius); padding: var(--up-space-md); }
.role-picks legend { padding: 0 var(--up-space-xs); }
.role-check {
  display: inline-flex !important;
  flex-direction: row !important;
  align-items: center;
  gap: var(--up-space-xs);
  margin-right: var(--up-space-md);
  font-weight: var(--up-weight-regular) !important;
}
.role-check input { accent-color: var(--up-copper); }

/* Search */
.searchform input {
  padding: 5px 10px;
  font-size: 13px;
  width: 140px;
  background: var(--up-neutral-600);
  border: 1px solid var(--up-neutral-500);
  border-radius: var(--up-radius-sm);
  color: var(--up-paper);
}
.searchform input::placeholder { color: var(--up-neutral-400); }
.search-snippet { font-size: 13px; color: var(--up-muted); margin-top: 2px; }
.search-snippet mark {
  background: color-mix(in srgb, var(--up-copper) 25%, transparent);
  color: inherit;
  padding: 0 1px;
  border-radius: 3px;
}

/* Docs */
.docs-layout > * { min-width: 0; }
.docs-layout {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr);
  gap: var(--up-space-xl);
  align-items: start;
}
.doc-sidebar {
  border-right: 1px solid var(--up-border);
  padding-right: var(--up-space-md);
}
.doc-tree { list-style: none; padding-left: 0; margin: var(--up-space-sm) 0; }
.doc-tree .doc-tree { padding-left: var(--up-space-md); }
.doc-tree a {
  display: block;
  padding: 3px 6px;
  text-decoration: none;
  color: var(--up-fg);
  font-size: 14px;
}
.doc-tree a:hover { color: var(--up-copper); }
.doc-tree a.doc-active { background: color-mix(in srgb, var(--up-fg) 9%, transparent); color: var(--up-fg); border-radius: var(--up-radius-sm); box-shadow: inset 2px 0 0 var(--up-copper); }
.doc-body { max-width: 640px; }
.doc-body pre {
  background: var(--up-surface);
  border: 1px solid var(--up-border);
  border-radius: var(--up-radius);
  padding: var(--up-space-md);
  overflow-x: auto;
  font-size: 13px;
}
.doc-body code { background: var(--up-surface); border-radius: var(--up-radius-sm); padding: 0 3px; font-size: 0.92em; }
.doc-body pre code { background: none; padding: 0; }
.doc-editor { max-width: 640px; }
.doc-editor textarea {
  font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--up-fg);
  background: var(--up-surface);
  border: 1px solid var(--up-line);
  border-radius: var(--up-radius-sm);
  padding: var(--up-space-md);
  width: 100%;
}

/* Tablet + narrow laptop. Tables could only scroll below 700px, so between
   701 and 1000 — where the sidebar is still taking 220px and the content
   column is at its narrowest — a wide table pushed the whole PAGE sideways
   instead of scrolling inside itself. No white-space: nowrap here (that's a
   phone measure): text still wraps, and only a table that genuinely can't fit
   gets its own scrollbar. */
@media (min-width: 701px) and (max-width: 1000px) {
  .table { display: block; overflow-x: auto; }
}
/* Same rule, shifted for the zoomed font scales: `zoom` shrinks the layout but
   a media query still measures the raw viewport, so a 1024px window at xlarge
   only has 1024/1.3 = 787px to lay a table out in and needs the scroll long
   before the 1000px cutoff says so. Threshold = 1000 x that scale's zoom. */
@media (min-width: 701px) and (max-width: 1150px) {
  body[data-font-scale="large"] .table { display: block; overflow-x: auto; }
}
@media (min-width: 701px) and (max-width: 1300px) {
  body[data-font-scale="xlarge"] .table { display: block; overflow-x: auto; }
}

/* Mobile pass */
@media (max-width: 700px) {
  /* Normal document scroll (the internal-scroll lock broke scrolling in the
     installed PWA). viewport-fit=cover makes the safe-area insets real, so the
     top clears the status bar and the bottom leaves room for the fixed nav +
     home indicator. */
  .content { padding: calc(env(safe-area-inset-top, 0px) + var(--up-space-md)) var(--up-space-sm) calc(96px + env(safe-area-inset-bottom, 0px)); }
  /* Desktop h1 came down to 27px, so the old 27px mobile override no longer
     stepped down at all. A phone title wants to be smaller than the body
     column, not the same size. */
  h1 { font-size: 22px; }
  h2 { font-size: 17px; }
  .table { display: block; overflow-x: auto; white-space: nowrap; }
  .docs-layout { grid-template-columns: minmax(0, 1fr); }
  .doc-sidebar { border-right: none; border-bottom: 1px solid var(--up-border); padding: 0 0 var(--up-space-md); }
  .stack, .doc-editor, .doc-body { max-width: 100%; }

  /* No topbar on phones — everything it held (search, notifications, theme,
     company switch, profile/settings/logout) lives in the bottom nav and its
     "More" sheet (lib/layout.js mobileMenu). */
  .topbar { display: none; }

  /* ...but with no topbar, content that SCROLLS up would show under the status
     bar / dynamic island, and there's a strip below the fixed bottom nav (home
     indicator / Safari's URL bar) where it shows too. Two fixed opaque masks in
     the chrome color seal those edges so nothing bleeds into them. The top mask
     matches .content's top padding, so content sits flush below it. */

  /* "More" opens a bottom SHEET: the sidebar slides UP from the bottom, full
     width with a rounded top and a grabber, over the content. Same
     .sidebar-open toggle (nav.js) — only the direction changes. */
  .sidebar {
    position: fixed;
    inset: auto 0 0 0;
    width: 100%;
    max-width: 100%;
    max-height: 84vh;
    z-index: 210;
    /* Push fully past the bottom edge AND hide it: on iOS Safari the bottom
       URL bar shifts the viewport enough that a plain translateY(100%) leaves
       the sheet's header peeking above the browser chrome. The extra offset +
       visibility:hidden guarantee it's gone until opened; visibility flips to
       visible instantly on open and hides only after the slide-out finishes. */
    /* Emerges from the bottom bar: scales up + fades in from bottom-center on
       an iOS-sheet spring, so it reads as expanding out of the pill rather than
       just sliding. visibility keeps it fully gone (no peek) until opened. */
    transform: translateY(100%) scale(0.94);
    transform-origin: bottom center;
    opacity: 0;
    visibility: hidden;
    transition: transform 0.42s cubic-bezier(0.32, 0.72, 0, 1),
                opacity 0.26s ease,
                visibility 0s linear 0.42s;
    border-right: none;
    border-top: 1px solid var(--up-chrome-edge);
    border-radius: 18px 18px 0 0;
    box-shadow: 0 -10px 44px rgba(0, 0, 0, 0.55);
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .app-shell.sidebar-open .sidebar {
    transform: translateY(0) scale(1);
    opacity: 1;
    visibility: visible;
    transition: transform 0.42s cubic-bezier(0.32, 0.72, 0, 1),
                opacity 0.24s ease;
  }
  /* inline-flex, not block: the 24x24 minimum needs the glyph centred in it. */
  .sidebar-close { display: inline-flex; align-items: center; justify-content: center; }
  .nav-grip { display: none; }
  /* Grabber handle at the top of the sheet. */
  .sidebar-head { position: relative; padding-top: 22px; }
  .sidebar-head::before {
    content: ""; position: absolute; top: 9px; left: 50%; transform: translateX(-50%);
    width: 40px; height: 4px; border-radius: 2px;
    background: color-mix(in srgb, var(--up-paper) 22%, transparent);
  }
  /* Desktop pins these to 240px so the collapse animation clips instead of
     reflowing (see .sidebar above); the mobile drawer isn't width-animated,
     so let them fill its full (slightly wider, 260px) box instead. */
  .sidebar-head, .sidebar-nav { width: 100%; }
  .sidebar-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    z-index: 205;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 0.3s ease, visibility 0s linear 0.3s;
  }
  .app-shell.sidebar-open .sidebar-backdrop {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: opacity 0.3s ease;
  }
}

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

/* CRM */
.deal-value {
  font-weight: var(--up-weight-semibold);
  /* The readable accent, not the fill: on a card's own plate the fill value
     measured 4.36:1 at 15px. Same split as every other accent-as-text in
     this file. */
  color: var(--up-copper-text);
  font-size: 15px;
  margin-left: var(--up-space-xs);
}
.agenda-day { margin-bottom: var(--up-space-lg); }
/* The agenda is a reading view, not a grid — its blocks get room to breathe
   and a full-width measure rather than a column's worth. */
.agenda-day .ev { max-width: 560px; font-size: 13.5px; margin-bottom: 5px; }
.agenda-overdue { color: var(--up-danger); }
/* A URL is long and wraps; pill radius turned it into a lozenge. */
.ics-url {
  display: block;
  background: var(--up-surface);
  border: 1px solid var(--up-border);
  border-radius: var(--up-radius);
  padding: var(--up-space-sm);
  font-size: 13px;
  word-break: break-all;
}

/* Excel-style grid tables (gear locker & friends) — the outer edge rounds
   with the rest of the UI; individual cells stay square, so the grid
   itself still reads as a dense data table. */
.table-grid { border: 1px solid var(--up-line); border-radius: var(--up-radius); overflow: hidden; }
.table-grid th, .table-grid td {
  border: 1px solid var(--up-line);
  padding: 5px 8px;
  font-size: 13px;
}
.table-grid th {
  background: var(--up-surface-2);
  border-color: var(--up-line);
}
.table-grid tbody tr:hover td {
  background: color-mix(in srgb, var(--up-copper) 5%, transparent);
}

/* Click-to-open dropdown popover (<details>) */
.menu { position: relative; display: inline-block; }
.menu > summary {
  list-style: none;
  cursor: pointer;
  display: inline-block;
}
.menu > summary::-webkit-details-marker { display: none; }
.menu[open] > summary { background: var(--up-surface); border-radius: var(--up-radius-sm); }
/* Quick Create's trigger is a filled .btn, not a plain text link like the
   other topbar menus — .menu[open] > summary above would otherwise swap it
   to the neutral surface color while the panel is open. */
.quick-create-menu[open] > summary.qc-trigger { background: var(--up-copper); }
/* .menu-panel sets its own `display`, which as an author rule outranks the
   UA stylesheet's `details:not([open]) > *:not(summary) { display: none }`
   — so closed panels stay visible unless we hide them explicitly here. */
.menu:not([open]) > .menu-panel { display: none; }
.menu-panel {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 20;
  display: flex;
  gap: var(--up-space-sm);
  align-items: center;
  background: var(--up-surface-2);
  border: 1px solid var(--up-line);
  border-radius: var(--up-radius);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.5);
  padding: var(--up-space-sm);
  white-space: nowrap;
}
.menu-panel select { padding: 5px 8px; font-size: 13px; }
/* Opt-in for a trigger positioned on the left of the screen (e.g. the
   gear item page's Transfer button) — .menu-panel's bare default expands
   leftward, which is correct for the topbar's far-right user-menu but
   runs the panel off the left edge of a narrow phone screen anywhere
   else. */
.menu-expand-right > .menu-panel { left: 0; right: auto; }

.menu-list {
  flex-direction: column;
  align-items: stretch;
  left: 0;
  right: auto;
  min-width: 220px;
  padding: var(--up-space-xs);
}
.qc-panel { min-width: 260px; gap: 0; }
/* Each Quick Create item (Task/Project/To-do) is its own <details name=
   "qc-item"> — the shared `name` makes them mutually exclusive natively,
   so opening one closes whichever other was open, and the form stays
   hidden until its label is clicked. */
.qc-item { padding: 0 var(--up-space-sm); }
.qc-item > summary {
  list-style: none;
  cursor: pointer;
  padding: 9px 2px;
  font-size: 14px;
  font-weight: var(--up-weight-medium);
  color: var(--up-fg);
}
.qc-item > summary::-webkit-details-marker { display: none; }
.qc-item > summary:hover { color: var(--up-copper); }
.qc-item + .qc-item { border-top: 1px solid var(--up-line); }
.qc-form { gap: var(--up-space-sm); margin: 0 0 var(--up-space-md); max-width: none; }
.qc-empty { padding: var(--up-space-xs) var(--up-space-sm) 0; max-width: 240px; }
.menu-list a {
  padding: 7px 10px;
  text-decoration: none;
  color: var(--up-fg);
  font-size: 14px;
  border-radius: var(--up-radius-sm);
}
.menu-list a:hover { background: var(--up-surface); color: var(--up-copper); }
.menu-list .menu-kicker { padding: 7px 10px 2px; }
/* The company you're already in. It had no rule at all, so it rendered as an
   unpadded span between two full-width buttons and read as a heading that had
   fallen into the list. Same row metrics as its neighbours, copper and marked
   rather than bordered — it's a position, not a control. */
.menu-list .menu-current {
  display: flex; align-items: baseline; gap: 8px;
  padding: 7px 10px; border-radius: var(--up-radius-sm);
  font-size: 14px; font-weight: 600; color: var(--up-copper);
}
.menu-list .menu-current::before {
  content: ""; flex: none; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; transform: translateY(-2px);
}
.menu-list .menu-divider { height: 1px; margin: var(--up-space-xs) 0; background: var(--up-line); }
.menu-newws { padding: 0 10px; }
.menu-newws-trigger {
  display: block; width: 100%; text-align: left;
  background: none; border: none;
  cursor: pointer;
  font: inherit; font-size: 14px;
  color: var(--up-fg);
  padding: 7px 0;
}
.menu-newws-trigger:hover { color: var(--up-copper); }

/* Cross-document view transition: a soft fade out of whatever page you're
   leaving (most noticeably the login page) into whatever loads next,
   instead of the instant cut a normal navigation gives you — no JS, and
   it's a real browser-native page navigation the whole time. Every page
   shares this stylesheet, so it quietly smooths every navigation in the
   app, not just login. Progressive enhancement: browsers without support
   (this API is recent) just navigate instantly like they always did —
   nothing to fall back to, nothing that can break. */
@view-transition {
  navigation: auto;
}
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.45s;
  animation-timing-function: ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 0.01s;
  }
}

/* ---- Home title-card hero (routes/home.js heroFrame) ----
   A graded "lit screen" that stays cinematic in BOTH themes: it's built from
   the fixed brand rawcolors (Ink stage, copper key-light, navy shadow), never
   the theme-dependent surface tokens, so the frame reads the same dark whether
   the page around it is Ink or cream. Text inside is always paper. */
/* max-width is load-bearing, not defensive. `aspect-ratio` and `min-height`
   together can drive the WIDTH: once the container is narrow enough that the
   natural height falls under the floor, min-height wins and the ratio expands
   the box sideways to match — 300px x 21/8 = 787.5px, which at a 1024 viewport
   with the sidebar is 36px wider than its parent and scrolled the whole page
   horizontally. Capping the width lets the ratio break instead of the layout,
   which is the right thing to give up. */
.hero-frame {
  position: relative; margin: 0 0 20px; border-radius: 10px; overflow: hidden;
  aspect-ratio: 21 / 8; min-height: 300px; max-width: 100%;
  border: 1px solid color-mix(in srgb, var(--up-paper) 14%, transparent);
}
/* Generated grade — NOT a photo, so it's universal. Warm copper key-light pool
   top-right, cool navy shadow bottom-left, over Ink: the teal-orange grade. */
.frame-media {
  position: absolute; inset: 0;
  background:
    radial-gradient(70% 80% at 72% 30%, color-mix(in srgb, var(--up-copper) 60%, transparent), transparent 60%),
    radial-gradient(90% 95% at 16% 92%, color-mix(in srgb, var(--up-navy) 85%, transparent), transparent 62%),
    linear-gradient(155deg, color-mix(in srgb, var(--up-navy) 45%, var(--up-ink)), var(--up-ink) 72%);
}
.frame-media::after { /* grain, heavier inside the frame */
  content: ""; position: absolute; inset: 0; opacity: 0.13; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
.frame-vignette { position: absolute; inset: 0; background: radial-gradient(120% 120% at 50% 40%, transparent 46%, rgba(0,0,0,0.6) 100%); }
.frame-scrim { position: absolute; inset: 0; background: linear-gradient(0deg, rgba(0,0,0,0.72) 0%, rgba(0,0,0,0.15) 42%, transparent 70%); }
/* Thin copper letterbox rules top & bottom — the frame edge. */
.frame-rule { position: absolute; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--up-copper) 70%, transparent) 10%, color-mix(in srgb, var(--up-copper) 70%, transparent) 90%, transparent); opacity: 0.5; }
.frame-rule.t { top: 14px; } .frame-rule.b { bottom: 14px; }
.frame-content { position: absolute; left: 0; bottom: 0; padding: 32px 36px; z-index: 3; }
.hero-frame .slug { display: inline-flex; align-items: center; gap: 12px; font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; font-weight: 700; color: var(--up-copper); margin-bottom: 12px; }
.hero-frame .slug .tc { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.05em; color: color-mix(in srgb, var(--up-paper) 70%, transparent); font-weight: 500; }
.hero-frame h1 {
  font-family: var(--up-font-display); font-weight: 400; margin: 0;
  font-size: clamp(34px, 5.5vw, 64px); line-height: 1.0; letter-spacing: -0.01em;
  text-wrap: balance; color: var(--up-paper);
  text-shadow: 0 2px 30px rgba(0,0,0,0.65), 0 0 34px color-mix(in srgb, var(--up-copper) 22%, transparent);
}
.hero-frame .sub { color: color-mix(in srgb, var(--up-paper) 82%, transparent); font-size: 15px; max-width: 50ch; margin: 12px 0 0; text-shadow: 0 1px 12px rgba(0,0,0,0.7); }
@media (max-width: 720px) {
  /* Let the frame collapse to its content height on narrow screens. The
     content switches to relative (not static) so it still flows — giving the
     frame its height — while keeping z-index above the grade layers. */
  .hero-frame { aspect-ratio: auto; min-height: 0; }
  .frame-content { position: relative; padding: 26px 22px; }
}
.hero-frame.welcome-in { animation: welcome-rise 0.8s ease-out 2.7s both; }
@media (prefers-reduced-motion: reduce) { .hero-frame.welcome-in { animation: none; } }

/* Welcome animation: only on the landing right after login (see
   routes/home.js justLoggedIn) — a quiet fade + rise, kicker then
   heading, never on ordinary visits to Home. */
@keyframes welcome-rise {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
.welcome-in .up-kicker { animation: welcome-rise 0.5s ease-out both; }
.welcome-in h1 { animation: welcome-rise 0.5s ease-out 0.08s both; }
@media (prefers-reduced-motion: reduce) {
  .welcome-in .up-kicker, .welcome-in h1 { animation: none; }
}

/* Full-screen splash: every login (routes/home.js justLoggedIn) — sits
   above everything, fades itself out, and stops intercepting clicks the
   moment it's gone. `pointer-events: none` unconditionally (not just
   after the animation ends) is deliberate belt-and-suspenders: if the
   fade-out animation ever gets frozen mid-way — e.g. a cold app launch
   backgrounding the tab while the CSS animation clock is paused, which
   installed-PWA standalone mode is more prone to than a normal browser
   tab — a stuck opaque overlay would otherwise block every tap on the
   page indefinitely, with no way to tell why. Losing the ability to
   click something *through* the splash during its brief on-screen
   window is a fine trade for never risking that. */
.welcome-splash {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--up-space-sm);
  text-align: center;
  pointer-events: none;
  color: var(--up-paper);
  /* Opaque cinematic title card — a lit screen (copper key-light + navy
     shadow over Ink) so it's the ONLY thing visible while it holds, then
     fades to reveal Home. Fixed brand rawcolors → same in both themes. */
  background:
    radial-gradient(58% 66% at 74% 22%, color-mix(in srgb, var(--up-copper) 34%, transparent), transparent 60%),
    radial-gradient(74% 84% at 22% 92%, color-mix(in srgb, var(--up-navy) 50%, transparent), transparent 62%),
    var(--up-ink);
  animation: welcome-splash-out 0.9s ease-in 2.7s both;
}
.welcome-splash .up-kicker { color: var(--up-copper); animation: welcome-rise 0.6s ease-out 0.15s both; }
.welcome-splash-mark { height: 56px; width: auto; margin-bottom: var(--up-space-md); }

/* The UR mark as a loading indicator (lib/mark.js `loader()`). Copper flows
   down the R's leg, holds, then recedes — a seamless loop because the first
   and last frames are the same position.
   
   Not a spinner, on purpose: this app is for people with ADHD and a rotating
   element in peripheral vision costs attention instead of reassuring. One warm
   element moving slowly along a path the eye already follows says "working"
   without demanding anything.

   The sweep translates the copper wedge FORWARD along the leg axis, which
   moves its cut line past the tip and leaves no copper at all. Backwards is
   not the mirror image — it floods the bowl and stem, and copper carrying a
   letterform is the one thing the mark's rules forbid (see lib/mark.js). So
   the loop breathes forward-and-back through the rest position, never past it
   on the near side. Offset comes from mark.SWEEP; keep the two in step. */
.now-loader { height: 56px; width: auto; display: block; }
.now-loader-sweep { animation: now-leg-sweep 2.6s ease-in-out infinite; }
@keyframes now-leg-sweep {
  0%   { transform: translate(169.1px, 110.8px); }   /* past the tip: no copper */
  45%  { transform: translate(0, 0); }               /* at rest */
  72%  { transform: translate(0, 0); }
  100% { transform: translate(169.1px, 110.8px); }
}
@media (prefers-reduced-motion: reduce) {
  .now-loader-sweep { animation: none; transform: none; }
}
.welcome-splash h1 { margin: 0; font-size: 48px; color: var(--up-paper); animation: welcome-rise 0.6s ease-out 0.28s both; }
.welcome-splash-name { color: var(--up-neutral-300); font-size: 15px; }
@keyframes welcome-splash-out {
  0% { opacity: 1; visibility: visible; }
  99% { opacity: 0; visibility: visible; }
  100% { opacity: 0; visibility: hidden; }
}
@media (prefers-reduced-motion: reduce) {
  .welcome-splash { animation-duration: 0.01s; animation-delay: 1s; }
  .welcome-splash .up-kicker, .welcome-splash h1 { animation: none; }
}

/* Stacked card entrance: same every-login gate as the splash — the
   cards land one after another as the splash clears, instead of all at
   once. Counts <section> only (:nth-of-type), so the .hero-frame sitting
   above them doesn't throw off the numbering. Sections now
   live inside .dashboard-grid rather than directly under .home-sections
   (see the dashboard-grid comment below), so this targets that grid's
   direct children instead. */
.home-sections.stack-in .dashboard-grid > section {
  animation: welcome-rise 0.7s ease-out both;
}
.home-sections.stack-in .dashboard-grid > section:nth-of-type(1) { animation-delay: 2.95s; }
.home-sections.stack-in .dashboard-grid > section:nth-of-type(2) { animation-delay: 3.15s; }
.home-sections.stack-in .dashboard-grid > section:nth-of-type(3) { animation-delay: 3.35s; }
.home-sections.stack-in .dashboard-grid > section:nth-of-type(4) { animation-delay: 3.55s; }
.home-sections.stack-in .dashboard-grid > section:nth-of-type(5) { animation-delay: 3.75s; }
@media (prefers-reduced-motion: reduce) {
  .home-sections.stack-in .dashboard-grid > section { animation: none; }
}

/* Dashboard split layout (Home first, reusable anywhere a page wants an
   Asana-style "cards side by side, some full width" grid instead of one
   flat stack). Two columns by default; .dashboard-full spans both for a
   section that wants the whole row (My Tasks, General tasks below). */
.dashboard-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--up-space-lg);
  align-items: start;
}
.dashboard-grid > * { min-width: 0; }
.dashboard-grid .dashboard-full { grid-column: 1 / -1; }
@media (max-width: 900px) {
  .dashboard-grid { grid-template-columns: minmax(0, 1fr); }
}

/* Section heading with an inline "add" trigger (the fm-plus button opens a
   form modal — see .form-modal). The heading takes the row; the trigger sits
   flush at the end. */
.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--up-space-sm);
}
.section-head h1, .section-head h2, .section-head h3 { margin: 0; flex: 1 1 auto; }
/* A card whose page title already names it leads with its caption instead of
   repeating the name (see the People roster). The caption then has to take the
   heading's job of pushing the trailing control to the far edge. */
.section-head > p.up-caption { margin: 0; flex: 1 1 auto; }

/* Gear */
.unconfirmed {
  font-size: 12px;
  font-weight: var(--up-weight-semibold);
  letter-spacing: 0;
  color: var(--up-copper);
  border: 1px dashed var(--up-copper);
  border-radius: var(--up-radius-sm);
  padding: 1px 6px;
}
.unconfirmed-stale {
  color: var(--up-danger);
  border-color: var(--up-danger);
}
.filter-bar { margin-bottom: var(--up-space-lg); }

.badge {
  display: inline-block;
  font-size: 12px;
  font-weight: var(--up-weight-semibold);
  letter-spacing: 0;
  color: var(--up-copper-text);
  border: 1px solid var(--up-copper);
  padding: 1px 8px;
  border-radius: var(--up-radius-sm);
}

/* A wide My-Projects table (already internally scrollable — see .table) still
   bubbles its unscrolled max-content size up through this plain block wrapper
   unless it's told not to. */
@media (max-width: 700px) {
  .home-sections { min-width: 0; }
}

/* In-page confirm dialog (confirm.js) — replaces window.confirm()'s
   native OS-styled box with one in the app's own paper-on-ink language,
   for the same reason cards went paper: legibility and consistency, not
   a system alert that looks like it came from somewhere else. */
/* Dialogs live in the top layer (outside .content), so they carry their own
   theme tokens: a dark panel in dark mode, near-white in light mode. */
body { --up-dialog-bg: color-mix(in srgb, var(--up-paper) 8%, var(--up-ink)); --up-dialog-fg: var(--up-paper); }
body[data-theme$="light"] { --up-dialog-bg: #FBFAF7; --up-dialog-fg: var(--up-ink); }
.confirm-dialog {
  border: 1px solid color-mix(in srgb, var(--up-dialog-fg) 15%, transparent);
  border-radius: var(--up-radius);
  padding: var(--up-space-lg);
  background: var(--up-dialog-bg);
  color: var(--up-dialog-fg);
  max-width: 360px;
  width: calc(100% - 2 * var(--up-space-lg));
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45);
}
.confirm-dialog::backdrop { background: color-mix(in srgb, var(--up-ink) 60%, transparent); }
.confirm-dialog-message { margin: 0 0 var(--up-space-lg); font-size: 15px; line-height: 1.4; }
.confirm-dialog-actions { display: flex; justify-content: flex-end; gap: var(--up-space-sm); }
.confirm-dialog .btn-ghost { border-color: color-mix(in srgb, var(--up-dialog-fg) 30%, transparent); color: var(--up-dialog-fg); }

/* Avatar reposition dialog (avatar-crop.js) — same theme-aware panel as
   confirm-dialog, pan/zoom a photo before it's uploaded. */
.avatar-crop-dialog {
  border: 1px solid color-mix(in srgb, var(--up-dialog-fg) 15%, transparent);
  border-radius: var(--up-radius);
  padding: var(--up-space-lg);
  background: var(--up-dialog-bg);
  color: var(--up-dialog-fg);
  max-width: 360px;
  width: calc(100% - 2 * var(--up-space-lg));
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45);
}
.avatar-crop-dialog::backdrop { background: color-mix(in srgb, var(--up-ink) 60%, transparent); }
.avatar-crop-dialog p { margin: 0 0 var(--up-space-md); font-size: 14px; }
.avatar-crop-stage {
  display: block;
  width: 260px;
  height: 260px;
  margin: 0 auto var(--up-space-md);
  border-radius: 50%;
  border: 2px solid var(--up-copper);
  touch-action: none;
  cursor: grab;
}
.avatar-crop-stage:active { cursor: grabbing; }
.avatar-crop-zoom { display: block; width: 100%; margin: 0 0 var(--up-space-lg); accent-color: var(--up-copper); }
.avatar-crop-actions { display: flex; justify-content: flex-end; gap: var(--up-space-sm); }

/* Gear QR labels (lib/qrcode.js) — the item page's own preview, and the
   sticker-sheet "Print labels" view (routes/gear.js printLabels). */
.gear-label-preview { display: flex; align-items: center; gap: var(--up-space-md); }
.gear-label-preview svg { flex: none; border-radius: var(--up-radius-sm); background: #fff; }

.label-sheet {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: var(--up-space-md);
}
.gear-label {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: var(--up-space-sm);
  border: 1px dashed var(--up-line);
  border-radius: var(--up-radius-sm);
  background: #fff;
  color: #000;
  text-align: center;
}
.gear-label svg { background: #fff; }
.gear-label-text { display: flex; flex-direction: column; font-size: 12px; }
.gear-label-text span { color: #555; }

/* Print: strip the app chrome (sidebar/topbar/backdrops) on any page —
   nobody wants three inches of dark navy burned into a sheet of labels —
   plus .no-print for content that's only useful on screen (the "Print"
   button and page heading on the label sheet itself). */
@media print {
  .sidebar, .topbar, .sidebar-backdrop, .menu-backdrop { display: none !important; }
  .app-shell, .main-col, .content { all: unset; display: block; }
  .no-print { display: none !important; }
  .gear-label { border-style: solid; break-inside: avoid; }
}

/* The Workflows table featured on Company Home — was the kanban board
   (Phase 28) until the V3 consolidation swapped in the table. Just spacing
   inside the page flow. */
.home-board { margin-top: var(--up-space-lg); }
.home-board .section-head { align-items: baseline; }

/* People (Phase 40): the roster is card ROWS, not a table — warmth is scanned
   ("who's slipping?"), and a table makes you read columns to find that out.
   Each row is face · who they are · the one next step · the warmth chip. */
.p-group + .p-group { margin-top: var(--up-space-md); }
.p-group-head {
  font-size: var(--up-text-caption); font-weight: var(--up-weight-semibold);
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--up-muted);
  padding: var(--up-space-xs) 2px;
}
/* --up-copper-text, not --up-copper: this is copper being READ at 11px, which
   is the whole reason the readable variant exists. The fill value measured
   4.49:1 here — a hair under, and the token that fixes it already existed. */
.p-group-head .n { color: var(--up-copper-text); }
/* A row holding a name, a company and a status wraps on a phone, so it takes
   the panel radius. At --up-radius-sm (999px) it rendered as a 114px lozenge. */
.person-row {
  display: flex; align-items: center; gap: var(--up-space-sm);
  padding: 10px 12px; margin-bottom: 6px;
  border: 1px solid var(--up-glass-edge); border-radius: var(--up-radius);
  background: var(--up-glass); box-shadow: inset 0 1px 0 var(--up-glass-hi);
}
/* An org's accent is DATA — anyone can pick any hex — so white initials on it
   cannot be guaranteed to clear 4.5:1, and measured 3.98-4.06 on the seeded
   ones. The hue still identifies the company; it just carries as a tint and a
   ring over a known surface instead of as a full fill, so the text sits on a
   value this system controls. routes/crm.js passes the accent in as --face. */
.p-face {
  width: 34px; height: 34px; border-radius: 50%; flex: none;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: var(--up-weight-bold);
  --face: var(--up-accent);
  background: color-mix(in srgb, var(--face) 26%, var(--up-surface-2));
  color: var(--up-fg);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--face) 55%, transparent);
}
.p-face-lg { width: 54px; height: 54px; font-size: 18px; }
.person-row .p-main { flex: 1; min-width: 0; }
.p-name { font-weight: var(--up-weight-semibold); }
.p-meta { font-size: 12px; color: var(--up-muted); margin-top: 1px; }
.p-next { font-size: 13px; margin-top: 3px; }
.p-next::before { content: "→ "; color: var(--up-copper); }
.p-next.none::before { content: "+ "; }
.p-next.none { color: var(--up-muted); }
/* A modal trigger that reads as a link — "Add a next step" sits inline in a
   roster row, where a button would shout. */
.linkish {
  background: none; border: none; padding: 0; font: inherit; cursor: pointer;
  color: var(--up-muted); text-decoration: underline; text-underline-offset: 2px;
}
.linkish:hover { color: var(--up-copper); }

/* Warmth chips. Semantic, not the copper accent: amber = slipping, danger =
   properly cold, quiet green = fine. Their appearance IS the signal. */
/* Renamed from .touch-chip with the Cyclorama redesign: there is nothing warm
   in this palette any more, and a class called "warm" in a cool system is the
   kind of drift that misleads whoever reads it next. It also carries real
   information — how long since you heard from someone — so it has to be
   legible rather than decorative, which is why the old `opacity: 0.75` on
   .fresh is gone: it measured 3.14:1. */
.touch-chip {
  flex: none; font-size: 11px; font-weight: var(--up-weight-semibold);
  padding: 3px 10px; border-radius: 999px; border: 1px solid currentColor;
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
.touch-chip.fresh { color: #6FD08C; background: color-mix(in srgb, #6FD08C 12%, transparent); }
.touch-chip.quiet { color: var(--up-due); background: color-mix(in srgb, var(--up-due) 12%, transparent); }
.touch-chip.cold  { color: var(--up-danger); background: color-mix(in srgb, var(--up-danger) 12%, transparent); }
body[data-theme$="light"] .touch-chip.fresh { color: #196B3C; background: color-mix(in srgb, #196B3C 10%, transparent); }

/* The client profile header — identity left, warmth right. */
.profile-head {
  display: flex; align-items: flex-start; gap: var(--up-space-md);
  flex-wrap: wrap; margin-bottom: var(--up-space-md);
}
.profile-head h1 { margin: 0; }
.profile-id { flex: 1; min-width: 200px; }
.profile-org { margin: 2px 0 0; color: var(--up-muted); }
.profile-contact { margin: 6px 0 0; font-size: 14px; }
.profile-contact .dot { color: var(--up-muted); margin: 0 8px; }
.profile-tags { margin: 8px 0 0; display: flex; flex-wrap: wrap; gap: 5px; }
.tag-chip {
  font-size: 11px; padding: 2px 9px; border-radius: 999px;
  border: 1px solid var(--up-border); color: var(--up-muted);
}
.profile-warmth { display: flex; flex-direction: column; align-items: flex-end; gap: 5px; text-align: right; }
/* 45f — the reply you owe. Deliberately NOT --up-due amber or --up-danger red:
   this line never escalates, so it uses the readable copper (--up-copper-text,
   which lifts on ink and deepens on cream) purely to separate it from the
   muted caption above it. A debt three weeks old must not shout louder than
   one from yesterday. */
.owed-note { color: var(--up-copper-text); }
.step-list { list-style: none; margin: 0 0 var(--up-space-sm); padding: 0; }
.step-list li {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--up-space-sm); padding: 7px 0; border-bottom: 1px solid var(--up-border);
}
.step-list li:last-child { border-bottom: 0; }

/* A quiet link inside a deck card (the nudge card's "open their profile"). */
.now-cardlink { font-size: 12px; color: var(--n-cool); text-decoration: underline; text-underline-offset: 2px; }

/* The capture dialog — what "+" opens app-wide. One field, deliberately the
   biggest thing on screen, so adding a card costs a keystroke and no
   decisions. Mirrors the deck's own full-screen capture in a dialog. */
.capture-modal {
  border: 1px solid var(--up-glass-edge); border-radius: var(--up-radius-lg);
  padding: var(--up-space-lg); width: min(520px, calc(100vw - 32px));
  background: var(--up-panel); color: var(--up-fg);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.4);
}
.capture-modal::backdrop { background: color-mix(in srgb, #100F0D 62%, transparent); backdrop-filter: blur(3px); }
.cap-label {
  display: block; font-size: 11px; letter-spacing: 0.15em;
  text-transform: uppercase; color: var(--up-copper); font-weight: var(--up-weight-semibold);
}
/* Element + two classes on purpose: the app's base field rule is
   `input:not([type=checkbox]):not([type=radio])`, and :not() contributes its
   argument's specificity, so that rule scores (0,2,1) and would beat a plain
   two-class selector. Matching its element part clears it without !important. */
.capture-modal .cap-form input.cap-input,
.capture-modal .cap-form input.cap-input:hover,
.capture-modal .cap-form input.cap-input:focus {
  width: 100%; margin-top: var(--up-space-sm);
  font-family: var(--up-font-display); font-size: clamp(21px, 4vw, 27px);
  color: var(--up-fg); background: none; border: none;
  border-bottom: 1px solid color-mix(in srgb, var(--up-copper) 55%, transparent);
  border-radius: 0; padding: 4px 2px 10px; outline: none; box-shadow: none;
}
/* What the bar read out of your sentence, echoed in words. An ISO string is
   something you have to decode; "Friday · 8 August" is something you can
   check at a glance — and a misread is obvious instantly rather than after
   the task has landed on the wrong day. */
.cap-read {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  margin-top: 10px; font-size: 13px;
}
.cap-read-date { color: var(--up-copper); font-weight: var(--up-weight-semibold); }
.cap-read-from { color: var(--up-muted); font-size: 12px; }

/* The offers. Never a required step — the default action (plain capture) is
   what Enter does with nothing selected, so this list can be ignored
   entirely by continuing to type. */
.cap-results {
  display: flex; flex-direction: column; gap: 2px;
  margin-top: 12px; max-height: 260px; overflow-y: auto;
}
.cap-result {
  display: flex; align-items: baseline; gap: 8px; width: 100%;
  padding: 9px 10px; border: 1px solid transparent; border-radius: var(--up-radius-sm);
  background: none; color: var(--up-fg); font: inherit; font-size: 14px;
  text-align: left; cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease;
}
.cap-result:hover { background: color-mix(in srgb, var(--up-fg) 6%, transparent); }
.cap-result.is-on {
  background: color-mix(in srgb, var(--up-fg) 9%, transparent);
  border-color: color-mix(in srgb, var(--up-copper) 45%, transparent);
}
.cap-result-kind {
  flex: none; font-size: 11px; font-weight: var(--up-weight-semibold);
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--up-muted);
}
.cap-result-name { flex: 1 1 auto; min-width: 0; font-weight: var(--up-weight-medium); }
.cap-result-sub { flex: none; font-size: 12px; color: var(--up-muted); font-variant-numeric: tabular-nums; }
.cap-chip {
  flex: none; font-size: 11px; font-weight: var(--up-weight-semibold);
  padding: 1px 7px; border-radius: 999px; white-space: nowrap; color: var(--up-fg);
  background: color-mix(in srgb, var(--chip, var(--up-copper)) 18%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--chip, var(--up-copper)) 40%, transparent);
}
.cap-read-sep { color: var(--up-muted); margin: 0 2px; }
/* The deck's own capture echoes the same reading, in its own quieter voice —
   it has no result list to sit above, so it's one line under the field. */
.now-cap-read {
  margin-top: 12px; font-size: 14px; letter-spacing: 0.01em;
  color: color-mix(in srgb, var(--n-copper-br, #C97A2E) 92%, transparent);
  font-variant-numeric: tabular-nums;
}
.cap-foot {
  display: flex; justify-content: space-between; align-items: center;
  gap: var(--up-space-sm); flex-wrap: wrap; margin-top: var(--up-space-md);
}
.create-destination {
  display: inline-flex; align-items: center; gap: 7px; min-width: 0;
  font-size: 12px; color: var(--up-muted);
}
.create-destination span { white-space: nowrap; }
.create-destination select {
  max-width: 180px; font: inherit; font-weight: var(--up-weight-medium); color: var(--up-fg);
  background: var(--up-surface); border: 1px solid var(--up-line); border-radius: 999px; padding: 5px 26px 5px 9px;
}
.create-destination select:focus-visible { outline: 2px solid var(--up-copper); outline-offset: 2px; }
.cm-destination { grid-column: 1 / -1; justify-content: space-between; padding-bottom: 2px; }
.cap-destination { margin-right: auto; }
.cap-more {
  background: none; border: none; padding: 0; font: inherit; cursor: pointer;
  font-size: 13px; color: var(--up-muted); text-decoration: underline; text-underline-offset: 3px;
}
.cap-more:hover { color: var(--up-copper); }

/* A client-facing document (lib/layout bareDoc): no app chrome at all, so the
   content sits on the cyc itself rather than inside the app's stage.

   This rule used to read `background: var(--up-paper)`, and that one token
   moved underneath it: in the workbench --up-paper WAS the paper, and in the
   cyclorama it is the default TEXT colour. The portal renders day-themed,
   where --up-paper is #101733 — so the page painted near-black ink as its
   ground and then set the same #101733 on every heading and every project
   name. The whole client-facing page was dark-on-dark at 1:1 and the only
   legible thing on it was the muted meta line. The wash is inherited from
   the shared shell rule above now; nothing here paints a ground at all. */
.bare-doc .content { max-width: 680px; margin: 0 auto; padding: var(--up-space-md); }

/* ===== The client portal (Phase 41) =========================================
   A page for someone who has never seen this app and never will again — it
   gets ONE column, generous type, and the studio's accent as the only color
   with a job. Deliberately not the app's chrome: no nav, no glass stack, no
   density. It's a letter, not a dashboard. */
.portal { max-width: 620px; margin: 0 auto; padding: 0 4px var(--up-space-lg); }
.portal-head { padding: var(--up-space-md) 0 var(--up-space-lg); }
.portal-brandline {
  display: flex; align-items: center; gap: 10px;
  font-size: 11px; letter-spacing: 0.15em; text-transform: uppercase; color: var(--up-muted);
}
/* An org's accent is user DATA — nobody can guarantee white clears on it, and
   the studio picks it in a colour well with no contrast check anywhere. Same
   fix the CRM avatars got: the hue comes in as --face and renders as a tint
   plus a ring over a surface the system controls, so the studio keeps its
   colour as identity while the initials sit on a known value. */
.portal-mark {
  width: 26px; height: 26px; border-radius: 9px; flex: none;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: var(--up-weight-bold); letter-spacing: 0;
  background: color-mix(in srgb, var(--portal-brand, var(--up-accent)) 26%, var(--up-surface-2));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--portal-brand, var(--up-accent)) 55%, transparent);
  color: var(--up-fg);
}
.portal-logo { height: 28px; width: auto; border-radius: 5px; }
.portal-head h1 {
  font-size: clamp(26px, 5vw, 34px); line-height: 1.15;
  margin: var(--up-space-md) 0 0; text-wrap: balance;
}
.portal-sub { color: var(--up-muted); margin-top: 6px; }
.portal-sect { margin-top: var(--up-space-lg); }
/* Was `color: var(--portal-brand)` — a section heading painted in whatever hex
   the studio happened to pick, over a ground the studio has never seen. The
   accent stays on the page (the mark, the dot, the ask's rim) where it is
   identity rather than a word you have to read. */
.portal-sect h2 {
  font-size: 11px; font-weight: var(--up-weight-semibold); letter-spacing: 0.15em;
  text-transform: uppercase; color: var(--up-muted);
  margin-bottom: var(--up-space-sm);
}
/* The count sits inside the muted h2, so `currentColor 15%` made it muted
   text on a muted tint — the same shape as the notification badge that
   measured 1.96:1. Both ends are named here instead of inherited. */
.portal-n {
  font-family: var(--up-font-body); letter-spacing: 0; padding: 1px 8px;
  border-radius: 999px; background: var(--up-surface-2); color: var(--up-fg);
}
/* A row is a pool of light, like every other region in this world — not a
   drawn box. --up-panel never existed after the redesign (nothing declares
   it), so the old fill resolved to nothing, and --up-radius-sm is 999px now,
   which turned each row into a lozenge the height of its content. */
.portal-row {
  border: 0; border-radius: var(--up-radius);
  padding: 15px 18px; margin-bottom: 8px;
  background: var(--up-glass, var(--up-surface));
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  box-shadow: inset 0 0 0 1px var(--up-line);
}
.portal-row b { display: block; font-weight: var(--up-weight-semibold); }
.portal-row small { display: block; color: var(--up-muted); font-size: 13px; margin-top: 3px; }
.portal-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 7px; vertical-align: 1px; }
/* Something is being asked of the reader — the one place the page raises its
   voice. On a cyc you raise it with LIGHT, not with a line: the pool gets
   brighter and its rim catches the studio's own accent. */
.portal-ask {
  background: var(--up-surface-2);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--portal-brand, var(--up-accent)) 45%, var(--up-line));
}
/* The one control on this page that commits — so it gets white-day, exactly
   like .btn in the app, and for the same reason. It used to be the studio's
   raw accent with #fff on top: the seeded copper #B5651D measures 3.0:1 that
   way, and a studio is free to pick a paler one tomorrow. */
.portal-btn {
  display: inline-block; margin-top: 12px; padding: 9px 18px;
  border-radius: var(--up-radius-sm);
  background: var(--up-strong-grad); color: var(--up-strong-fg);
  font-size: 13px; font-weight: var(--up-weight-semibold); text-decoration: none;
  box-shadow: 0 4px 10px -6px var(--up-strong-lift);
}
/* Delivered work is done, not dimmed: opacity: 0.6 on a whole row takes the
   project NAME down with it, and "what you already got" is the one section a
   client is most likely to come back for. It recedes by sitting flat on the
   cyc instead — no pool, no rim. */
.portal-done {
  background: none; box-shadow: none;
  -webkit-backdrop-filter: none; backdrop-filter: none;
  padding-left: 0; padding-right: 0;
}
.portal-clear { color: var(--up-muted); }
.portal-foot {
  margin-top: var(--up-space-xl); padding-top: var(--up-space-md);
  font-size: 12px; color: var(--up-muted); text-align: center; line-height: 1.7;
}

/* Ideas backlog (Phase 18): captured ideas as light cards in a responsive
   grid — small enough that several fit at a glance, roomy enough for the
   promote/delete controls to breathe. */
.idea-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: var(--up-space-md); margin-top: var(--up-space-md); }
.idea-card { margin: 0; }
.idea-card h3 { margin: 0; }
.idea-card p { margin: var(--up-space-xs) 0 var(--up-space-sm); }

/* Deliverables (Phase 18): each is an editable row — inline form on the
   left, status chip + meta on the right, wrapping on narrow screens. The
   chip is colored by where the deliverable sits in the review cycle: quiet
   for draft/in-progress, warm amber at needs-review, danger when changes
   are requested, green once approved. No green token exists app-wide, so
   the approved chip carries its own. */
.deliverable-row { display: flex; flex-wrap: wrap; gap: var(--up-space-sm); align-items: center; justify-content: space-between; padding: var(--up-space-sm) 0; border-bottom: 1px solid var(--up-border); }
.deliverable-row:last-child { border-bottom: 0; }
.deliverable-meta { display: flex; flex-wrap: wrap; gap: var(--up-space-sm); align-items: center; }
.status-chip {
  display: inline-block; font-size: 12px; font-weight: var(--up-weight-semibold);
  letter-spacing: 0; padding: 2px 9px;
  border-radius: var(--up-radius-sm); border: 1px solid currentColor;
}
/* Every chip reads a theme-scoped token now. The two literals here were the
   last ones left: #C98A2B measured 2.94:1 on the day stage and #4B9E6A 3.28:1
   — both invisible in exactly the theme their author wasn't looking at. */
.status-draft { color: var(--up-muted); }
.status-in_progress { color: var(--up-copper); }
.status-needs_review { color: var(--up-due); }
.status-changes_requested { color: var(--up-danger); }
.status-approved { color: var(--up-ok); }

/* Deliverable share control (Phase 21): the review-link row sits full-width
   under its deliverable, wrapping the URL + revoke/generate together. */
.deliverable-share { display: flex; flex-wrap: wrap; align-items: center; gap: var(--up-space-sm); flex-basis: 100%; margin-top: var(--up-space-xs); }
.deliverable-share .ics-url { word-break: break-all; }

/* Public client review page (Phase 21): standalone, centered, no app chrome.
   A pool of light on the cyc — the same thing a .card is, because a client
   looking at a cut is looking at the same world the studio works in. It was
   a paper slab on a dark ground, which is the workbench composition: the
   `--up-paper` fill is the TEXT colour now, so on any day theme this card
   was #101733 on #101733, and `--up-ink` captions were near-black on the
   night stage. A 16:9 responsive embed for the cut. */
.review-card {
  max-width: 760px; margin: var(--up-space-lg) auto;
  border: 0; border-radius: var(--up-radius);
  background: var(--up-glass, var(--up-surface));
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  box-shadow: inset 0 0 0 1px var(--up-line), var(--cyc-spill);
  padding: var(--up-space-lg);
}
.review-card h1, .review-card h2 { margin: 0 0 var(--up-space-sm); }
.review-card .up-caption { color: var(--up-muted); }
/* The studio's mark on the page: its accent as a dot beside its own name,
   which is identity rather than a value anything has to be read against. */
.review-face {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  margin-right: 8px; vertical-align: 1px;
  background: var(--face, var(--up-accent));
  box-shadow: 0 0 0 1px var(--up-line);
}
/* --up-radius-sm is 999px in this world (controls are pills), so the video
   was rendering as an ellipse with its own corners cropped off. */
.review-embed { position: relative; padding-top: 56.25%; margin-top: var(--up-space-md); border-radius: var(--up-radius); overflow: hidden; background: #000; }
.review-embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.review-form { display: flex; flex-direction: column; gap: var(--up-space-md); margin-top: var(--up-space-md); }
.review-form label { display: flex; flex-direction: column; gap: 6px; font-weight: var(--up-weight-medium); }
/* No field override here any more. It hard-coded `background: #fff` on a card
   that is now translucent, which made the name input a white pill on a
   near-white plate — invisible, on the one field the form REQUIRES. The
   app-wide input rule already styles fields from the stage tokens. */
/* The verdict is the point of this page, so Approve goes last and takes the
   far end of the row — it was a 12px `btn-small` sitting between two ghosts
   of the same size, which is the opposite of what reserving white-day is
   for. `margin-left: auto` on it rather than `justify-content: space-between`
   so the two secondary controls stay grouped when the row wraps. */
.review-actions { display: flex; flex-wrap: wrap; gap: var(--up-space-sm); align-items: center; margin-top: var(--up-space-sm); }
.review-actions .btn { margin-left: auto; align-self: center; }
/* Approved is the one genuinely positive state in the app and there is still
   no green token; this one is picked to clear on both stages rather than
   inherited from the workbench's #3d7d54, which measured under 3:1 on night. */
.review-approved { margin-top: var(--up-space-md); font-weight: var(--up-weight-semibold); color: var(--up-ok); }

/* Guest-to-episode offer (Phase 20): the "Create episode" control tucked
   under a booked guest deal on the CRM board — indented a touch so it reads
   as belonging to the deal above it. */
.deal-spawn { margin-top: var(--up-space-xs); }

/* ===== Interaction layer (Phase 15) =====
   The app-wide "feels alive" pass — Asana-style responsiveness. Almost
   entirely additive: transitions + hover/active states layered onto
   existing components, nothing structural. currentColor-based mixes so
   the same hover tint works on paper cards (ink text) and the dark
   ground (paper text) without per-surface variants. The check-pop
   animation pairs with public/js/interact.js, which delays the mark-done
   form submit ~200ms so the pop is actually visible before the reload. */
.btn, .btn-ghost, .todo-check {
  transition: background-color 0.15s ease, border-color 0.15s ease,
              color 0.15s ease, transform 0.1s ease, filter 0.15s ease;
}
.btn:hover { filter: brightness(1.12); }
.btn:active, .btn-ghost:active, .todo-check:active { transform: scale(0.95); }
.btn-ghost:hover { border-color: var(--up-copper); color: var(--up-copper); }

/* Rows respond to the pointer — table rows, Home/My Tasks rows, todos,
   feed items. Background only (no transform): rows that shift make
   tables feel wobbly, a tint is what Asana actually does. */
.table tbody tr, .home-task-row, .todo, .feed li {
  transition: background-color 0.12s ease;
}
.table tbody tr:hover, .home-task-row:hover, .todo:hover {
  background: color-mix(in srgb, currentColor 6%, transparent);
}

.todo-check:hover {
  border-color: var(--up-copper);
  background: color-mix(in srgb, var(--up-copper) 14%, transparent);
}
/* interact.js adds this the moment mark-done is clicked: instant fill +
   overshoot pop, then the form actually submits. */
.todo-check-pop {
  /* The Copper Rule. This one is only on screen for 220ms, which is exactly
     why it survived the sweep — but it's the confirmation that you completed
     something, so it should be the clearest thing on the row, not the least
     legible. --up-paper measured 2.29:1 night / 2.87:1 day. */
  background: var(--up-strong-bg) !important;
  color: var(--up-strong-fg) !important;
  border-color: var(--up-strong-bg) !important;
  animation: check-pop 0.22s ease-out;
}
@keyframes check-pop {
  0% { transform: scale(1); }
  45% { transform: scale(1.3); }
  100% { transform: scale(1); }
}

.project-tile, .board-card {
  transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.project-tile:hover, .board-card:hover {
  border-color: var(--up-copper);
  transform: translateY(-1px);
  box-shadow: 0 3px 10px color-mix(in srgb, var(--up-ink) 18%, transparent);
}

/* Keyboard focus: same copper ring inputs already use, now on every
   interactive element instead of the browser default (or nothing). */
a:focus-visible, button:focus-visible, summary:focus-visible, [draggable]:focus-visible {
  outline: 2px solid var(--up-copper);
  outline-offset: 2px;
  border-radius: var(--up-radius-sm);
}

@media (prefers-reduced-motion: reduce) {
  .btn, .btn-ghost, .todo-check, .project-tile, .board-card { transition: none; }
  .btn:active, .btn-ghost:active, .todo-check:active { transform: none; }
  .project-tile:hover, .board-card:hover { transform: none; }
  .todo-check-pop { animation: none; }
}

/* ===== Phase 16: task-list quick wins ===== */

/* Relative due dates (lib/time.js dueHtml) — exact ISO date lives in the
   title tooltip. Overdue reads through --up-danger, which .card re-tunes
   for cream, so one rule serves both surfaces. */
.due { font-size: 13px; color: var(--up-muted); white-space: nowrap; }
/* Inside a week — amber, between calm and trouble (Phase 28 urgency tiers). */
.due-soon { color: var(--up-due); font-weight: var(--up-weight-semibold); }
.due-overdue { color: var(--up-danger); font-weight: var(--up-weight-semibold); }

/* The topbar overdue chip was removed — see the note in lib/layout.js. Its
   replacement on phones is .bnav-dot: presence, not a score. Sized and placed
   like .bnav-badge so the two read as one family, but it carries no number and
   therefore cannot grow. */
.bnav-dot {
  position: absolute; top: -3px; right: -6px;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--up-due);
}

/* My Tasks inline add row: title stretches, picker/date/button hug it. */
.mytasks-add { display: flex; flex-wrap: wrap; gap: var(--up-space-sm); align-items: center; max-width: none; margin: var(--up-space-md) 0 var(--up-space-lg); }
.mytasks-add input[type="text"] { flex: 1 1 200px; }
.mytasks-add select, .mytasks-add input[type="date"] { flex: 0 1 auto; }

/* Project tiles grew a star (favoriting without visiting the project
   page) — the tile div keeps the border/hover treatment from Phase 15,
   the inner link carries the old flex layout. */
.project-tile-link {
  display: flex;
  align-items: center;
  gap: var(--up-space-sm);
  flex: 1;
  min-width: 0;
  text-decoration: none;
  color: inherit;
}
.tile-star {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  color: var(--up-muted);
  padding: 4px 6px;
  border-radius: var(--up-radius-sm);
  flex: none;
  transition: color 0.15s ease, transform 0.1s ease;
}
.tile-star:hover { color: var(--up-copper); transform: scale(1.2); }
.tile-star-on { color: var(--up-copper); }

/* My Week: seven slim day cells under the greeting, agenda items as
   truncated chips. Sits on the dark ground (not a card), so it reads
   through the ground tokens. Scrolls sideways once it can't fit. */
.week-strip {
  display: grid;
  grid-template-columns: repeat(7, minmax(110px, 1fr));
  gap: var(--up-space-xs);
  margin: 0 0 var(--up-space-lg);
  overflow-x: auto;
  padding-bottom: 4px;
}
.week-day {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: var(--up-space-xs) var(--up-space-sm);
  border: 1px solid var(--up-line);
  border-radius: var(--up-radius-sm);
  text-decoration: none;
  color: inherit;
  min-width: 0;
  transition: border-color 0.15s ease, background-color 0.15s ease;
}
.week-day:hover { border-color: var(--up-copper); }
.week-day-today {
  border-color: var(--up-copper);
  background: color-mix(in srgb, var(--up-copper) 10%, transparent);
}
.week-day-head {
  font-size: 11px;
  font-weight: var(--up-weight-semibold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--up-muted);
}
.week-day-num { color: var(--up-fg); }
.week-chip {
  font-size: 11px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  background: var(--up-surface);
  border-radius: 4px;
  padding: 2px 6px;
}
.week-chip-call { box-shadow: inset 2px 0 0 var(--up-copper); }

/* Phase 22 — BYOK AI summaries */
.ai-block { margin: 12px 0; }
/* No copper rule down the left edge. A thick coloured border on one side of a
   card is the single most recognisable tell of a generated interface, and this
   one was decorative — the "✦ Break it down" control directly above already
   says what this block is, so the stripe was carrying no information. It also
   sat oddly on the one box in the app whose contents really are AI-written. */
.ai-summary-out {
  margin-top: 10px;
  padding: 12px 14px;
  border: 1px solid var(--up-neutral-400);
  border-radius: var(--up-radius-sm);
  background: var(--up-surface);
}
.ai-summary-out > :first-child { margin-top: 0; }
.ai-summary-out > :last-child { margin-bottom: 0; }
.ai-summary-loading { color: var(--up-neutral-500, inherit); font-style: italic; }
/* The error state used to recolour the left rule only. With that rule gone it
   takes the whole border instead, which is a stronger signal than the stripe
   was and doesn't need a decorative edge to exist in order to have something
   to tint. */
.ai-summary-error { border-color: #b23; color: #b23; }
.ai-help {
  font-size: 14px;
  padding: 10px 14px;
  border: 1px solid var(--up-neutral-400);
  border-radius: var(--up-radius-sm);
  background: var(--up-surface);
}
.ai-help ol { margin: 6px 0 0; padding-left: 20px; }
.ai-help li { margin: 3px 0; }
.ai-help code { font-size: 13px; }

/* Google sign-in button + "or" divider on the auth pages */
.google-btn { display: block; width: 100%; text-align: center; text-decoration: none; }
.auth-or { text-align: center; margin: 10px 0; }

/* ============================================================================
   V3 "Now" front door (routes/now.js + public/js/now.js). The whole app as
   three surfaces — Now deck / Later / Done — in the cinematic language. Cards,
   sheet, capture and toast stay a fixed dark "lit screen" in BOTH themes (same
   principle as the home hero-frame); on-stage text (greeting, tabs) rides the
   theme tokens so it reads on the cream light-mode stage too.
   ============================================================================ */
/* --- the deck's palette --------------------------------------------------
   Two sets, one seam. The DARK set below is declared on the stage AND
   re-declared on every overlay that stays a dark screen in both themes (the
   schedule sheet, the toast, the capture/new-client overlays, the deck-clear
   title card) — those float over their own scrim, so they don't re-theme.
   The light override further down then only has to speak for the stage.

   Because the overlays declare the dark set ON THEMSELVES, they beat the
   light values they'd otherwise inherit from the stage — no !important, and
   no per-overlay light-theme exceptions to keep in sync.

   `--n-fg` is "text on a panel that sits on the stage" (card, Do rows, action
   buttons) — it flips. `--n-paper` stays paper in both themes because it's
   text on a DARK fill (the sheet, the capture overlay). Mixing those two jobs
   into one token is what made light mode read as mud.

   `--n-strong-fg` is the third job, added 2026-08-09: text on a COPPER fill.
   This comment used to hand that job to `--n-paper` too, and it was wrong in
   the dark — `--n-copper` LIFTS to #D8853A at night, so paper on it measured
   2.56:1 (it passes at 5.75:1 by day, which is why nobody caught it). Copper
   moves in the opposite direction to the ground it sits on, so its foreground
   has to flip: ink at night, day-white by day. This is the deck's private
   spelling of the app-level Copper Rule (--up-strong-fg, see "Copper, in the
   two jobs it can't do with one value" above). A copper fill never takes a
   fixed foreground. */
.now-surface,
.now-surface .now-sheet,
.now-surface .now-toast,
.now-surface .now-capture,
.now-surface .now-clear {
  --n-paper: #F4F2EE; --n-cream: #EDEAE4; --n-copper: #D8853A; --n-copper-br: #E9A45E;
  --n-danger: #FF7F6D; --n-amber: #F3CB63; --n-neutral: #8F887C; --n-cool: #8FA8C4;
  --n-muted: #C2BAAE;
  --n-ok: #6FE0A0;   /* the tally's WRAPPED state — same green as --up-ok */
  --n-fg: var(--n-paper);
  --n-strong-fg: #150E04;   /* ink, on the lifted night copper — 6.69:1 */
  /* The card is a pool of light, not a pane of glass: it lets the cyc through
     and its edge is a rim rather than a border. */
  --n-glass: rgba(255, 252, 246, 0.085);
  --n-edge: var(--cyc-rim);
  --n-line: rgba(255, 252, 246, 0.07);
  --n-hi: rgba(255, 252, 246, 0.26);
  --n-shadow: var(--cyc-spill);
  --n-stamp-bg: rgba(20, 23, 28, 0.60);
  --n-field: rgba(255, 252, 246, 0.075);
  /* The lift under the deck card — the app's one elevated object. Neutral,
     never tinted: a coloured glow reads as a light source and there isn't one. */
  --n-lift: rgba(0, 0, 0, 0.55);
}
.now-surface {
  overscroll-behavior: none;
  position: relative;
  /* One variable for the deck column so the tab bar, sheet and toast — which
     are fixed and centred separately — can't drift from the cards when the
     desktop breakpoints widen it. */
  --deck-w: 620px;
  max-width: var(--deck-w);
  margin: 0 auto;
  min-height: calc(100dvh - 220px);
  display: flex;
  flex-direction: column;
  --tabbar-space: 92px;            /* the fixed tab bar's footprint */
  padding-bottom: var(--tabbar-space);
  color: var(--up-fg);
}
/* Light theme: the stage is cream, so the panels on it are near-white frost
   with ink text — a real light theme, not a dark card marooned on paper. The
   previous version only thickened the dark glass to 46%, which composited to
   flat grey and left the meta text (near-white on cream) invisible.

   Copper splits by job here: `--n-copper` keeps the brand value because it's
   FILLS (the FAB, the progress bar), while
   `--n-copper-br` — the emphasis variant, lifted on dark so it clears Ink —
   deepens on light so an 11px uppercase kicker still clears 4.5:1 on paper. */
body[data-theme$="light"] .now-surface {
  --n-copper: #8F4E15; --n-copper-br: #7A3F10;
  --n-strong-fg: #FFFDF9;   /* day-white, on the deepened day copper — 6.32:1 */
  --n-danger: #A8301F; --n-amber: #6B4600; --n-neutral: #4A4139; --n-cool: #3A5670;
  --n-ok: #186B41;
  /* 46% measured 2.91:1 against the light stage — under the 4.5:1 floor, and
     since this token carries every small muted string on the surface it took
     .now-amnesty (45a), .now-load (45c) and 45g's filter line down with it.
     68% clears AA at 12px while staying obviously quieter than --n-fg
     (#1B1A18), which is the only job the token has. */
  --n-muted: #665D53;
  --n-fg: #171310;
  --n-paper: #FFFDF9;
  --n-glass: rgba(255, 253, 249, 0.94);
  --n-edge: var(--cyc-rim);
  --n-line: rgba(23, 19, 16, 0.065);
  --n-hi: #FFFDF9;
  --n-shadow: var(--cyc-spill);
  --n-stamp-bg: rgba(255, 253, 249, 0.72);
  --n-field: rgba(23, 19, 16, 0.055);
  --n-lift: rgba(23, 19, 16, 0.30);
}
.now-surface .now-plain { list-style: none; padding: 0; }
.now-surface .now-plain li { padding: 10px 0; border-bottom: 1px solid var(--up-line); }

/* header */
.now-head { padding-top: 4px; }
.now-slug {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--up-muted); font-variant-numeric: tabular-nums;
}
.now-rec { color: var(--n-copper-br); }
.now-rec::before {
  content: ""; display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  background: var(--n-copper-br); margin-right: 7px; vertical-align: 1px;
  animation: now-blink 2.4s ease-in-out infinite;
}
@keyframes now-blink { 0%,100% { opacity: 1; } 50% { opacity: 0.25; } }
.now-slug-right { display: inline-flex; align-items: center; gap: 12px; }
.now-brand { display: inline-flex; align-items: center; gap: 9px; }
.now-live { width: 8px; height: 8px; border-radius: 50%; background: var(--n-copper-br); flex: none; animation: now-blink 2.4s ease-in-out infinite; }
/* NOW wordmark, top-left of the surface — the product's name since
   2026-08-10 (was the UROS slug). One quiet identity cue in copper, not a
   second piece of visual scenery. */
.now-wordmark { font-family: var(--up-font-body); font-weight: 600; font-size: 13px; letter-spacing: 0.22em; line-height: 1; text-transform: uppercase; color: var(--n-copper-br); }
/* Forced colours (Windows High Contrast) drop background-image, and this mark
   is a gradient painted THROUGH transparent glyphs — so without this the
   wordmark does not lose its colour, it disappears entirely. Hand the glyphs
   back a real foreground and drop the clip. */
@media (forced-colors: active) {
  .now-wordmark { background: none; color: CanvasText; -webkit-text-fill-color: CanvasText; }
}

/* Escape hatch to the rest of the app — mobile only (desktop keeps the full
   sidebar). Carries id="bottom-menu" so nav.js wires it to the off-canvas
   drawer with zero extra JS. */
.now-menu { display: none; background: none; border: none; color: var(--up-muted); font-size: 20px; line-height: 1; padding: 2px 4px; cursor: pointer; }
@media (max-width: 700px) { .now-menu { display: inline-flex; } }
.now-greeting { margin-top: 12px; }
.now-greeting h1 {
  font-family: var(--up-font-display); font-weight: 300;
  font-size: clamp(30px, 4.1vw, 52px); letter-spacing: -0.035em;
  line-height: 1.02; margin: 0; text-wrap: balance;
}
.now-greeting p { margin: 7px 0 0; font-size: 17px; font-weight: 300; color: var(--up-muted); }
.now-greeting strong { color: var(--up-fg); font-weight: 500; }
.now-progress {
  margin-top: 12px; height: 2px; border-radius: 2px; overflow: hidden;
  background: color-mix(in srgb, var(--up-fg) 12%, transparent);
}
/* impeccable-disable-next-line layout-transition: the fill is a GRADIENT — scaleX would stretch the ramp as the bar grew, which costs more than the relayout on a 2px bar */
.now-progress i { display: block; height: 100%; width: 0; background: var(--n-copper); transition: width 0.4s ease; }

/* panels */
.now-panel { display: none; flex: 1; flex-direction: column; min-height: 0; }
.now-panel.now-active { display: flex; }

/* deck */
.now-deck { flex: 1; position: relative; margin: 30px 0 6px; min-height: 430px; touch-action: none; }
.now-card {
  position: absolute; inset: 0; margin: auto; max-height: 430px;
  border-radius: var(--up-radius-lg); padding: 40px 42px 36px;
  /* The deck is part of the product, not a separate film frame: one solid
     surface, one honest edge, and the same restrained depth as every other
     meaningful panel. */
  background: var(--n-glass);
  border: 1px solid var(--n-edge);
  /* THE ONE LIFTED OBJECT (soft pass, 2026-08-10). Every other surface in the
     app is now printed on the paper — a fill a few values up, a whisper of a
     hairline, one contact shadow. The deck card is the exception, because it
     is the subject: it is the thing you are being handed, and on a ground this
     light a 7-value fill and a 7.5% rim are not enough to make 600x400 read as
     an object rather than as a slightly different patch of page.
     This is NOT a shadow ladder — it is the top of a two-step system that has
     exactly one occupant. Wide, low, and cheap: 34px of blur at 11% does not
     draw an outline under the card, it just stops the card lying flat. */
  box-shadow: var(--n-shadow), 0 12px 34px -16px var(--n-lift);
  display: flex; flex-direction: column; color: var(--n-fg);
  user-select: none; -webkit-user-select: none; cursor: grab; will-change: transform;
  touch-action: none; overscroll-behavior: contain;
  /* The content sits in the MIDDLE of the card rather than being pinned to its
     top and bottom with a hole between. Measured before this change: 179px of
     121px-of-content in a 305px card on a phone — 59% of the card was a void
     under the title, which reads as something that failed to load rather than
     as focus. The card keeps its size deliberately: its presence IS the "this
     one thing matters" signal, so the fix is to balance the emptiness around
     the content, not to remove it.

     `safe center` is the important half. Plain `center` on a flex column that
     overflows pushes the overflow out BOTH ends, and the top end is
     unreachable — a card with a four-line title would lose its own first line
     with no way to scroll to it. `safe` falls back to flex-start exactly when
     that would happen. The plain value is declared first for anything that
     doesn't understand the keyword. */
  justify-content: center;
  justify-content: safe center;
}
.now-card:active { cursor: grabbing; }
/* impeccable-disable-next-line bounce-easing: a released card settling back SHOULD overshoot slightly — this is a swipe deck, and dead-flat reads as broken */
.now-card.settling { transition: transform 0.35s cubic-bezier(0.2,0.9,0.3,1.15); }
.now-card.leaving { transition: transform 0.42s cubic-bezier(0.5,0,0.9,0.6), opacity 0.42s ease; }
.now-card.stacked { transition: transform 0.35s ease, opacity 0.35s ease; pointer-events: none; }
/* A stacked card is a SLIVER OF EDGE saying "there's more behind this one" —
   it is not a second card you can read. Its contents were rendering through
   below the top card, which put a second "Start" button on screen under the
   real one: two identical controls, one of them dead. visibility (not display)
   so the card keeps its height and the stack keeps its shape. */
.now-card.stacked > * { visibility: hidden; }
.now-proj { font-size: 12.5px; letter-spacing: 0; color: var(--n-muted); font-weight: 600; display: flex; align-items: center; gap: 8px; }
.now-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
/* Which company the card belongs to, for someone who's in more than one. Sits
   after the project in the kicker, separated by a hairline rather than a
   middot so it reads as a second field and not part of the project's name. */
.now-org {
  color: var(--n-muted); font-weight: 500; letter-spacing: 0.1em;
  padding-left: 9px; margin-left: 1px;
  border-left: 1px solid color-mix(in srgb, var(--n-fg) 22%, transparent);
}
.now-card h2 { font-family: var(--up-font-display); font-weight: 300; font-size: clamp(26px, 3.2vw, 40px); letter-spacing: -0.03em; line-height: 1.1; margin: 16px 0 0; text-wrap: balance; }
/* Workflow context — the middle of the card. Hidden until the deck is on a
   wide screen (the desktop media block below turns it on): a phone deck is
   deliberately one thing and nothing else, but in a browser the card was a
   300px-tall box holding a title and air, and this is the thing worth putting
   in it — what this is part of, how far along, and what comes after. */
.now-ctx { display: none; margin-top: 20px; }
.now-ctx-track { height: 3px; border-radius: 2px; overflow: hidden; background: color-mix(in srgb, var(--n-fg) 13%, transparent); }
.now-ctx-track i { display: block; height: 100%; border-radius: 2px; background: var(--n-copper); }
.now-ctx-line { display: flex; align-items: baseline; gap: 10px; margin-top: 9px; font-size: 11.5px; }
.now-ctx-stage { letter-spacing: 0; font-weight: 600; color: var(--n-copper-br); white-space: nowrap; }
.now-ctx-next { flex: 1; min-width: 0; color: var(--n-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.now-ctx-line b { font-weight: 600; color: var(--n-muted); font-variant-numeric: tabular-nums; }

/* Teaching cards (migration 083) — the intro a new person is dealt. They look
   like a card because they ARE one: the lesson is the gesture, so anything
   that made them read as an overlay or a modal would teach the wrong thing.
   The kicker drops its dot and goes quiet, and the instruction gets the same
   display type a real title would. */
.now-proj-teach { color: var(--n-muted); letter-spacing: 0; }
.now-teach-hint {
  margin: 12px 0 0; font-size: 14px; line-height: 1.5;
  color: var(--n-muted); max-width: 34ch;
}
.now-cardlink.js-skip-tour {
  background: none; border: none; padding: 0; cursor: pointer;
  font-family: var(--up-font-body); font-size: 12px; color: var(--n-muted);
  text-decoration: underline; text-underline-offset: 2px;
}
.now-cardlink.js-skip-tour:hover { color: var(--n-fg); }

/* `margin-top: auto` was the mechanism that split the card in two — it shoved
   this row to the bottom edge and left the gap above it. With the card now
   centring its content, the meta row travels with the rest as one block and
   only needs its own breathing room above. */
.now-meta { padding-top: 18px; display: flex; gap: 8px; flex-wrap: wrap; }
.now-chip { font-size: 12px; font-weight: 600; padding: 5px 11px; border-radius: 999px; border: 1px solid var(--n-line); color: var(--n-muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
/* Late is AMBER, not red. Red is for danger and for things you cannot undo;
   a task you didn't get to on Tuesday is neither. It reads a step firmer than
   .soon so the two are still distinguishable, and that is the whole escalation
   — there is no third, louder step no matter how old the task gets. */
.now-chip.overdue { color: var(--n-amber); border-color: color-mix(in srgb, var(--n-amber) 62%, transparent); background: color-mix(in srgb, var(--n-amber) 16%, transparent); }
.now-chip.soon { color: var(--n-amber); border-color: color-mix(in srgb, var(--n-amber) 45%, transparent); background: color-mix(in srgb, var(--n-amber) 10%, transparent); }
/* The clock on the card. Quiet at rest — a Start you can ignore — and only
   becomes the loud thing on the card once it's actually running, because
   that's the one moment the elapsed number is what you want to see. */
.now-time { display: flex; align-items: baseline; gap: 10px; margin-top: 12px; }
.now-time-go {
  flex: none; font-family: var(--up-font-body); font-size: 12.5px; font-weight: 500;
  color: var(--n-muted); background: none; border: 1px solid var(--n-line);
  border-radius: 999px; padding: 5px 13px; cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.now-time-go:hover { color: var(--n-fg); border-color: var(--n-copper-br); }
/* "Hold me to it" — the committing way to start (migration 094). Deliberately
   quieter than Start: plain Start is the ordinary act and should stay the easy
   reach, while agreeing to a block you cannot leave is the considered one. Same
   underlined-text language the amnesty and the fuel filters use for "findable
   when you want it, not the thing you hit by reflex". */
.now-time-hold {
  flex: none; background: none; border: none; padding: 0; cursor: pointer;
  font-family: var(--up-font-body); font-size: 12px;
  color: var(--n-muted); text-decoration: underline; text-underline-offset: 3px;
}
.now-time-hold:hover { color: var(--n-copper-br); }
.now-time.is-running .now-time-go {
  color: var(--n-strong-fg); background: var(--n-copper); border-color: var(--n-copper);
}
.now-time-clock {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 15px; font-variant-numeric: tabular-nums;
  color: var(--n-copper-br); font-weight: 600;
}
.now-time-est { font-size: 12.5px; color: var(--n-muted); }
/* A committed run. The clock is the loudest thing on the card while it's
   counting down, because for these minutes it IS the card — and the way out
   sits right next to it, never hidden behind a confirm. */
.now-time.is-locked .now-time-go {
  color: var(--n-strong-fg); background: var(--n-copper); border-color: var(--n-copper);
}
.now-time.is-locked .now-time-clock { font-size: 19px; }
.now-time-lock { font-size: 12px; color: var(--n-muted); }
@media (prefers-reduced-motion: no-preference) {
  .now-time.is-locked .now-time-clock { animation: now-tick 2s ease-in-out infinite; }
}
@keyframes now-tick { 0%, 100% { opacity: 1; } 50% { opacity: 0.72; } }
.now-stamp { position: absolute; top: 26px; font-size: 15px; font-weight: 700; letter-spacing: 0.22em; text-transform: uppercase; padding: 7px 14px; border: 2.5px solid; border-radius: 8px; opacity: 0; pointer-events: none; background: var(--n-stamp-bg); }
.now-stamp.done { right: 20px; color: var(--n-copper-br); border-color: var(--n-copper-br); transform: rotate(9deg); }
.now-stamp.later { left: 20px; color: var(--n-neutral); border-color: var(--n-neutral); transform: rotate(-9deg); }
.now-stamp.sched { left: 50%; top: 40%; translate: -50% 0; color: var(--n-cool); border-color: var(--n-cool); transform: rotate(-4deg); }
.now-stamp.grow { left: 50%; top: auto; bottom: 26px; translate: -50% 0; color: var(--n-copper-br); border-color: var(--n-copper-br); transform: rotate(3deg); }
.now-opt-head { font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--n-muted); padding: 4px 2px 2px; }
/* Type-ahead over the client roster in the grow sheet. Element+class so the
   base `input:not([type=checkbox]):not([type=radio])` rule (0,2,1) doesn't
   win — same trap as the capture field. */
.now-opt-find { padding: 4px 0 8px; }
.now-surface .now-opt-find input.now-opt-find-x,
.now-surface .now-opt-find input {
  width: 100%; font-size: 15px; color: var(--n-paper);
  background: color-mix(in srgb, #F7F5F1 8%, transparent);
  border: 1px solid var(--n-edge); border-radius: 10px; padding: 10px 12px;
}
.now-surface .now-opt-find input::placeholder { color: var(--n-muted); }
.now-sheet .workflow-picker-list { max-height: 210px; }
.now-grow-matches { display: flex; flex-direction: column; gap: 4px; }
.now-sheet .workflow-picker-choice, .now-sheet #now-grow-matches button { width: 100%; display: flex; justify-content: space-between; align-items: baseline; padding: 10px 12px; border-radius: 10px; font: inherit; font-size: 14.5px; cursor: pointer; color: var(--n-cream); border-color: var(--n-line); background: var(--n-glass); }
.now-sheet .workflow-picker-choice:hover, .now-sheet .workflow-picker-choice:focus-visible,
.now-sheet #now-grow-matches button:hover, .now-sheet #now-grow-matches button:focus-visible { color: var(--n-paper); background: color-mix(in srgb, var(--n-copper) 12%, var(--n-glass)); border-color: var(--n-copper); }
.now-sheet .workflow-picker-choice small { color: var(--n-muted); }
.now-grow-destination {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 2px 2px 10px; font-family: var(--up-font-body); font-size: 13px; color: var(--n-muted);
}
.now-grow-destination select {
  max-width: 190px; padding: 7px 28px 7px 10px; font: inherit; font-weight: 600;
  color: var(--n-cream); background: var(--n-glass); border: 1px solid var(--n-edge); border-radius: 999px;
}
.now-grow-destination select:focus-visible { outline: 2px solid var(--n-copper-br); outline-offset: 2px; }
/* Secondary fields on the new-client screen — the name keeps the big display
   treatment, these sit quietly under it. */
.now-client-fields { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 16px; }
.now-surface .now-capture .now-client-fields input,
.now-surface .now-capture .now-client-fields input:hover,
.now-surface .now-capture .now-client-fields input:focus {
  flex: 1 1 180px; width: auto; font-family: var(--up-font-body); font-size: 15px;
  padding: 10px 12px; border: 1px solid var(--n-edge); border-radius: 10px;
  background: color-mix(in srgb, #F7F5F1 8%, transparent); color: var(--n-paper);
}
/* Capture stamp — sits in the meta row beside the date chip, deliberately
   the quietest thing on the card. It's the deck's queue order made visible:
   undated cards come up in the order they were added. */
.now-captured {
  align-self: center;
  font-size: 11.5px; letter-spacing: 0;
  /* 62%, not 46%: it's meant to be the quietest thing on the card, but at 46%
     over the light card it measured 2.9:1 — quiet had turned into unreadable.
     62% is 4.5:1 on both grounds and still recedes behind the date chip. */
  color: color-mix(in srgb, var(--n-fg) 62%, transparent);
  font-variant-numeric: tabular-nums;
}
/* The hint line and the undo occupy ONE slot, stacked in the same grid cell:
   they're never both wanted, and the space is reserved either way so nothing
   on the page moves when an undo appears. The toast used to be fixed and
   floated at bottom:140px, which measured directly on top of the four action
   buttons — it covered the controls you need for the very next card, at
   exactly the moment you're reaching for them. */
.now-underbar { display: grid; min-height: 46px; align-items: center; justify-items: center; }
.now-underbar > * { grid-area: 1 / 1; }
/* 65%, not 40%. This is the only line that teaches the deck's whole
   interaction, and at 40% it measured 2.51:1 on the day ground and 3.53:1 on
   the night one — the least legible text on the screen was the one explaining
   how to use the app. 65% clears AA in both (5.34 / 7.59) and is still
   obviously the quietest thing in the underbar. */
.now-verbs { text-align: center; font-size: 12.5px; letter-spacing: 0; color: color-mix(in srgb, var(--up-fg) 65%, transparent); padding-bottom: 8px; transition: opacity 0.6s ease; }
/* pointer-events, not just opacity. Everything in .now-underbar shares ONE
   grid cell, so a faded sibling still sits on top of whatever replaced it and
   swallows every click — opacity: 0 hides a thing from the eye and from
   nothing else. */
.now-verbs.gone { opacity: 0; pointer-events: none; }
/* The size dial and the verbs line occupy the same cell, and the dial only
   appears while a card is running. Driven off the dial's own [hidden] rather
   than a second class the JS has to remember to toggle: there is no state to
   keep in sync, so it cannot drift. Without this, "Quick" and "A sitting"
   measured ZERO reachable tap points at every width — the control shipped
   fully keyboard-accessible and completely dead to a mouse or thumb. */
.now-underbar:has(.now-sizeask:not([hidden])) .now-verbs { opacity: 0; pointer-events: none; }

/* Action buttons — click targets that mirror the swipe gestures, so the
   browser (mouse, no swipe) has an obvious way to act. Keyboard ← ↑ → still
   work in parallel. Circular, fixed-dark like the cards; the centre Schedule
   button sits a touch larger, echoing the concept mock. */
.now-actions { display: flex; justify-content: center; align-items: center; gap: 20px; padding: 4px 0 12px; }
/* Not four identical circles any more. They are lit pills sitting in the
   spill under the card, and exactly one of them — the one that commits —
   carries the white-day gradient. That is what reserving day is for. */
.now-act { width: auto; min-width: 74px; height: 56px; padding: 0 20px; border-radius: 999px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; color: var(--n-fg); background: var(--n-field); border: 0; box-shadow: inset 0 0 0 1px var(--n-line), 0 3px 8px -6px var(--n-lift); -webkit-backdrop-filter: none; backdrop-filter: none; cursor: pointer; transition: transform 0.22s var(--cyc-ease), box-shadow 0.22s var(--cyc-ease); }
.now-act svg { width: 22px; height: 22px; }
.now-act small { font-size: 12px; letter-spacing: 0; color: var(--n-muted); }
.now-act:hover, .now-act:focus-visible { transform: translateY(-2px); outline: none; }
.now-act:focus-visible { outline: 2px solid var(--n-copper); outline-offset: 3px; }
.now-act-big { min-width: 108px; height: 64px; }
/* Done carries the copper fill at rest.

   Before this, all four were the same glass circle and copper arrived only on
   :hover — which does not exist on a touch screen, so on a phone the action the
   entire deck exists to produce had no resting identity at all, and on desktop
   it was distinguished solely by being 12px wider. Four interchangeable glass
   circles is the most category-interchangeable thing on this surface.

   The fix is not a new treatment, because the app already owns this exact pair:
   `.btn` is a copper fill with paper text, `.btn-ghost` is a hairline outline
   that goes copper on hover. Buttons here are filled or they are ghosts; glass
   belongs to cards. The action row had invented a flat middle ground instead of
   speaking the system's own language, and this brings it back in line —
   the same brand, more sure of itself.

   Filled with --n-copper-br, NOT --n-copper, and that is a contrast decision
   rather than a taste one. Measured on the brand copper #B5651D: paper 3.69:1,
   ink 4.19:1, even pure white only 4.34:1 — nothing clears 4.5:1, because that
   copper sits in the dead zone where neither a light nor a dark foreground
   works at label sizes. (tokens.css rules ink out and never checks paper;
   paper is the worse of the two.) --n-copper-br is the emphasis variant the
   system already built for exactly this: lifted on dark (#D07A2C, ink at
   5.62:1) and deepened on light (#8F4E15, paper at 5.47:1). So the foreground
   flips with it — this is the one place in the app where the text on a copper
   fill is INK, and it is why the button is legible. */
.now-act-done {
  background: var(--up-strong-grad);
  color: var(--up-strong-fg);
  box-shadow: 0 4px 12px -7px var(--up-strong-lift), inset 0 1px 0 rgba(255,255,255,0.28);
}
body[data-theme$="light"] .now-surface .now-act-done { color: #FFFFFF; }
/* On a coloured surface the secondary text tints from the foreground, never
   grey — a muted grey label on copper is the one combination guaranteed to
   fail. `inherit` keeps it locked to whichever way the fill just flipped. */
.now-act-done small { color: inherit; }
.now-act-done:hover, .now-act-done:focus-visible { box-shadow: 0 8px 18px -10px var(--up-strong-lift), inset 0 1px 0 rgba(255,255,255,0.28); }

/* ...and the other three get out of its way. Ghost, like every other secondary
   control in the app: the hairline stays, the fill goes. One decisive move,
   then quiet around it — if all four had got louder the row would read flatter
   than it did before. */
.now-act-later, .now-act-sched, .now-act-grow {
  background: var(--n-field);
}
.now-act-later:hover { border-color: var(--n-neutral); }
.now-act-sched:hover { border-color: var(--n-cool); background: color-mix(in srgb, #2C3E52 45%, transparent); }
/* ↓ — grow a loose card, or open the workflow a card already belongs to. Sized
   like Remind (a secondary move), not like the primary → button. */
.now-act-grow:hover { border-color: var(--n-neutral); background: color-mix(in srgb, var(--n-neutral) 16%, transparent); }
/* Action buttons are a browser (mouse/keyboard) affordance — on phones the
   swipe IS the interaction, so hide them and switch the hint to the swipe copy. */
.now-verbs-cursor { display: none; }
@media (min-width: 701px) { .now-verbs-touch { display: none; } .now-verbs-cursor { display: inline; } }
@media (max-width: 700px) { .now-actions { display: none !important; } }

/* --- short viewports -----------------------------------------------------
   The deck is meant to be one screen: a card, the buttons, the hint. Nothing
   here should need the page to scroll. But the column's natural height is
   ~900px (topbar + greeting + a 340px deck + 92px of buttons + the hint + the
   92px the fixed tab bar occupies), so on any screen shorter than that the
   bottom of the column ran underneath the tab bar until you scrolled — which
   on a 720px laptop window meant the hint line was simply hidden.

   Fix the cause rather than the symptom: give the column less to fit. The deck
   keeps `flex: 1`, so on tall screens it still grows to fill everything going
   spare — these only lower the floor. Deliberately height-based, and NOT an
   internal scroller: making .content the scroller froze the installed PWA once
   before (see the mobile overhaul), so document scroll stays as it is. */
/* The deck reserves the tab bar's footprint itself (--tabbar-space), so the
   shell's own bottom padding is a second reservation for the same bar — 64px
   of nothing that put a scrollbar on a page built not to scroll. Scoped to the
   page that carries the deck; every other route keeps the shell padding. */
.content:has(> .now-surface) { padding-bottom: 0; }

@media (max-height: 900px) {
  /* Reach the bottom of the screen instead of stopping short. The stock
     `100dvh - 220px` left the column ~100px shy of the tab bar, so shrinking
     the deck just moved the overlap into dead space. Because the surface's
     own padding-bottom (92px) is larger than the bar (60px), ending the box
     13px above the viewport floor leaves a constant 45px of clearance at every
     height — and the deck, still `flex: 1`, absorbs whatever is going spare. */
  .now-surface { min-height: calc(100dvh - 130px); }
  .now-greeting { margin-top: 8px; }
  /* The bottom margin clears the stacked cards behind the top one: they're
     offset +22px each, so the deck box is 44px shorter than what's painted. */
  .now-deck { min-height: 240px; margin: 12px 0 46px; }
  .now-actions { gap: 16px; padding: 2px 0 8px; }
  .now-act { width: 58px; height: 58px; }
  .now-act svg { width: 20px; height: 20px; }
  .now-act-big { width: 68px; height: 68px; }
  .now-verbs { padding-bottom: 4px; }
}
@media (max-height: 780px) {
  .now-deck { min-height: 200px; margin: 10px 0 44px; }
  .now-act { width: 52px; height: 52px; }
  .now-act svg { width: 18px; height: 18px; }
  .now-act-big { width: 60px; height: 60px; }
  .now-act small { font-size: 12px; }
}
/* A laptop with a lot of browser chrome. The card is still the biggest thing
   on screen — the greeting gives up the room, not the deck. */
@media (max-height: 680px) {
  .now-greeting h1 { font-size: clamp(21px, 5vw, 25px); }
  .now-greeting p { font-size: 13px; }
  .now-deck { min-height: 170px; margin: 8px 0 46px; }
  .now-verbs { font-size: 10.5px; }
}

/* Thumb reach — from the /impeccable critique of /now, which measured the card
   sitting 451px up an 812px screen with its centre in the TOP half, while the
   reachable bottom half sat empty. This is a one-handed swipe surface; the most
   repeated action in the whole product was positioned where a thumb has to
   change grip to reach it.

   This deliberately REVERSES the intent of the max-height rules above for phone
   WIDTHS only. Those let the deck keep `flex: 1` so it "absorbs whatever is
   going spare" — which sounds right and is wrong here, because a deck that
   absorbs the slack keeps the card centred in the column, and centred on a tall
   phone means above the thumb. Stopping the absorption and hanging the column
   from its bottom edge sends that same slack ABOVE the deck, where nothing
   needs to be tapped.

   Width-scoped, not height-scoped, because this is about thumbs rather than
   short windows: a laptop with a lot of browser chrome is short but is being
   driven by a mouse, and should keep the centred behaviour.
   Measured effect at 375x812: card in the thumb zone 107px -> 187px. */
@media (max-width: 700px) {
  .now-surface { min-height: calc(100dvh - 40px); }
  #now-panel-now { justify-content: flex-end; padding-bottom: 10px; }
  .now-deck { flex: 0 0 auto; }
  /* An actionable deck belongs in thumb reach; the clear state has no gesture
     to reach for. Keep that summary with the day status instead of marooning
     it above the fixed navigation on tall phones. */
  #now-panel-now:has(.now-clear.show) { justify-content: flex-start; }
  #now-panel-now:has(.now-clear.show) .now-deck { margin-top: 28px; }
}

/* Tall phones only, so the height ladder above still owns small screens — an
   iPhone SE correctly stays on its 170px deck rather than being handed a card
   that doesn't fit. Restores the deck's own base min-height (340px) rather than
   inventing a number: once the deck stops growing, the ladder's 240px floor is
   the only thing setting the card's size, and 240 is smaller than this surface
   was ever meant to be on a screen with room to spare. */
@media (max-width: 700px) and (min-height: 780px) {
  .now-deck { min-height: 340px; }
}


/* Desktop two-pane: on a wide screen the Do list rides as a persistent
   quiet rail to the RIGHT of the deck — Tinder web's queue-beside-the-stage
   move. renderLater() refreshes it every action regardless of the active
   tab, so the rail is always current; its own Later tab hides up here.

   Alignment is the fiddly part. The deck sits in normal flow inside the
   CONTENT column, while the tab bar, sheet, toast and FAB are position:fixed
   and centred on the VIEWPORT. Two different centres, so everything fixed
   needs one shared correction — and the correction has to be derived from
   how .content actually behaves, not from a guess:

     .content is flush against the sidebar with a 1200px cap (see its rule),
     so its centre is  sidebar + min(viewport − sidebar, 1200px) / 2
     — NOT the midpoint between the sidebar and the window edge. Those two
     happen to coincide at exactly 1440px with the sidebar open, which is
     what made a naive "half the sidebar" version look correct while being
     90px out the moment the rail collapsed.

   The deck then steps left by half the rail+gap so the pair reads as one
   composition, and its chrome follows by the same amount. Every term is a
   variable, so the deck and its chrome cannot drift apart.

   `left: 50vw` rather than `left: 50%`: percentages on a fixed element
   resolve against the viewport EXCLUDING the scrollbar while vw INCLUDES
   it, and mixing the two units puts the bar half a scrollbar off-centre on
   any platform without overlay scrollbars. */
.now-surface { --rail-w: 0px; --rail-gap: 0px; }
.app-shell { --sidebar-w: 0px; --content-max: 1200px; }
@media (min-width: 701px) {
  .app-shell { --sidebar-w: 240px; }
  .app-shell.sidebar-collapsed { --sidebar-w: var(--sidebar-rail-width, 60px); }
}
/* The toast used to be in this list. It isn't fixed any more — it lives in the
   deck column's underbar — so it gets the column's alignment for nothing. */
.now-tabs, .now-sheet {
  left: 50vw;
  margin-left: calc(
    var(--sidebar-w)
    + min(100vw - var(--sidebar-w), var(--content-max)) / 2
    - 50vw
    - (var(--rail-w) + var(--rail-gap)) / 2
  );
}

@media (min-width: 1280px) {
  /* The deck page opts out of the 1200px content cap. Everywhere else that cap
     is right (prose and tables shouldn't run edge to edge), but the deck + rail
     are one centred COMPOSITION — under the cap they hugged the sidebar and
     left a few hundred pixels of bare stage to the right of the rail on any
     wide monitor, which is what made the page read as unfinished rather than
     composed. --content-max follows so the fixed chrome (tab bar, sheet,
     toast) keeps computing the same centre the cards use. */
  .content:has(> .now-surface) { max-width: none; }
  .app-shell:has(.now-surface) { --content-max: 100vw; }
  /* Context and meta group into one base plate at the foot of the card, so the
     card reads as a framed shot — title up top, a horizon line, the facts along
     the bottom — instead of a tall box with a caption stranded in it. */
  .now-ctx { display: block; margin-top: auto; }
  .now-card:has(.now-ctx) .now-meta { margin-top: 14px; }
  /* The card is the subject standing in the light, so it is given room: 600
     wide by 400 tall (3:2, a frame rather than a tile). The old 540×330 was a
     card on a page; this is a thing on a stage. */
  .now-card { max-height: 400px; }
  .now-surface {
    --deck-w: 600px;
    --rail-w: 300px;
    --rail-gap: 40px;
    /* `right`, not `transform` — a transform would make the surface the
       containing block for its own fixed chrome and the tab bar would
       scroll away with the page. */
    position: relative;
    right: calc((var(--rail-w) + var(--rail-gap)) / 2);
  }
  /* The rail runs from the top of the deck to the bottom of the page. Its
     height is derived (top + bottom, no max-height) so it grows and shrinks
     with the window instead of being capped. The bottom reclaims the tab
     bar's reserved strip — the bar is centred on the deck column and the rail
     is off to its right, so they never actually collide — leaving a small
     breathing gap at the page edge.

     Clamped at 0 by the max(): `24px - 92px` is NEGATIVE, which hung the rail
     68px below the surface box and made the document taller than the window
     even when the rail read "Nothing waiting" — a permanent scrollbar on a
     page built not to scroll, and the thing that dragged the deck back under
     the tab bar as soon as you touched the wheel. A long Do list now scrolls
     inside the rail instead of taking the whole page with it. */
  #now-panel-later {
    display: flex;
    position: absolute;
    left: calc(100% + var(--rail-gap));
    width: var(--rail-w);
    top: 130px;
    bottom: max(0px, calc(24px - var(--tabbar-space)));
    max-height: none;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  #now-panel-later::before {
    content: "Do";
    position: absolute; top: -26px; left: 2px;
    font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
    color: var(--up-muted);
  }
  .now-tab[data-tab="later"] { display: none; }
}

/* Font scale and the two-pane deck. body[data-font-scale] works by CSS `zoom`,
   but a media query measures the VIEWPORT, not the zoomed layout — so at
   xlarge (1.3) a 1280px window turns the rail on while the layout only has
   1280/1.3 = 985px to put it in, and the rail lands off the right edge of the
   page. Each scale needs the breakpoint multiplied by its own zoom before the
   two-pane composition is affordable; below that the deck goes back to being
   one column with the Do list behind its tab — which means the tab has to
   come back too, or the list is unreachable. Keep these thresholds in step
   with the zoom values declared at the top of this file (0.9/1/1.15/1.3). */
@media (max-width: 1151px) {
  body[data-font-scale="small"] #now-panel-later { position: static; width: auto; top: auto; bottom: auto; overflow-y: visible; }
  body[data-font-scale="small"] #now-panel-later:not(.now-active) { display: none; }
  body[data-font-scale="small"] #now-panel-later::before { content: none; }
  body[data-font-scale="small"] .now-surface { right: 0; }
  body[data-font-scale="small"] .now-tab[data-tab="later"] { display: inline-flex; }
}
@media (max-width: 1471px) {
  body[data-font-scale="large"] #now-panel-later { position: static; width: auto; top: auto; bottom: auto; overflow-y: visible; }
  body[data-font-scale="large"] #now-panel-later:not(.now-active) { display: none; }
  body[data-font-scale="large"] #now-panel-later::before { content: none; }
  body[data-font-scale="large"] .now-surface { right: 0; }
  body[data-font-scale="large"] .now-tab[data-tab="later"] { display: inline-flex; }
}
@media (max-width: 1663px) {
  body[data-font-scale="xlarge"] #now-panel-later { position: static; width: auto; top: auto; bottom: auto; overflow-y: visible; }
  body[data-font-scale="xlarge"] #now-panel-later:not(.now-active) { display: none; }
  body[data-font-scale="xlarge"] #now-panel-later::before { content: none; }
  body[data-font-scale="xlarge"] .now-surface { right: 0; }
  body[data-font-scale="xlarge"] .now-tab[data-tab="later"] { display: inline-flex; }
}

/* A big monitor gets a bigger composition, not bigger margins: the card grows
   into a wide frame (the deck reads as a viewer rather than a phone screen
   pasted onto a desktop) and the rail widens enough that a two-line task title
   stops wrapping to three. */
@media (min-width: 1600px) {
  .now-surface { --deck-w: 680px; --rail-w: 340px; --rail-gap: 48px; }
  .now-card { max-height: 440px; padding: 44px 46px 40px; }
  .now-card h2 { font-size: clamp(30px, 2.6vw, 44px); }
}

/* deck-clear card */
.now-clear {
  position: absolute; inset: 0; margin: auto; max-height: 380px; border-radius: 16px;
  border: 1px solid var(--n-edge); overflow: hidden; color: var(--n-paper);
  background:
    radial-gradient(140% 100% at 85% 0%, color-mix(in srgb, var(--n-copper) 22%, transparent), transparent 60%),
    radial-gradient(140% 100% at 10% 100%, color-mix(in srgb, #2C3E52 50%, transparent), transparent 60%),
    #100F0D;
  box-shadow: 0 18px 44px rgba(0,0,0,0.45);
  display: none; flex-direction: column; justify-content: center; align-items: center; text-align: center; padding: 30px 26px;
}
.now-clear.show { display: flex; animation: now-rise 0.5s cubic-bezier(0.2,0.9,0.3,1); }
@keyframes now-rise { from { opacity: 0; transform: translateY(18px) scale(0.97); } to { opacity: 1; transform: none; } }
.now-clear::before, .now-clear::after { content: ""; position: absolute; left: 22px; right: 22px; height: 1px; background: color-mix(in srgb, var(--n-copper) 55%, transparent); }
.now-clear::before { top: 18px; } .now-clear::after { bottom: 18px; }
.now-eyebrow { font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--n-copper-br); font-weight: 600; }
.now-clear h2 { font-family: var(--up-font-display); font-weight: 400; font-size: 29px; margin: 10px 0 0; }
.now-clear-tally { font-family: var(--up-font-display); font-size: 40px; color: var(--n-copper-br); margin-top: 14px; }
.now-clear p { margin: 14px 0 0; font-size: 13.5px; color: var(--n-muted); max-width: 28ch; line-height: 1.55; }
.now-clear button { margin-top: 20px; font-family: var(--up-font-body); font-size: 13.5px; font-weight: 600; letter-spacing: 0; color: var(--n-strong-fg); background: var(--n-copper); border: none; border-radius: 999px; padding: 11px 22px; cursor: pointer; }
/* :hover lifts to --n-copper-br, which on DARK is lighter (#E9A45E) and on
   LIGHT is darker (#7A3F10) — the same flip --n-strong-fg is built for, so
   the ink/day-white foreground stays correct through the hover. */
.now-clear button:hover { background: var(--n-copper-br); }

/* later + done lists */
.now-list { flex: 1; overflow-y: auto; margin: 14px 0 10px; min-height: 0; }
/* The rail's group headings are GAFFER TAPE — see "the quiet accents" block
   below for the rule that says why tape, and why only here. A strip of tape
   is how a studio labels a temporary state, and Today/Later/Someday are the
   app's three temporary states: everything under them is parked, not filed. */
.now-group {
  /* `table`, not `inline-block`: the tape must hug its word AND still take its
     own line above the rows it labels. */
  display: table; transform: rotate(1.1deg);
  background:
    repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.05) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.04) 0 1px, transparent 1px 2px),
    #57524A;
  color: #F2EFE9;
  padding: 5px 16px 5px 14px; border-radius: 2px;
  font: 700 10px/1.5 var(--up-font-mono); letter-spacing: 0.14em; text-transform: uppercase;
  box-shadow: 0 1px 2px rgba(23, 19, 16, 0.16);
  clip-path: polygon(0.8% 8%, 2.6% 0, 97.5% 3%, 100% 16%, 99.2% 94%, 96.5% 100%, 2% 97%, 0 80%);
  margin: 18px 2px 10px;
}
.now-group:first-child { margin-top: 4px; }
/* THE RAIL IS A LIST, NOT A STACK OF CARDS (soft pass, 2026-08-10). Every
   parked item used to be its own bordered, filled, 12px-radius plate with 8px
   of gutter under it, so eight things waiting looked like eight documents
   rather than one list of eight. That is the exact composition the Todoist
   reference doesn't have, and it is the loudest thing on the deck screen after
   the card itself. Rows now sit on the stage, separated by one hairline
   between neighbours, with the fill reserved for the row under the pointer —
   which is the only row that needs to be a surface. */
.now-row { border: 0; border-radius: 9px; background: transparent; margin-bottom: 0; overflow: hidden; color: var(--n-fg); transition: background 0.15s ease; }
.now-row + .now-row { border-top: 1px solid var(--n-line); }
.now-row:hover { background: color-mix(in srgb, var(--n-fg) 4.5%, transparent); }
/* An open row IS a surface — it has controls in it now, so it earns a plate. */
.now-row.open { background: var(--n-glass); box-shadow: inset 0 0 0 1px var(--n-edge); }
.now-row-head { display: flex; align-items: center; gap: 11px; padding: 14px 13px; cursor: pointer; }
.now-row-t { flex: 1; min-width: 0; }
.now-row-t b { display: block; font-weight: 500; font-size: 15px; line-height: 1.3; }
.now-row-t small { display: block; font-size: 11.5px; color: var(--n-muted); margin-top: 2px; }
.now-row-acts { display: none; gap: 8px; padding: 0 14px 13px; }
.now-row.open .now-row-acts { display: flex; }
.now-row-acts button { flex: 1; font-family: var(--up-font-body); font-size: 13px; font-weight: 500; letter-spacing: 0; color: var(--n-fg); background: var(--n-field); border: 1px solid var(--n-line); border-radius: 999px; padding: 9px 0; cursor: pointer; transition: border-color 0.15s ease, background 0.15s ease; }
.now-row-acts .a-now:hover { border-color: var(--n-copper-br); background: color-mix(in srgb, var(--n-copper) 18%, transparent); }
.now-row-acts .a-done:hover { border-color: var(--n-copper-br); }
.now-empty { text-align: center; color: var(--up-muted); font-size: 13.5px; margin-top: 40px; line-height: 1.6; }
.now-done-hero { text-align: center; margin: 22px 0 4px; }
.now-done-hero b { font-family: var(--up-font-display); font-weight: 400; font-size: 60px; color: var(--n-copper-br); line-height: 1; }
.now-done-hero span { display: block; font-size: 13px; letter-spacing: 0; color: var(--up-muted); margin-top: 8px; }
.now-done-list .now-row-head { cursor: default; }
.now-done-list .now-row-t b { text-decoration: line-through; text-decoration-color: color-mix(in srgb, var(--n-copper) 60%, transparent); color: var(--n-muted); font-weight: 400; }

/* ============================================================================
   THE QUIET ACCENTS (2026-08-10). Five information-carrying studio objects,
   chosen over the louder "desk" variant — comps and the full argument live in
   Claude CoWork/Uproad/now-tactile-comps/. The law they share: every accent
   REPORTS something true, in the data voice (--up-font-mono, the one
   sanctioned second family — DESIGN.md § Typography). None of them is
   decoration; the pin-and-tilt desk layer was rejected for exactly that.
   ============================================================================ */

/* 1 · The tally light. One pill in the header reporting true studio state, an
   instrument rather than a mascot: REC rides the running timer, ON DECK counts
   the deck, IN REVIEW counts deliverables sitting with clients, WRAPPED is a
   cleared deck with work behind it. Click flips through whichever states are
   live right now (public/js/now.js renderTally). It sits on the STAGE, so its
   colours are the --n-* stage tokens and re-theme with it. */
.now-tally {
  display: inline-flex; align-items: center; gap: 7px; padding: 5px 12px;
  border: 0; border-radius: 999px; cursor: pointer;
  font: 700 10px/1 var(--up-font-mono); letter-spacing: 0.12em;
  text-transform: uppercase; white-space: nowrap; font-variant-numeric: tabular-nums;
  background: color-mix(in srgb, var(--up-fg) 5%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--up-fg) 10%, transparent);
}
.now-tally i { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.now-tally.t-ondeck { color: var(--n-copper-br); }
.now-tally.t-ondeck i { background: var(--n-copper); box-shadow: 0 0 0 3px color-mix(in srgb, var(--n-copper) 16%, transparent); }
.now-tally.t-rec { color: var(--n-danger); }
.now-tally.t-rec i { background: var(--n-danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--n-danger) 20%, transparent); animation: now-blink 2.4s ease-in-out infinite; }
.now-tally.t-review { color: var(--n-amber); }
.now-tally.t-review i { background: var(--n-amber); box-shadow: 0 0 0 3px color-mix(in srgb, var(--n-amber) 20%, transparent); }
.now-tally.t-wrap { color: var(--n-ok); }
.now-tally.t-wrap i { background: var(--n-ok); box-shadow: 0 0 0 3px color-mix(in srgb, var(--n-ok) 16%, transparent); }
.now-tally.t-standby { color: var(--up-muted); }
.now-tally.t-standby i { background: var(--up-muted); }
@media (prefers-reduced-motion: reduce) { .now-tally i { animation: none; } }

/* 2 · The slate strip. The card's facts restated as one mono data line along
   its bottom edge — org · project · pressure · queue age. A clapper slate for
   the take you're on. It deliberately re-voices what the kicker and chips
   already say (same facts, instrument voice), so it is aria-hidden in the
   markup and absolute here: it lives inside the card's bottom padding and
   never moves the content. Keep the horizontal padding in step with
   .now-card's (42px base, 46px at 1600px+). */
.now-slate {
  position: absolute; left: 0; right: 0; bottom: 12px; padding: 0 42px;
  display: flex; align-items: center; overflow: hidden;
  font: 600 10px/1 var(--up-font-mono); letter-spacing: 0.10em;
  text-transform: uppercase; color: var(--n-muted);
  font-variant-numeric: tabular-nums; pointer-events: none;
}
.now-slate span { padding: 0 12px; border-left: 1px solid color-mix(in srgb, var(--n-fg) 14%, transparent); white-space: nowrap; }
.now-slate span:first-child { padding-left: 0; border-left: 0; font-weight: 700; }
@media (min-width: 1600px) { .now-slate { padding: 0 46px; } }

/* 3 · Gaffer tape (.now-group above, .stage-pill.stage-current here). Tape is
   how a studio labels a TEMPORARY state, so it appears on exactly two things:
   the rail's parked piles and the stage a project is currently in — both
   states the work is meant to leave. Tape is a physical object laid on the
   page: fixed ink on fixed tape in every theme, like the swipe stamps. */
.stage-pill.stage-current {
  display: inline-block; transform: rotate(-1.2deg);
  background:
    repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.05) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.04) 0 1px, transparent 1px 2px),
    #57524A;
  color: #F2EFE9;
  padding: 5px 16px 5px 14px; border-radius: 2px; border: 0;
  font: 700 10px/1.5 var(--up-font-mono); letter-spacing: 0.14em; text-transform: uppercase;
  box-shadow: 0 1px 2px rgba(23, 19, 16, 0.16);
  clip-path: polygon(0.8% 8%, 2.6% 0, 97.5% 3%, 100% 16%, 99.2% 94%, 96.5% 100%, 2% 97%, 0 80%);
}

/* 4 · The film-strip tag. A deliverable's review round and verdict as ONE
   leader tag — R2 · NEEDS REVIEW — because "which round" and "whose move is
   it" are one fact, not a chip plus a caption. Fixed dark like the tape; the
   verdict colour is the only thing that changes. */
.filmtag {
  display: inline-flex; align-items: stretch; gap: 9px;
  background: #3D3931; color: #F2EFE9; border-radius: 3px; padding: 5px 11px 5px 6px;
  font: 700 10px/1.4 var(--up-font-mono); letter-spacing: 0.10em;
  text-transform: uppercase; white-space: nowrap; font-variant-numeric: tabular-nums;
  box-shadow: 0 1px 2px rgba(23, 19, 16, 0.16);
}
.filmtag .perf {
  width: 5px; margin: -2px 0;
  background-image: radial-gradient(circle, rgba(239, 235, 227, 0.8) 1.05px, transparent 1.35px);
  background-size: 5px 6px; background-repeat: repeat-y;
}
.filmtag em { font-style: normal; align-self: center; }
.ft-approved em { color: #7FE3AC; }
.ft-review em { color: #F3CB63; }
.ft-changes em { color: #FF9C8D; }

/* 5 · The data voice on the rail: the Do list's chips are readouts, and they
   say so. (The gear/spec badges from the comps wait until the schema actually
   tracks gear-per-shoot and delivery specs — nothing here may report data the
   app doesn't hold.) */
.now-row .now-chip { font: 600 10px/1.1 var(--up-font-mono); letter-spacing: 0.08em; text-transform: uppercase; }

/* The quiet themes keep every accent (they are information) but drop the
   object shadows, per the pair's no-shadow law. */
body[data-theme^="adhd"] .now-group,
body[data-theme^="adhd"] .stage-pill.stage-current,
body[data-theme^="adhd"] .filmtag { box-shadow: none; }
body[data-theme^="adhd"] .now-tally i { animation: none; }
/* The quiet pair carries no shadow at all, and the deck card's lift is a
   shadow — it has to go with the rest of them or the "no depth that isn't
   there" promise stops being true on the one screen that matters most.
   Declared on .now-surface because that is where --n-lift is set, and a
   body-level override would lose on specificity. */
body[data-theme^="adhd"] .now-surface { --n-lift: transparent; }

/* tab bar (fixed within the surface's column) */
.now-tabs { position: fixed; left: 50%; transform: translateX(-50%); bottom: 0; z-index: 30; width: min(var(--deck-w, 480px), 100vw); display: flex; justify-content: center; gap: 6px; padding: 10px 12px max(14px, env(safe-area-inset-bottom)); }
.now-tab { font-family: var(--up-font-body); font-size: 13.5px; font-weight: 500; letter-spacing: 0; color: var(--up-muted); background: none; border: 1px solid transparent; border-radius: 999px; padding: 10px 18px; cursor: pointer; transition: color 0.15s, background 0.15s, border-color 0.15s; }
.now-tab-on { color: var(--up-fg); background: var(--cyc-pool); border-color: transparent; box-shadow: inset 0 0 0 1px var(--cyc-rim); -webkit-backdrop-filter: none; backdrop-filter: none; }
.now-fab { position: fixed; right: max(14px, calc(50vw - 240px + 14px)); bottom: calc(max(14px, env(safe-area-inset-bottom)) + 68px); z-index: 31; width: 56px; height: 56px; border-radius: 50%; font-size: 26px; line-height: 1; color: var(--up-strong-fg); background: var(--up-strong-grad); border: none; cursor: pointer; box-shadow: 0 6px 16px -8px var(--up-strong-lift); transition: box-shadow 0.2s, transform 0.2s; }
.now-fab:hover { transform: rotate(90deg); box-shadow: 0 10px 24px -10px var(--up-strong-lift); }
/* The FAB's containing block is the transformed .now-tabs box (a translateX'd
   fixed ancestor traps fixed descendants), and that box is the deck's width —
   so a tab-box-relative `right` pins the + to the deck's bottom-right corner
   and it inherits the deck's alignment for free. The old 50vw math treated it
   as viewport-anchored, which is why it drifted left of the cards. */
@media (min-width: 701px) { .now-fab { right: -20px; } }

/* schedule sheet */
.now-sheet { position: fixed; left: 50%; transform: translateX(-50%) translateY(120%); bottom: 0; z-index: 40; width: min(456px, calc(100vw - 24px)); visibility: hidden; background: color-mix(in srgb, #221F1C 90%, transparent); -webkit-backdrop-filter: none; backdrop-filter: none; border: 1px solid var(--n-edge); border-bottom: none; border-radius: 18px 18px 0 0; padding: 18px 18px max(18px, env(safe-area-inset-bottom)); box-shadow: 0 -14px 40px rgba(0,0,0,0.5); transition: transform 0.32s cubic-bezier(0.2,0.9,0.3,1), visibility 0s 0.32s; color: var(--n-paper); }
.now-sheet.open { transform: translateX(-50%) translateY(0); visibility: visible; transition-delay: 0s, 0s; }
/* A scheduling decision should follow the surrounding theme. Keeping this
   sheet as a black film frame in a light app made its calendar look like it
   was fading into a different product. Light mode gets one opaque paper
   surface; the night pair keeps the dark, low-light sheet above. */
body[data-theme$="light"] .now-surface .now-sheet {
  --n-paper: #171310; --n-cream: #2F2924; --n-muted: #625950;
  --n-copper: #8F4E15; --n-copper-br: #7A3F10;
  --n-glass: #FFFDF9; --n-edge: rgba(23, 19, 16, 0.13); --n-line: rgba(23, 19, 16, 0.11);
  background: #FBFAF7; box-shadow: 0 -12px 32px rgba(43, 34, 25, 0.16);
}
.now-sheet h3 { font-family: var(--up-font-title); font-weight: 600; font-size: 16px; letter-spacing: -0.01em; margin: 0 0 13px; }
.now-opts { display: flex; flex-direction: column; gap: 8px; }
/* Only the sheet's own action rows get this larger button treatment. The date
   picker lives inside .now-opt-date and owns its month arrows/day cells; a
   descendant selector here gave every day action-row padding and broke its
   seven-column grid. */
.now-opts > button { font-family: var(--up-font-body); font-size: 14.5px; font-weight: 500; color: var(--n-cream); text-align: left; padding: 12px 14px; border-radius: 10px; background: color-mix(in srgb, #F7F5F1 6%, transparent); border: 1px solid var(--n-line); cursor: pointer; display: flex; justify-content: space-between; align-items: baseline; transition: border-color 0.15s, background 0.15s; }
.now-opts > button:hover { border-color: var(--n-copper); background: color-mix(in srgb, var(--n-copper) 12%, transparent); }
.now-opts > button small { color: var(--n-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.now-opts > .now-cancel { justify-content: center; color: var(--n-muted); background: none; border-color: transparent; }
body[data-theme$="light"] .now-surface .now-sheet .now-opts > button { background: #FFFDF9; }

/* --- cadence sheet (→ on an anchor that books a run of sessions) --------- */
.now-cad-row { display: flex; gap: 10px; padding: 4px 0 8px; }
.now-cad-row label { flex: 1; display: flex; flex-direction: column; gap: 5px; font-size: 12px; letter-spacing: 0; color: var(--n-muted); }
.now-surface .now-cad-row input {
  width: 100%; padding: 11px 12px; font-size: 17px; font-variant-numeric: tabular-nums;
  color: var(--n-cream); background: var(--n-glass); border: 1px solid var(--n-edge);
  border-radius: 10px; text-align: center;
}
.now-surface .now-cad-row input:focus-visible { outline: 2px solid var(--n-copper); outline-offset: 1px; }
.now-cad-sum { padding: 2px 2px 10px; font-size: 13px; color: var(--n-muted); font-variant-numeric: tabular-nums; }
.now-opts .now-cad-go {
  justify-content: center; font-weight: 600;
  border-color: var(--n-copper-br); background: color-mix(in srgb, var(--n-copper) 20%, transparent);
}

/* --- workflow details (↓ on a card already in a workflow) ---------------- */
.now-wf-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; padding: 2px 2px 8px; }
.now-wf-head span { font-size: 12px; color: var(--n-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.now-wf-bar { height: 4px; border-radius: 2px; background: var(--n-edge); overflow: hidden; margin-bottom: 4px; }
/* Grows by transform, not width. Animating width relayouts on every frame;
   scaleX runs on the compositor. Safe here specifically because the fill is a
   FLAT colour — the deck's own progress bar (.now-progress i) is a gradient
   and scaling it would stretch the ramp as it grew, so that one deliberately
   still animates width. */
.now-wf-bar i {
  display: block; height: 100%; width: 100%;
  background: var(--n-copper);
  transform: scaleX(0); transform-origin: left center;
  transition: transform 0.3s ease;
}
.now-wf-days { display: flex; flex-wrap: wrap; gap: 6px; padding: 2px 0 8px; }
.now-wf-days span { font-size: 11.5px; padding: 3px 8px; border-radius: 999px; color: var(--n-cream); background: var(--n-glass); border: 1px solid var(--n-edge); white-space: nowrap; }
.now-wf-list { display: flex; flex-direction: column; gap: 2px; max-height: 40vh; overflow-y: auto; }
.now-wf-row { display: grid; grid-template-columns: 62px 1fr auto; gap: 10px; align-items: baseline; padding: 7px 2px; border-bottom: 1px solid var(--n-edge); }
.now-wf-row:last-child { border-bottom: none; }
.now-wf-state { font-size: 12px; letter-spacing: 0; color: var(--n-muted); }
.now-wf-row.deck .now-wf-state { color: var(--n-copper); }
.now-wf-row.overdue .now-wf-state { color: #D9744F; }
.now-wf-row.done .now-wf-t { text-decoration: line-through; color: var(--n-muted); }
.now-wf-t { font-size: 13.5px; color: var(--n-cream); }
.now-wf-d { font-size: 11.5px; color: var(--n-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.now-wf-open { display: block; padding: 10px 2px 2px; font-size: 13px; color: var(--n-copper); text-decoration: none; }
.now-wf-open:hover { text-decoration: underline; }
/* The deck sheet's copy of the type-first picker, on the sheet's dark ground. */
.now-opt-date { display: block; }
.now-opt-date .dp { display: flex; width: 100%; }
.now-opt-date .dp-field {
  width: 100%; min-width: 0; border-radius: 10px; padding: 0 6px 0 0;
  color: var(--n-cream); background: color-mix(in srgb, #F7F5F1 6%, transparent);
  border: 1px solid var(--n-line);
}
.now-opt-date .dp-field:hover { border-color: var(--n-copper); background: color-mix(in srgb, var(--n-copper) 12%, transparent); }
.now-opt-date .dp-field:focus-within, .now-opt-date .dp.dp-open .dp-field {
  border-color: var(--n-copper); box-shadow: 0 0 0 3px color-mix(in srgb, var(--n-copper) 25%, transparent);
}
.now-opt-date .dp-field input.dp-input,
.now-opt-date .dp-field input.dp-input:hover,
.now-opt-date .dp-field input.dp-input:focus {
  font-family: var(--up-font-body); font-size: 15px; font-weight: 500;
  color: var(--n-cream); padding: 12px 4px 12px 14px;
}
.now-opt-date .dp-field input.dp-input::placeholder { color: var(--n-muted); }
.now-opt-date .dp-x, .now-opt-date .dp-cal-btn { color: var(--n-muted); }
.now-opt-date .dp-cal-btn:hover, .now-opt-date .dp.dp-open .dp-cal-btn { color: var(--n-copper); background: color-mix(in srgb, var(--n-copper) 18%, transparent); }
.now-opt-date .dp-echo-ok { color: var(--n-copper); }
.now-opt-date .dp-echo-wait { color: var(--n-muted); }
/* A date inside the scheduling sheet is not a desktop popover. Keeping it
   inline lets the sheet grow upward from its fixed bottom edge, so the whole
   selection remains one contained decision rather than covering the deck and
   the Later list. The ordinary app surface token is translucent by design; a
   picker cannot be, or its dates look disabled over the dark sheet. */
.now-opt-date .dp-pop { position: static; box-sizing: border-box; width: 100%; margin-top: 8px; z-index: auto; background: #25231F; }
body[data-theme$="light"] .now-surface .now-opt-date .dp-pop { background: #FFFDF9; }

/* capture overlay */
/* Centred, and held to a readable measure. It used to be a full-bleed column:
   on a 1600px screen the label sat alone in the top-left of an enormous empty
   field and the input's underline ran the entire width of the monitor, so the
   one thing you were meant to look at was the least central thing on screen.
   align-items centres the column; the child width caps the measure. */
.now-capture { position: fixed; inset: 0; z-index: 1000; isolation: isolate; background: rgba(16, 15, 13, 0.96); -webkit-backdrop-filter: none; backdrop-filter: none; display: none; flex-direction: column; justify-content: center; align-items: center; padding: 0 26px; color: #FFFDF9; }
.now-capture.open { display: flex; }
/* The capture moment owns the whole viewport. Leaving the sticky topbar's
   bright light visible behind it made the modal feel leaky rather than focused. */
body:has(.now-capture.open) .main-col > .topbar { visibility: hidden; }
.now-capture > * { width: 100%; max-width: 620px; }
.now-capture label { font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--n-copper-br); font-weight: 600; }
.now-surface .now-capture input,
.now-surface .now-capture input:hover,
.now-surface .now-capture input:focus { margin-top: 14px; width: 100%; font-family: var(--up-font-display); font-size: clamp(24px, 6vw, 30px); color: #FFFDF9; background: none; border: none; border-bottom: 1px solid color-mix(in srgb, var(--n-copper-br) 78%, transparent); border-radius: 0; padding: 6px 2px 12px; outline: none; box-shadow: none; }
.now-capture input::placeholder { color: rgba(255, 253, 249, 0.72); }
.now-cap-destination { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
.now-cap-destination span { font-family: var(--up-font-body); font-size: 12px; letter-spacing: 0; text-transform: none; color: rgba(255, 253, 249, 0.72); }
.now-surface .now-capture .now-cap-destination select {
  width: auto; max-width: 240px; margin: 0; padding: 7px 30px 7px 10px;
  font-family: var(--up-font-body); font-size: 13px; font-weight: 600; color: #FFFDF9;
  background: rgba(255, 253, 249, 0.08); border: 1px solid rgba(255, 253, 249, 0.28); border-radius: 999px;
}
.now-surface .now-capture .now-cap-destination select:focus { border-color: var(--n-copper-br); }
.now-cap-hint { margin-top: 14px; font-size: 12.5px; color: rgba(255, 253, 249, 0.72); }
.now-cap-hint b { color: #FFFDF9; font-weight: 600; }

/* undo toast */
/* In the flow now, in the underbar's cell — so it needs no share of the fixed
   chrome's centring math, and can't drift from the deck it belongs to. */
.now-toast { display: flex; align-items: center; gap: 12px; max-width: 100%; background: color-mix(in srgb, #221F1C 92%, transparent); border: 1px solid var(--n-edge); border-radius: 999px; -webkit-backdrop-filter: none; backdrop-filter: none; padding: 8px 9px 8px 15px; font-size: 13px; color: var(--n-cream); opacity: 0; pointer-events: none; transition: opacity 0.25s ease, transform 0.25s ease; transform: translateY(4px); white-space: nowrap; overflow: hidden; }
.now-toast.show { opacity: 1; pointer-events: auto; transform: translateY(0); }
.now-toast span { overflow: hidden; text-overflow: ellipsis; }
.now-toast b { font-weight: 600; }
.now-toast button { flex: none; font-family: var(--up-font-body); font-size: 13px; font-weight: 600; letter-spacing: 0; color: var(--n-copper-br); background: color-mix(in srgb, var(--n-copper) 14%, transparent); border: 1px solid color-mix(in srgb, var(--n-copper) 40%, transparent); border-radius: 999px; padding: 6px 12px; cursor: pointer; }

/* Reduced motion on the deck: MOVEMENT goes, FEEDBACK stays.
   This was `* { animation-duration: 0.01ms !important; transition-duration:
   0.01ms !important }` — the blanket kill — and it was the wrong instrument on
   the one surface where motion is load-bearing. A vestibular trigger is
   movement: translation, scale, rotation, a looping blink. Opacity and colour
   are not, and on this deck they are the entire "your action registered"
   signal. Killing both left a user who asks for less motion with no feedback
   at all, on a card whose whole job is to make committing feel cheap.
   So: every transform animation and every loop is removed outright, and
   opacity/colour transitions survive at a shortened 120ms. */
@media (prefers-reduced-motion: reduce) {
  .now-surface *, .now-surface *::before, .now-surface *::after {
    animation: none !important;
    transition-property: opacity, color, background-color, border-color, box-shadow, outline-color;
    transition-duration: 0.12s;
  }
  /* The dealt card's rise is opacity+filter only, so it is the one animation
     worth keeping — as a plain fade, with no filter sweep. */
  .now-surface .now-card { animation: now-fade-in 0.14s ease-out !important; }
  /* Hover/press movement, removed at the source rather than duration-zeroed. */
  .now-surface .now-act:hover, .now-surface .now-act:focus-visible,
  .now-fab:hover, .now-surface .now-tab:hover { transform: none; }
  /* The live dot stops blinking and simply stays on — the state it encodes is
     "running", and a static dot says that without pulsing. */
  .now-live { animation: none !important; opacity: 1; }
}
@keyframes now-fade-in { from { opacity: 0; } to { opacity: 1; } }

/* Phase 31 — Workflows table + Board/Table toggle */
.wf-table td { vertical-align: middle; }
.wf-star { color: var(--up-copper); font-size: 12px; }
/* Which company a row belongs to. A tinted chip carrying that org's own
   accent — the same device the deck already uses on its cards — so a
   cross-org list reads as one book of work with labels, not as three lists
   glued together. Hidden entirely when you're only in one company. */
.wf-org {
  margin-left: 8px; font-size: 11px; font-weight: var(--up-weight-semibold);
  padding: 1px 7px; border-radius: 999px; white-space: nowrap;
  color: var(--up-fg);
  background: color-mix(in srgb, var(--chip, var(--up-copper)) 18%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--chip, var(--up-copper)) 40%, transparent);
}
/* The scope row that replaced the topbar switcher. */
.wf-scopes { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 var(--up-space-md); }
.wf-scope {
  font-size: 12.5px; font-weight: var(--up-weight-medium); text-decoration: none;
  padding: 5px 12px; border-radius: 999px; color: var(--up-muted);
  border: 1px solid transparent; transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.wf-scope:hover { color: var(--up-fg); background: color-mix(in srgb, var(--up-fg) 6%, transparent); }
.wf-scope.is-on {
  color: var(--up-fg); background: color-mix(in srgb, var(--up-fg) 10%, transparent);
  border-color: color-mix(in srgb, var(--chip, var(--up-fg)) 45%, transparent);
}
/* "Confirm your email." Amber, not red — nothing is broken and nothing is
   blocked; it's the same --up-due token the deadline warnings use, which is
   exactly the register this wants: worth doing, not urgent. It sits above the
   page rather than floating, so it scrolls away and never covers anything. */
.verify-banner {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin: 0 0 var(--up-space-lg); padding: 10px 14px;
  /* Wraps to three lines on a phone; a pill radius made it a lozenge there. */
  border-radius: var(--up-radius); font-size: 13.5px;
  color: var(--up-fg);
  /* Eased with the soft pass. --up-due deepens to #6B4600 on the day theme
     because it has to be readable AS TEXT there, and a 12% wash of a dark
     brown-gold over paper is not a gold — it is mud, and it was the heaviest
     object on any page that carried it. Less fill, and let the ring say it. */
  background: color-mix(in srgb, var(--up-due) 7%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--up-due) 26%, transparent);
}
.verify-banner span { flex: 1 1 20ch; min-width: 0; }

/* The Company door (Phase 44e). Its picker is the same chip as the scope rows
   above, but each one is a submit button rather than a link — picking here
   CHANGES which company you're pointed at, and a change is a POST. Reset the
   button chrome so the two read identically. */
button.wf-scope { background: none; font-family: inherit; cursor: pointer; }
.door-picker { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 var(--up-space-md); }
/* .door-row shares every rule with .prefs-row — see the grouped-inset-list
   block above. It's the same kind of thing: a labelled way into a full page. */

.wf-anchor { font-size: 12.5px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.wf-anchor.wf-release { color: var(--up-cool, #7FA6C6); }
.wf-faces { display: inline-flex; }
.wf-faces > * { margin-left: -6px; }
.wf-faces > *:first-child { margin-left: 0; }
.wf-prog { display: inline-block; width: 90px; height: 6px; border-radius: 3px; background: color-mix(in srgb, var(--up-fg) 12%, transparent); overflow: hidden; vertical-align: middle; margin-right: 6px; }
.wf-prog-bar { display: block; height: 100%; background: var(--up-copper); border-radius: 3px; }

/* ============================================================================
   Keyboard (public/js/keys.js) — the ? sheet, the ⌘K palette, and the list
   cursor. Both overlays sit on their own scrim above everything, and both are
   rendered from the same registry as the bindings, so nothing here encodes
   which shortcuts exist.
   ============================================================================ */
.keys-sheet, .keys-palette {
  position: fixed; inset: 0; z-index: 300; display: none;
  background: color-mix(in srgb, #100F0D 62%, transparent);
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
.keys-sheet.open { display: grid; place-items: center; padding: 24px; }
.keys-palette.open { display: grid; place-items: start center; padding: 12vh 24px 24px; }

.keys-panel, .kp-panel {
  width: min(760px, 100%); max-height: 76vh; overflow-y: auto;
  background: var(--up-dialog-bg, #201E1B); color: var(--up-dialog-fg, var(--up-paper));
  border: 1px solid var(--up-glass-edge); border-radius: 14px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
}
.keys-panel { padding: 22px 24px 18px; }
.keys-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.keys-head h2 { font-family: var(--up-font-title); font-weight: 600; font-size: 19px; letter-spacing: -0.01em; margin: 0; }
.keys-x { background: none; border: none; color: var(--up-muted); font-size: 16px; cursor: pointer; padding: 4px 6px; }
.keys-x:hover { color: var(--up-fg); }
.keys-cols { columns: 2; column-gap: 34px; margin-top: 16px; }
@media (max-width: 640px) { .keys-cols { columns: 1; } }
.keys-cols section { break-inside: avoid; margin-bottom: 18px; }
.keys-cols h3 {
  font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--up-copper); font-weight: 600; margin: 0 0 6px;
}
.keys-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 5px 0; font-size: 14px; border-bottom: 1px solid var(--up-line);
}
.keys-row:last-child { border-bottom: none; }
.keys-foot { margin: 6px 0 0; font-size: 12.5px; color: var(--up-muted); }
kbd {
  font-family: var(--up-font-body); font-size: 11.5px; font-weight: 600;
  padding: 2px 7px; border-radius: 5px; white-space: nowrap;
  background: color-mix(in srgb, var(--up-fg) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--up-fg) 18%, transparent);
  box-shadow: 0 1px 0 color-mix(in srgb, var(--up-fg) 14%, transparent);
}
.keys-row i, .kp-keys i { font-style: normal; font-size: 12px; color: var(--up-muted); padding: 0 3px; }

/* palette */
.kp-panel { padding: 0; overflow: hidden; }
.kp-panel .kp-input,
.kp-panel .kp-input:hover,
.kp-panel .kp-input:focus {
  width: 100%; border: none; border-bottom: 1px solid var(--up-line); border-radius: 0;
  background: none; box-shadow: none; outline: none;
  font-family: var(--up-font-body); font-size: 17px; padding: 16px 18px;
  color: var(--up-dialog-fg, var(--up-paper));
}
.kp-list { max-height: 52vh; overflow-y: auto; padding: 6px; }
.kp-item {
  display: flex; align-items: baseline; gap: 12px; width: 100%; text-align: left;
  background: none; border: none; cursor: pointer; border-radius: 8px;
  padding: 9px 12px; color: inherit; font: inherit; font-size: 14px;
}
.kp-item.is-on { background: color-mix(in srgb, var(--up-copper) 22%, transparent); }
.kp-label { flex: 1 1 auto; }
.kp-group { font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--up-muted); }
.kp-empty { padding: 18px; color: var(--up-muted); font-size: 14px; margin: 0; }

/* the list cursor — j/k. A copper rail on the left rather than a fill, so it
   marks position without competing with the row's own content. */
[data-keyrow].is-cursor {
  position: relative;
  background: color-mix(in srgb, var(--up-copper) 10%, transparent);
}
/* impeccable-disable-next-line side-tab: this rail IS the j/k keyboard cursor — a state indicator, not decoration. Removing it breaks keyboard navigation feedback. */
[data-keyrow].is-cursor::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  background: var(--up-copper); border-radius: 0 2px 2px 0;
}
tr[data-keyrow].is-cursor td:first-child { box-shadow: inset 3px 0 0 var(--up-copper); }
tr[data-keyrow].is-cursor::before { content: none; }

/* Remove a deal — sits at the end of its stage track, quiet until you're on
   the row. Destructive, so it never competes with the ‹ › stage controls. */
.stage-track-x {
  background: none; border: none; cursor: pointer; padding: 2px 6px;
  color: color-mix(in srgb, var(--up-fg) 30%, transparent);
  font-size: 13px; line-height: 1; border-radius: 6px;
  transition: color 0.15s, background 0.15s;
}
.stage-track-row:hover .stage-track-x,
.stage-track-row:focus-within .stage-track-x { color: var(--up-muted); }
.stage-track-x:hover, .stage-track-x:focus-visible {
  color: var(--up-danger); background: color-mix(in srgb, var(--up-danger) 12%, transparent);
}

/* Keyboard chip in the topbar, and the one-time nudge that points at it. The
   chip is desktop-only — a phone has no keyboard to teach, and the topbar is
   already tight there. */
.keys-chip { background: none; border: none; cursor: pointer; display: inline-flex; align-items: center; }
@media (max-width: 700px) { .keys-chip { display: none; } }

.keys-nudge {
  position: fixed; z-index: 260; max-width: 290px;
  display: flex; flex-direction: column; align-items: flex-start; gap: 7px;
  padding: 14px 16px 13px; border-radius: 12px;
  background: var(--up-dialog-bg, #201E1B); color: var(--up-dialog-fg, var(--up-paper));
  border: 1px solid var(--up-glass-edge);
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.4);
  opacity: 0; transform: translateY(-6px);
  transition: opacity 0.28s ease, transform 0.28s ease;
  pointer-events: none;
}
.keys-nudge.open { opacity: 1; transform: none; pointer-events: auto; }
.keys-nudge b { font-size: 14px; font-weight: 600; }
.keys-nudge span { font-size: 12.5px; color: var(--up-muted); line-height: 1.5; }
.keys-nudge kbd { font-size: 10.5px; padding: 1px 5px; }
.keys-nudge-x {
  align-self: flex-end; margin-top: 2px;
  background: none; border: none; cursor: pointer; padding: 3px 2px;
  font-family: var(--up-font-body); font-size: 12px; font-weight: 600;
  letter-spacing: 0; color: var(--up-copper);
}
/* A copper pip aiming at the chip above it. */
.keys-nudge::before {
  content: ""; position: absolute; top: -5px; right: 16px;
  width: 9px; height: 9px; transform: rotate(45deg);
  background: var(--up-dialog-bg, #201E1B);
  border-left: 1px solid var(--up-glass-edge); border-top: 1px solid var(--up-glass-edge);
}

/* Getting started — the lesson list inside the ? sheet. A tutorial you can
   come back to, rather than one that happens to you once and is gone. */
.keys-lessons { margin-top: 6px; padding-top: 14px; border-top: 1px solid var(--up-line); }
.keys-lessons h3 {
  font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--up-copper); font-weight: 600; margin: 0 0 8px;
}
.keys-lesson { display: flex; align-items: baseline; gap: 10px; padding: 6px 0; font-size: 14px; }
.keys-lesson-tick { flex: none; width: 14px; color: var(--up-muted); font-size: 12px; }
.keys-lesson.is-done .keys-lesson-tick { color: var(--up-copper); }
.keys-lesson-t { flex: 1 1 auto; min-width: 0; }
.keys-lesson-t b { font-weight: 600; }
.keys-lesson-t small { display: block; font-size: 12.5px; color: var(--up-muted); line-height: 1.45; }
.keys-lesson-again {
  flex: none; background: none; border: 1px solid var(--up-line); cursor: pointer;
  border-radius: 999px; padding: 4px 11px; color: var(--up-muted);
  font-family: var(--up-font-body); font-size: 12.5px; font-weight: 500;
  letter-spacing: 0;
}
.keys-lesson-again:hover { color: var(--up-copper); border-color: var(--up-copper); }
.keys-lesson-soon {
  flex: none; font-size: 12px; letter-spacing: 0;
  color: color-mix(in srgb, var(--up-fg) 35%, transparent);
}
/* A nudge anchored to a control on the LEFT points from its left edge. */
.keys-nudge:not(.nudge-right)::before { right: auto; left: 16px; }

/* --- Reduced transparency -------------------------------------------------
   The app leans on frosted glass in ~25 places, and translucency means every
   panel has other content moving behind its text. "Reduce Transparency" is the
   OS switch people with visual-processing sensitivity actually turn on, and we
   were ignoring it entirely while carefully honouring prefers-reduced-motion.

   Solid surfaces, no blur. The tokens are re-declared rather than the rules
   rewritten, so this stays one block and can't drift from the design. Layered
   in luminance order: chrome darkest, stage above it, panels above that. */
@media (prefers-reduced-transparency: reduce) {
  :root, .content, .create-modal, .form-modal {
    --up-glass:       var(--up-surface);
    --up-glass-2:     var(--up-surface-2);
    --up-chrome-glass: var(--up-ground);
  }
  .now-surface { --n-glass: #26251F; }
  [data-theme$="light"] .now-surface,
  .now-surface[data-theme$="light"] { --n-glass: #FBFAF7; }
  .card, .board-column, .board-card, .project-tile, .now-card, .now-sheet,
  .now-toast, .now-capture, .now-clear, .sidebar, .topbar, .bottom-nav,
  .keys-nudge, dialog {
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
  }
  /* The lit stage and the grain are the other half of the effect. */
  .app-shell::before, .app-shell::after, .main-col::before, .main-col::after,
  body::after { display: none !important; }
}

/* One "+" per screen. The deck carries its own copper FAB, so the topbar's
   copper "+" was a second filled disc offering the identical action a few
   hundred pixels away — two attention magnets in permanent chrome competing
   with the card, which is the only thing on that page that should pull. Copper
   means CURRENT; the create button is not current, and there is no version of
   "which + should I press" worth making someone answer. Same :has() hook the
   deck already uses to opt out of the content width cap. */
.app-shell:has(.now-surface) .qc-trigger { display: none; }

/* --- the running card ------------------------------------------------------
   A timer that's running replaces the card it belongs to. Centred column: the
   company, the dial, the task, and the one way out. No date chip, no swipe
   stamps, no workflow context — during the work, every one of those is a thing
   to think about instead of the thing you committed to. */
.now-run { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; height: 100%; text-align: center; }
.now-run .now-proj { justify-content: center; }
.now-run-title { font-family: var(--up-font-display); font-weight: 400; font-size: clamp(18px, 3.4vw, 23px); line-height: 1.2; margin: 0; color: var(--n-fg); text-wrap: balance; }

.now-dial { position: relative; width: 168px; height: 168px; display: grid; place-items: center; }
.now-dial-svg { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }
.now-dial-track, .now-dial-fill { fill: none; stroke-width: 5; }
.now-dial-track { stroke: color-mix(in srgb, var(--n-fg) 14%, transparent); }
.now-dial-fill {
  stroke: var(--n-copper); stroke-linecap: round;
  stroke-dasharray: 339.292;
  /* Just under the 1s tick, so each step lands before the next arrives. */
  transition: stroke-dashoffset 0.95s linear;
}
.now-dial-time { position: relative; font-family: var(--up-font-display); font-size: 40px; letter-spacing: 0.01em; color: var(--n-fg); font-variant-numeric: tabular-nums; }

/* Open run: nothing to count down to, so the ring doesn't pretend. A short arc
   sweeps once a minute purely to show the clock is live. */
.now-run.is-open .now-dial-fill {
  stroke-dasharray: 42 339.292; stroke-dashoffset: 0;
  transition: none;
  transform-origin: 60px 60px;
  animation: now-sweep 60s linear infinite;
}
@keyframes now-sweep { to { transform: rotate(360deg); } }

.now-run .now-time-go { font-size: 14px; padding: 9px 22px; }
.now-run .now-time-lock { font-size: 12px; color: var(--n-muted); }

@media (prefers-reduced-motion: reduce) {
  .now-dial-fill { transition: none; }
  .now-run.is-open .now-dial-fill { animation-duration: 240s; }
}
@media (max-width: 700px) {
  .now-dial { width: 200px; height: 200px; }
  .now-dial-time { font-size: 46px; }
}

/* --- sidebar head: the account row ----------------------------------------
   Todoist's arrangement, which is what Mark asked for: you, your name, a
   chevron — top-left, above the nav. It replaced the UROS wordmark AND the
   topbar's account gear, so there is exactly one "you" on screen and it's
   also the thing that opens your account. Bell and keyboard chip sit beside
   it; the topbar keeps only search. */
.sidebar-head { display: flex; align-items: center; gap: 6px; }
/* A plain link to /profile now, not a dropdown — see the note in lib/layout.js. */
.sidebar-user {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: 8px;
  padding: 5px 7px; border: 0; border-radius: var(--up-radius-sm);
  background: transparent; font: inherit; cursor: pointer;
  color: var(--up-paper); text-decoration: none;
  transition: background 0.15s ease;
}
.sidebar-user:hover { background: color-mix(in srgb, var(--up-paper) 10%, transparent); }
.sidebar-user-name {
  font-weight: var(--up-weight-semibold); font-size: 14px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
/* Points down at rest, up when the panel is open — the only moving part. */
/* (.sidebar-user-caret went with the dropdown — the row is a plain link now.) */
/* The panel hangs from the row, expanding RIGHT: this trigger is on the left
   of the screen, the opposite of the topbar menus it inherits from. */
.sidebar-user .menu-panel { left: 0; right: auto; min-width: 210px; }

.sidebar-head-actions { display: flex; align-items: center; gap: 10px; flex: none; }
.sidebar-head-actions .topbar-link { display: inline-flex; align-items: center; color: var(--up-paper); opacity: 0.75; }
.sidebar-head-actions .topbar-link:hover { opacity: 1; }

/* Collapsed to the rail: the name, the bell and the keys chip go — the avatar
   STAYS, in the top-left corner, at the same size and the same place it holds
   when the panel is open. It used to be display:none'd here and replaced by a
   monogram tile; see the collapse block up top for why that was the wrong
   shape of change. The label fades are declared there too, with the timing. */

/* The amnesty (Phase 45a). Quiet on purpose — it's the way out of a bad week,
   and it should be findable when you're drowning without being the easy thing
   to reach for on an ordinary Tuesday. Underlined text, not a button shape. */
.now-amnesty { margin: 10px 0 0; }
.now-amnesty button {
  background: none; border: none; padding: 0; cursor: pointer;
  font-family: var(--up-font-body); font-size: 12px;
  color: var(--n-muted); text-decoration: underline; text-underline-offset: 3px;
}
.now-amnesty button:hover { color: var(--n-fg); }

/* What you can reach for (Phase 45g). Same visual weight as the amnesty above
   it, and for the same reason: it's an escape hatch, and an escape hatch that
   competes with the deck for attention has stopped being one. The picked
   option firms up rather than filling in — a lit-up pill would read as a mode
   you are now IN, and this is deliberately something you are only passing
   through. */
.now-fuel { margin: 8px 0 0; font-size: 12px; color: var(--n-muted); }
.now-fuel-lead { margin-right: 2px; }
.now-fuel-sep { margin: 0 4px; opacity: 0.6; }
/* min-height 24 with the padding split around the text, so the hit area grows
   without the row growing: these sit on a baseline with .now-fuel-sep between
   them and any vertical padding would push that separator off-centre. */
.now-fuel-opt {
  background: none; border: none; padding: 5px 0; cursor: pointer;
  min-height: 24px;
  font-family: var(--up-font-body); font-size: 12px;
  color: var(--n-muted); text-decoration: underline; text-underline-offset: 3px;
}
.now-fuel-opt:hover { color: var(--n-fg); }
.now-fuel-opt.is-picked { color: var(--n-fg); font-weight: 600; text-decoration-thickness: 2px; }


/* Nothing fits the filter. Reuses the clear card, so it has to actively undo
   the celebration: the big tally means "left", not "done", and there is no
   trophy here. */
.now-clear.is-nofit .now-clear-tally { opacity: 0.5; }

/* How big is this? (Phase 45g) — only ever on a running card, only until it's
   answered. Sits under the Stop button as a quiet afterthought, because that
   is exactly its priority: the work is the point, this is a question you may
   ignore forever at no cost. */
.now-sizeask {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 6px; margin-bottom: 8px;
}
.now-sizeask[hidden] { display: none; }
.now-size-ask { font-size: 11.5px; color: var(--n-muted); margin-right: 2px; }
/* 24px minimum (WCAG 2.5.8). Was 22px tall — close enough to look fine and
   still under the floor, which is how it survived. A pill is correct here
   because each label is a single line. */
.now-size-opt {
  background: none; cursor: pointer;
  display: inline-flex; align-items: center; min-height: 24px;
  border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
  border-radius: 999px; padding: 4px 10px;
  font-family: var(--up-font-body); font-size: 11.5px;
  color: var(--n-muted);
}
.now-size-opt:hover { color: var(--n-fg); border-color: color-mix(in srgb, currentColor 45%, transparent); }

/* Capacity line (Phase 45c). States the arithmetic and stops — no warning
   colour when you're over, because the useful thing is seeing the number, not
   being told off by it. Slightly firmer weight is the whole escalation. */
.now-load { margin: 10px 0 0; font-size: 12.5px; color: var(--n-muted); }
.now-load.is-over { color: var(--n-fg); }

/* The blocked-card wizard (Phase 45e). A sheet, not a toast: it asks a real
   question and deserves the screen for the moment it's up. Dark in both themes
   like the deck's other overlays. */
.now-stuck {
  position: fixed; inset: 0; z-index: 60; display: grid; place-items: center;
  padding: 20px; background: color-mix(in srgb, #100F0D 74%, transparent);
  -webkit-backdrop-filter: none; backdrop-filter: none;
  opacity: 0; transition: opacity 0.18s ease;
}
.now-stuck.open { opacity: 1; }
.now-stuck-card {
  width: 100%; max-width: 420px; padding: 26px 24px 20px;
  border-radius: var(--up-radius-lg); background: #201E1B; color: var(--up-paper);
  border: 1px solid color-mix(in srgb, var(--up-paper) 12%, transparent);
  box-shadow: 0 24px 60px rgba(0,0,0,0.5);
}
.now-stuck-kicker { margin: 0; font-size: 12px; color: var(--up-due); }
.now-stuck-card h3 { font-family: var(--up-font-display); font-weight: 400; font-size: 22px; margin: 6px 0 0; }
.now-stuck-sub { margin: 8px 0 16px; font-size: 13px; color: color-mix(in srgb, var(--up-paper) 62%, transparent); }
.now-stuck-card button[data-stuck] {
  display: block; width: 100%; text-align: left; margin-bottom: 8px;
  padding: 11px 13px; border-radius: var(--up-radius-sm); cursor: pointer;
  background: color-mix(in srgb, var(--up-paper) 7%, transparent);
  border: 1px solid transparent; color: var(--up-paper);
  transition: background 0.14s ease, border-color 0.14s ease;
}
.now-stuck-card button[data-stuck]:hover {
  background: color-mix(in srgb, var(--up-paper) 12%, transparent);
  border-color: color-mix(in srgb, var(--up-copper) 45%, transparent);
}
.now-stuck-card button b { display: block; font-size: 14px; font-weight: 600; }
.now-stuck-card button span { display: block; font-size: 12px; color: color-mix(in srgb, var(--up-paper) 55%, transparent); }
.now-stuck-skip { margin-top: 6px; background: none !important; border: none !important; text-align: center !important; }
.now-stuck-skip { font-size: 12px; color: color-mix(in srgb, var(--up-paper) 50%, transparent) !important; }

/* "<name> is waiting" (Phase 45f). Amber like the other attention states, and
   sitting in the meta row with the date chip rather than shouting above it. */
.now-waiting { font-size: 12px; font-weight: 600; color: var(--n-amber); align-self: center; }

/* --- preferences dialog ---------------------------------------------------
   Claude's settings shape: grouped rail on the left, wide pane on the right,
   x top-right. Settings are a MODE you enter and leave — a dialog says that,
   where a nav row said "this is a place you live".

   Scoped theme tokens like .form-modal above: a <dialog> lives in the top
   layer, OUTSIDE .content, so it inherits none of the stage's surface tokens
   and every field inside would otherwise render with the wrong theme's colours.
   I shipped exactly that bug first — a dark-assumed rail on a cream dialog,
   with the tab labels near-invisible. Don't hardcode --up-paper in here. */
.prefs {
  --up-fg: var(--up-paper);
  --up-surface:   color-mix(in srgb, var(--up-paper) 8%, var(--up-ink));
  --up-surface-2: color-mix(in srgb, var(--up-paper) 12%, var(--up-ink));
  --up-line:      color-mix(in srgb, var(--up-paper) 15%, var(--up-ink));
  --up-muted:     color-mix(in srgb, var(--up-paper) 55%, var(--up-ink));
  --up-due:       #D8A44B;
  --prefs-bg:     color-mix(in srgb, var(--up-paper) 4%, var(--up-ink));
  --prefs-rail:   color-mix(in srgb, var(--up-paper) 8%, var(--up-ink));
  padding: 0; border: none; background: none;
  width: 100%; height: 100%; max-width: none; max-height: none;
}
body[data-theme$="light"] .prefs {
  --up-fg: var(--up-ink);
  --up-surface:   #FBFAF7;
  --up-surface-2: color-mix(in srgb, var(--up-ink) 5%, var(--up-cream));
  --up-line:      color-mix(in srgb, var(--up-ink) 14%, var(--up-cream));
  /* 2026-08-07: 46% -> 58%. Measured while building the quiet themes: a
     .up-caption on a card in light mode composited to 3.24:1, under the
     4.5 small-text floor, and .up-caption is the most-used text class in
     the app. Nothing to do with those themes — a pre-existing hole the
     measurement happened to walk into. Dark measured 4.85 and is left
     alone. */
  --up-muted:     color-mix(in srgb, var(--up-ink) 66%, var(--up-cream));
  --up-due:       #8A5B14;
  --prefs-bg:     #F7F5F1;
  --prefs-rail:   color-mix(in srgb, var(--up-ink) 5%, var(--up-cream));
}
.prefs::backdrop { background: rgba(8, 7, 6, 0.55); -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); }
.prefs-shell {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  display: grid; grid-template-columns: 216px minmax(0, 1fr);
  width: min(880px, calc(100vw - 32px)); height: min(620px, calc(100vh - 48px));
  border-radius: var(--up-radius-lg); overflow: hidden;
  background: var(--prefs-bg); color: var(--up-fg);
  border: 1px solid var(--up-line);
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45);
}
.prefs-tabs {
  padding: 16px 10px; overflow-y: auto;
  background: var(--prefs-rail);
  border-right: 1px solid var(--up-line);
}
.prefs-group {
  display: block; padding: 12px 10px 6px; font-size: 12px; color: var(--up-muted);
}
.prefs-group:first-child { padding-top: 0; }
.prefs-tab {
  display: block; width: 100%; text-align: left; margin-bottom: 2px;
  padding: 8px 10px; border: none; border-radius: var(--up-radius-sm);
  background: none; color: var(--up-fg); opacity: 0.75;
  font-size: 14px; cursor: pointer; transition: background 0.14s ease, opacity 0.14s ease;
}
.prefs-tab:hover { background: var(--up-surface-2); opacity: 1; }
.prefs-tab-on { background: var(--up-surface-2); opacity: 1; font-weight: 600; }

.prefs-body { position: relative; padding: 26px 28px; overflow-y: auto; }
.prefs-x {
  position: absolute; top: 14px; right: 16px; width: 30px; height: 30px;
  display: grid; place-items: center; border: none; border-radius: 50%;
  background: none; color: var(--up-muted); font-size: 22px; line-height: 1; cursor: pointer;
}
.prefs-x:hover { background: var(--up-surface-2); color: var(--up-fg); }

.prefs-pane { display: none; }
.prefs-pane-on { display: block; }
.prefs-pane h3 { margin: 0 0 18px; font-size: 19px; font-weight: 600; color: var(--up-fg); }

.prefs-field { padding: 14px 0; border-top: 1px solid var(--up-line); }
.prefs-field:first-of-type { border-top: none; padding-top: 0; }
.prefs-field > label { display: block; margin-bottom: 8px; font-size: 14px; font-weight: 600; color: var(--up-fg); }
.prefs-field .up-caption { display: block; margin-top: 8px; max-width: 52ch; color: var(--up-muted); }
/* The theme field explains itself under the control rather than beside it —
   "quiet" is a word that needs a sentence, and a sentence needs a line. */
.prefs-field-stack .prefs-hint {
  margin: 8px 0 0; max-width: 52ch;
  font-size: 12.5px; line-height: 1.5; color: var(--up-muted);
}

/* Rows that leave for a real page — they're links, and they say so.
   ---------------------------------------------------------------------------
   iOS GROUPED INSET LIST. These were separate floating tiles with a gap between
   each, which is a Material/web idiom; Apple joins consecutive rows into ONE
   rounded group and separates them with a hairline that is INSET from the
   leading edge so it starts under the text rather than cutting the whole card
   in half. That single detail is probably the most recognisable thing in iOS
   layout, and it costs a first/last-child rule.

   Note there is no drop shadow on the group. iOS grouped lists read as raised
   purely from the background step — recessed page, lighter card — which is
   exactly the direction the quiet themes already went. */
.prefs-row, .door-row {
  display: block; position: relative; padding: 12px 15px;
  text-decoration: none; background: var(--up-surface-2); color: var(--up-fg);
  font-size: 14px; font-weight: 600;
  transition: background 0.14s ease;
}
.prefs-row span, .door-row span {
  display: block; margin-top: 2px; font-size: 12.5px; font-weight: 400; color: var(--up-muted);
}
/* The group: corners on the ends only, hairline between. */
.prefs-row:first-of-type, .door-row:first-of-type {
  border-top-left-radius: var(--up-radius); border-top-right-radius: var(--up-radius);
}
.prefs-row:last-of-type, .door-row:last-of-type {
  border-bottom-left-radius: var(--up-radius); border-bottom-right-radius: var(--up-radius);
}
.prefs-row + .prefs-row::before, .door-row + .door-row::before {
  content: ""; position: absolute; top: 0; left: 15px; right: 0;
  height: 1px; background: var(--up-line);
}
.prefs-row:hover, .door-row:hover { background: color-mix(in srgb, var(--up-fg) 7%, var(--up-surface-2)); }
/* The chevron. A row that leaves for another page says so on iOS, and it is
   the difference between a list of links and a list of buttons. */
.prefs-row::after, .door-row::after {
  content: ""; position: absolute; right: 15px; top: 50%;
  width: 6px; height: 6px; margin-top: -3px;
  border-right: 1.5px solid var(--up-muted); border-top: 1.5px solid var(--up-muted);
  border-radius: 1px;
  transform: rotate(45deg);
  opacity: 0.5;
}
.prefs-row, .door-row { padding-right: 34px; }

/* The card holding a grouped list gets out of the way.
   ---------------------------------------------------------------------------
   iOS puts the light GROUP on the recessed page and the section header OUTSIDE
   it, in small muted caps. Wrapping the group in a lighter card inverted that —
   pale card, grey rows — which is the one arrangement Apple never uses, because
   then the rows read as recessed and the container reads as the content.

   So a card that contains rows drops its own fill entirely and becomes just a
   heading plus the group. Scoped with :has() so every other card on the app is
   untouched. */
.card:has(> .door-row) {
  background: none; border: none; box-shadow: none;
  -webkit-backdrop-filter: none; backdrop-filter: none;
  padding: 0; margin-top: var(--up-space-lg);
}
.card:has(> .door-row) > h2 {
  font-size: 12.5px; font-weight: var(--up-weight-semibold);
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--up-muted); margin: 0 0 8px 15px;
}
/* On the page itself the rows are the lifted surface, so they take the panel
   colour and the stage behind them does the recessing. */
.card:has(> .door-row) .door-row { background: var(--up-surface); }
.card:has(> .door-row) .door-row:hover { background: color-mix(in srgb, var(--up-fg) 5%, var(--up-surface)); }

.prefs-who { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; }
.prefs-who .up-caption { display: block; color: var(--up-muted); }
.prefs-logout { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--up-line); }
.prefs-saved { margin-top: 16px; font-size: 13px; color: var(--up-due); }

@media (max-width: 700px) {
  .prefs-shell { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 1fr); height: min(88vh, calc(100vh - 24px)); }
  .prefs-tabs { display: flex; gap: 6px; overflow-x: auto; padding: 10px; border-right: none; border-bottom: 1px solid var(--up-line); }
  .prefs-group { display: none; }
  .prefs-tab { width: auto; white-space: nowrap; margin-bottom: 0; }
}

/* ============================================================================
   THE QUIET THEMES — "ADHD dark" and "ADHD light" (2026-08-07)

   Mark asked for an ADHD-friendly theme, and the research is worth writing
   down because half of it says DON'T.

   What the evidence does NOT support, and what this therefore is not:

     * Coloured overlays / tinted backgrounds as a reading aid. Forty years of
       investigation and the effect keeps failing to replicate; a 5,822-child
       study at Bristol and Newcastle found no link between visual stress and
       dyslexia, and "visual stress" is recognised by neither the WHO's ICD,
       the DSM, nor the American Optometric Association. So there is no lilac
       wash and no "ADHD colour" here. Selling one would be selling a myth.
     * The blue-yellow colour-discrimination finding in ADHD (Banaschewski
       2006) is a 14-vs-13-child study; the 2014 follow-up testing the retinal
       dopaminergic hypothesis found a difference only in women on a saturation
       task and NOTHING on the standard Farnsworth-Munsell test. Too thin to
       design a palette around — but it costs nothing to stop blue-vs-yellow
       ever being the only thing telling two states apart, which is also plain
       WCAG 1.4.1, so that hedge is taken.

   What the evidence does support is about NOISE, not hue, and it comes from
   W3C's COGA guidance (Making Content Usable, §4.4.11, §4.3.3, §4.4.10):
   solid backgrounds behind blocks of text, regions separated by real dividing
   lines and whitespace rather than by suggestion, strong contrast, and fewer
   things competing for attention.

   So these two are not new palettes. They are the SAME hues with the salience
   spent differently — every decorative use of colour and light switched off so
   the handful of colours that MEAN something are the only ones left talking:
   copper for current, amber for due, red for wrong. On a page where the
   background is quietly glowing, the panels are translucent and everything
   lifts on hover, the one card that matters has to shout to be heard over the
   furniture. Here there is no furniture.

   Implementation note: `adhd-dark` and `adhd-light` inherit their base theme
   wholesale — every selector in this file matches on `[data-theme$="dark"]` /
   `[data-theme$="light"]`, so not one token is duplicated. This block is the
   difference, and it lives at the end of the file so it wins on source order
   against the equal-specificity theme blocks above.
   ============================================================================ */

/* 1. The ambient grade goes. Two fixed radial pools (copper top-right, navy
      bottom-left) light the whole stage; it is the single largest piece of
      moving-looking colour in the app and it is behind everything you read. */
/* The wash STAYS — it is the world, not decoration, and tokens.css already
   pulls the rose out of it for the quiet pair. What goes is the drift: a
   background that moves is exactly what this pair exists to remove. */
body[data-theme^="adhd"] .app-shell,
body[data-theme^="adhd"].bare-doc,
body[data-theme^="adhd"].cyc-plain { animation: none; }

/* 2. The film grain goes. It is only 2.5% opacity, but it is a texture over
      every glyph on the screen, which is exactly what COGA means by not
      obscuring foreground content with background. */
body[data-theme^="adhd"]::after { display: none; }

/* 3. Glass becomes paper. --up-glass* are translucent by design so panels pick
      up the grade behind them; with the grade gone they would just be muddy,
      and translucency is what makes a region's edge a suggestion instead of a
      fact. Solid fills, real hairlines, no drop shadows, no backdrop blur. */
body[data-theme^="adhd"] .content,
body[data-theme^="adhd"] .create-modal,
body[data-theme^="adhd"] .form-modal {
  --up-glass: var(--up-surface);
  --up-glass-2: var(--up-surface-2);
  --up-glass-hi: transparent;
  /* NOT `none`. .card composes `box-shadow: inset ..., var(--up-glass-shadow)`,
     and `none` inside a shadow list is invalid — the whole declaration then
     drops at computed-value time, which happened to look right and was pure
     luck. A fully transparent shadow is the value that means "no shadow" in a
     list, and it stays correct if the composition changes. */
  --up-glass-shadow: 0 0 0 rgba(0, 0, 0, 0);
  /* The panel edge becomes the firmed hairline. With translucency and drop
     shadows both gone, the border is the ONLY thing left saying where a card
     stops — which is COGA's "clearly differentiate regions" done with a line
     instead of with an illusion of depth. */
  --up-glass-edge: color-mix(in srgb, var(--up-fg) 22%, transparent);
  /* Lines carry the structure now that translucency doesn't. ~24% against the
     stage instead of ~14%: a hairline you can actually see is the cheapest
     version of COGA's "clearly differentiate regions". */
  --up-line: color-mix(in srgb, var(--up-fg) 24%, transparent);
  --up-border: var(--up-line);
  /* Secondary text is where contrast fails first, and it is most of the
     writing on this app — captions, metadata, timestamps. Up a step. */
  --up-muted: color-mix(in srgb, var(--up-fg) 72%, transparent);
}
body[data-theme^="adhd"] .card,
body[data-theme^="adhd"] .board-column,
body[data-theme^="adhd"] .board-card,
body[data-theme^="adhd"] .project-tile,
body[data-theme^="adhd"] .sidebar,
body[data-theme^="adhd"] .topbar,
body[data-theme^="adhd"] .bottom-nav,
body[data-theme^="adhd"] .prefs-shell,
body[data-theme^="adhd"] .now-card {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
/* The chrome is a flat bezel rather than a frosted one. */
/* The chrome firms up into a flat plane with a real edge. This is the one
   place the quiet pair departs from the seamless rule, and deliberately: with
   translucency gone, a line is the only thing left that can say where the nav
   stops, which is COGA's "clearly differentiate regions" done honestly. */
body[data-theme^="adhd"] { --up-chrome-glass: var(--up-surface-2); }
body[data-theme^="adhd"] .sidebar { border-right: 1px solid var(--up-line); }
body[data-theme^="adhd"] .topbar { border-bottom: 1px solid var(--up-line); }

/* Belt and braces on the panels that hand-roll their own shadow rather than
   composing the token. */
body[data-theme^="adhd"] .card,
body[data-theme^="adhd"] .board-column,
body[data-theme^="adhd"] .project-tile,
body[data-theme^="adhd"] .prefs-shell,
body[data-theme^="adhd"] .create-modal,
body[data-theme^="adhd"] .form-modal { box-shadow: none; border: 1px solid var(--up-line); }

/* 4. Nothing lifts, glows or slides on hover. A hover state still has to be
      visible — it just says so with colour instead of with movement, which is
      the one thing on a page that catches the eye involuntarily. */
body[data-theme^="adhd"] .now-act:hover,
body[data-theme^="adhd"] .now-act:focus-visible,
body[data-theme^="adhd"] .ev:hover,
body[data-theme^="adhd"] .door-row:hover,
body[data-theme^="adhd"] .prefs-row:hover { transform: none; filter: none; }

/* 5. The home hero stops being a lit film frame. It is a graded, grained,
      vignetted screen with a timecode on it — the most decorative object in
      the app, and it is the first thing on the page you open most. Here it
      keeps the greeting and loses the cinema. */
body[data-theme^="adhd"] .hero-frame { background: var(--up-surface); border: 1px solid var(--up-line); }
body[data-theme^="adhd"] .frame-grade,
body[data-theme^="adhd"] .frame-vignette,
body[data-theme^="adhd"] .frame-grain,
body[data-theme^="adhd"] .frame-rule { display: none; }
body[data-theme^="adhd"] .hero-frame h1,
body[data-theme^="adhd"] .hero-frame .sub { color: var(--up-fg); text-shadow: none; }
body[data-theme^="adhd"] .hero-frame .sub { color: var(--up-muted); }

/* 6. The deck's own dark palette is declared on .now-surface and its overlays,
      so it needs the same treatment named explicitly rather than inherited. */
body[data-theme^="adhd"] .now-surface { background-image: none; }
body[data-theme^="adhd"] .now-card { box-shadow: none; border-color: var(--n-edge); }

/* ---- the quiet pair, against the Cyclorama additions ---------------------
   These sit last on purpose: the motion rules above are more specific than a
   plain [data-theme^="adhd"] selector, so the quiet overrides have to match
   them selector for selector or they lose. */
body[data-theme^="adhd"] .now-card:not(.stacked):not(.settling):not(.leaving) {
  animation: none; filter: none;
}
/* The horizon is a large soft field behind reading matter. It is the best
   thing in the loud themes and the wrong thing here. */
body[data-theme^="adhd"] .content::after { display: none; }
body[data-theme^="adhd"] #now-panel-later { opacity: 1; transition: none; }
/* One flat fill for the committing action instead of the day gradient: a
   gradient is the definition of decoration on a control. */
body[data-theme^="adhd"] .now-surface .now-act-done {
  background: var(--up-strong-bg); color: var(--up-strong-fg); box-shadow: none;
}
body[data-theme^="adhd"] .btn { background: var(--up-strong-bg); box-shadow: none; }
body[data-theme^="adhd"] .now-fab { background: var(--up-strong-bg); box-shadow: none; }
body[data-theme^="adhd"] .now-fab:hover { transform: none; box-shadow: none; }
/* Still keeps category meaning, but turns the calendar back into a reading
   surface: color lives in the tiny category mark, never in the entire row. */
body[data-theme^="adhd"] .wk .ev,
body[data-theme^="adhd"] .cal .ev { background: var(--cal-chip); }
body[data-theme^="adhd"] .wk .ev-mark,
body[data-theme^="adhd"] .cal .ev-mark { font-weight: var(--up-weight-bold); }
body[data-theme^="adhd"] .wk .ev-project .ev-mark,
body[data-theme^="adhd"] .cal .ev-project .ev-mark { color: var(--ev-tint-project); }
body[data-theme^="adhd"] .wk .ev-shoot .ev-mark,
body[data-theme^="adhd"] .cal .ev-shoot .ev-mark { color: var(--ev-tint-shoot); }
body[data-theme^="adhd"] .wk .ev-release .ev-mark,
body[data-theme^="adhd"] .cal .ev-release .ev-mark { color: var(--ev-tint-release); }
body[data-theme^="adhd"] .wk .ev-call .ev-mark,
body[data-theme^="adhd"] .cal .ev-call .ev-mark { color: var(--ev-tint-call); }
body[data-theme^="adhd"] .ev-release { box-shadow: inset 1px 0 0 var(--ev-accent, var(--up-copper)); }


/* The card appears already present. A deck action supplies plenty of feedback;
   a blur-in on every deal turns task selection into scenery. */
/* The Do rail sits OUTSIDE the light — distance rendered as dimness rather
   than as a divider, which is the only way this world is allowed to say
   "further back". */
#now-panel-later { opacity: 0.78; transition: opacity 0.3s var(--cyc-ease); }
#now-panel-later:hover, #now-panel-later:focus-within { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .now-card:not(.stacked) { animation: none; filter: none; }
  .now-act { transition: none; }
  #now-panel-later { transition: none; }
}

/* ============================================================================
   INPUT METHOD AND REACH (2026-08-10, /impeccable adapt)

   Before this the app had ZERO `pointer:` or `hover:` queries — every
   adaptation it made was keyed on viewport WIDTH alone. Width is not input
   method and it is not reach, and the three places that gap showed up were all
   on the surface PRODUCT.md calls a first-class usage scene:

     · a 26x24 hamburger that is the ONLY way off the deck on a phone
     · 14px inputs, which make iOS Safari zoom the whole page on focus and
       never zoom back — on the add-a-task field, the project select and both
       halves of the date picker, i.e. the entire mobile capture flow
     · the Do rail parked at 78% opacity and lit only by :hover, so on a touch
       screen it is permanently dimmed and there is no gesture that undims it

   Everything here is scoped to coarse pointers or hover-less devices. Nothing
   below changes a single pixel of the mouse-and-keyboard rendering, because the
   fine-pointer layout is not the one that was broken.
   ============================================================================ */

/* --- 1. The 16px floor -----------------------------------------------------
   iOS Safari zooms the viewport when a focused field is under 16px. It is not
   a preference and there is no way to opt out short of disabling user zoom,
   which would be an accessibility regression traded for a layout one. The
   answer is simply not to go under 16. */
@media (pointer: coarse) {
  input:not([type="checkbox"]):not([type="radio"]),
  select,
  textarea,
  .dp-field input.dp-input,
  .mnav-search input,
  .mytasks-add input,
  .mytasks-add select { font-size: 16px; }
}

/* --- 2. Reach --------------------------------------------------------------
   44x44 is the floor for a finger. Where a control is a real block (the deck's
   escape hatch, the tab bar, the drawer's close) it simply grows. Where it is
   a small instrument sitting inline in a line of type — the tally light, the
   fuel chips — growing the BOX would wreck the line, so the box holds its size
   and an invisible pseudo-element carries the hit area. The visual scale of the
   soft pass is unchanged on every one of these. */
@media (pointer: coarse) {
  .now-menu,
  .sidebar-close { min-width: 44px; min-height: 44px; flex: none; }
  /* .sidebar-close measured 22.6px square despite its own min-width/min-height:
     it is a flex item and was being shrunk below its minimum. flex: none is the
     half of that fix the original never had. */

  .now-tab { padding-top: 13px; padding-bottom: 13px; }

  .now-tally,
  .now-fuel-opt { position: relative; }
  .now-tally::after,
  .now-fuel-opt::after {
    content: "";
    position: absolute;
    left: 0; right: 0; top: 50%;
    height: 44px;
    transform: translateY(-50%);
  }

  /* Undo is a product commitment, not a nicety — PRODUCT.md puts it on every
     destructive action — and it was a 29px target inside a toast that dismisses
     itself. The one control in the app you reach for in a hurry was the hardest
     one to hit. */
  .now-toast button { min-height: 44px; padding-left: 16px; padding-right: 16px; }

  /* The nav rows themselves. 36px is a comfortable MOUSE row and it is the one
     the icon-column work settled on, but on a phone this list IS the app's
     navigation — it is what the drawer opens to show you. 30px of content plus
     the row's own 14px of padding lands it on 44 without touching the 14px
     left padding that holds the icon column, so the column survives.
     NOTE the value is the FULL 44, not 30: `* { box-sizing: border-box }` is
     set at the top of this file, so min-height is the border box, padding
     included. Reasoning in content-box terms here silently leaves the row at
     its natural 36px, which is exactly what it did on the first attempt. */
  .nav-link { min-height: 44px; }
  .sidebar-user { min-height: 44px; }

  /* Small chrome that is still a real destination: notifications, and the one
     button on the confirm-your-email banner. Both were pointer-sized. */
  .topbar-link.bell,
  .topbar-link { min-width: 44px; min-height: 44px; justify-content: center; }
  .btn-small { min-height: 44px; padding-top: 0; padding-bottom: 0; }
  /* The mobile menu's own rows — Settings, Log out and friends. They landed at
     43px, one short, which is the kind of miss only measurement catches. */
  .mnav-link { min-height: 44px; }
  /* The deck's cleared-state CTA — the one button on the screen you see when
     you have finished everything, and the only way to pull more work without
     leaving the deck. It carries no class of its own, hence the id. */
  #now-pull { min-height: 44px; }
}

/* --- 3. Hover is not available -------------------------------------------
   DESIGN.md renders the Do rail's distance as dimness: 78% opacity, coming up
   "when you reach for it". On a touch screen there is no reaching — there is
   only touching — so the rail sat permanently dimmed and the dimness stopped
   meaning "further back" and started meaning "harder to read". The rail is
   information, so on a hover-less device it is simply lit. */
@media (hover: none) {
  #now-panel-later { opacity: 1; }

  /* The task tick goes copper on hover to say "this is the one you are about
     to commit". Touch has no hover, so :active carries it instead — the signal
     arrives under the finger rather than never. */
  button.todo-check:active { color: var(--up-copper); }
}

/* --- 4. Landscape, which is a phone the app was calling a desktop ----------
   An iPhone on its side is 844x390. That is wider than the 700px breakpoint,
   so every phone rule missed it and the deck ran the desktop masthead — slug,
   40px greeting, subtitle, fuel row — which ate 253px of a 390px window before
   the card even started. Measured: the card began at y=253, the fixed tab bar
   starts at y=328, and the page scrolled to 608. The card was underneath its
   own tab bar.

   The height tiers above already shrink the deck; what they never shrink is the
   masthead, and on a short screen the masthead is the part that is hospitality
   rather than work. The greeting gives up the room, exactly as the note on the
   680px tier says it should — but this time all of it. The slug stays: it is
   the tally, the date and the deck count, and those are instruments. */
@media (max-height: 520px) and (orientation: landscape) {
  .now-greeting h1 { display: none; }
  .now-greeting { margin-top: 2px; }
  .now-greeting p { margin: 0; font-size: 14px; }
  .now-head { padding-top: 0; }
  .now-deck { margin: 6px 0 34px; min-height: 140px; }
  .now-surface { min-height: 0; padding-bottom: 72px; }
  .now-fuel { margin-top: 4px; }
  /* The stage's own 40px of top padding is the last thing between the card and
     the tab bar. Collapsing the masthead alone left the card's bottom edge 28px
     under the bar — measured, not estimated — and this is whitespace rather
     than content, so it is the right 28px to spend. */
  .content { padding-top: 10px; }

  /* The cleared state has to give up the same things the masthead did, and for
     a sharper reason: its content is 224px tall and the short-landscape deck is
     138px, so with `overflow: hidden` the ONLY ACTION ON THE SCREEN — "Pull one
     from your list" — was sitting 55px below the cut, invisible. The heading
     carries the message and the tally carries the count; the eyebrow restates
     the heading and the paragraph is reassurance. Those two go, the action
     stays. Hospitality is what a short screen spends. */
  .now-clear { padding: 10px 20px; }
  .now-clear .now-eyebrow,
  .now-clear > p { display: none; }
  .now-clear h2 { font-size: 22px; margin: 0; }
  .now-clear-tally { font-size: 26px; margin: 2px 0 4px; }

  /* The FAB is positioned inside .now-tabs (a transformed fixed ancestor traps
     fixed descendants), and its offset is derived from the DECK's width — which
     stops being a safe assumption when the window is wider than it is tall.
     Measured 28px past the right edge at 844x390. Pin it to the viewport edge
     here; the deck-relative placement is right everywhere it fits. */
  .now-fab { right: 14px; }
}

/* --- 5. Tables wrap instead of hiding their last column --------------------
   The phone tier set `white-space: nowrap` with `overflow-x: auto`, which is
   the honest version of "it still fits" — nothing is lost, but on /my-tasks it
   put DEADLINE 296px off the right edge behind a horizontal swipe inside a
   vertically scrolling page. That is the one column a task list exists to show,
   and the gesture that reveals it fights the gesture that scrolls the page.

   Letting the prose columns wrap fits the same table in 390px with no scroll at
   all. Dates keep `nowrap` so "Sep 13" never breaks across two lines. The
   horizontal scroll stays available underneath for the genuinely wide tables —
   this removes the need for it, it does not remove the ability. */
@media (max-width: 700px) {
  .table { white-space: normal; }
  .table th, .table td { word-break: normal; overflow-wrap: anywhere; }
  .table th:last-child, .table td:last-child { white-space: nowrap; }
}
