/* ============================================================================
 * CoSi Platform — Design System (production stylesheet)
 * Source of truth: DESIGN.md. Figma mirrors this. Do not add off-system values.
 * Organized: FOUNDATIONS (tokens) -> COMPONENTS. The legacy assets/css/main.css
 * is being replaced page-by-page; do NOT merge this into it.
 *
 * Class conventions: flat kebab, component-prefixed. Root = DESIGN.md §11 name
 * (.applet-card, .dynamic-island, .context-menu, .segmented-control). Element =
 * root-element (.applet-card-name). Variant = compound modifier (.btn-primary,
 * .pill.solid). State = .is-* (.switch.is-on, .field.is-invalid). Category hue =
 * .cat-* modifier (works on pill, applet-card, loved-applet).
 * ============================================================================ */

/* ===== FONTS (self-hosted, #241) ========================================
   Was a render-blocking cross-origin Google Fonts fetch — the font-swap pop on
   first paint was the visible "flash" on nav/island text in Arc. Latin subset;
   Inter is a variable file covering 400–700. */
@font-face{font-family:'Inter'; font-style:normal; font-weight:400 700; font-display:swap; src:url('../fonts/inter-latin.woff2') format('woff2'); unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Space Mono'; font-style:normal; font-weight:400; font-display:swap; src:url('../fonts/space-mono-400-latin.woff2') format('woff2'); unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Space Mono'; font-style:normal; font-weight:700; font-display:swap; src:url('../fonts/space-mono-700-latin.woff2') format('woff2'); unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}

/* ===== FOUNDATIONS ===================================================== */
:root{
  /* Layer 1 — palette primitives (the only raw hex in the system besides theme
     chrome). Each hue carries an -rgb triplet twin for rgb(... / alpha) fills. */
  --blue:#436FFF;       --blue-rgb:67 111 255;
  --teal:#02C2A5;       --teal-rgb:2 194 165;
  --red:#FA3B2B;        --red-rgb:250 59 43;
  --amber:#FBA603;      --amber-rgb:251 166 3;
  --pink:#F95C9B;       --pink-rgb:249 92 155;
  --purple:#7C5CE1;     --purple-rgb:124 92 225;
  --chartreuse:#DEFE24; --chartreuse-rgb:222 254 36;
  --slate:#AEC8E1;      --slate-rgb:174 200 225;
  --gray:#8A8F98;       --gray-rgb:138 143 152;   /* added: was missing its twin */

  /* Layer 2 — semantic aliases (role -> primitive) */
  /* action */
  --accent:var(--blue);   --accent-rgb:var(--blue-rgb);   --accent-hover:#385FE0;
  /* state + data valence — same hues, disambiguated by component + label, never hue alone */
  --success:var(--teal);  --success-rgb:var(--teal-rgb);
  --error:var(--red);     --error-rgb:var(--red-rgb);
  --warning:var(--amber); --warning-rgb:var(--amber-rgb);  /* added twin */
  --info:var(--gray);     --info-rgb:var(--gray-rgb);      /* added twin — neutral, not blue */
  /* taxonomy (category sorting) — unified --cat-* prefix (was split --c-* / --cat-*-rgb) */
  --cat-create:var(--teal);         --cat-create-rgb:var(--teal-rgb);
  --cat-research:var(--pink);       --cat-research-rgb:var(--pink-rgb);
  --cat-strategy:var(--amber);      --cat-strategy-rgb:var(--amber-rgb);
  --cat-testing:var(--purple);      --cat-testing-rgb:var(--purple-rgb);
  --cat-writing:var(--slate);       --cat-writing-rgb:var(--slate-rgb);
  --cat-thinking:var(--chartreuse); --cat-thinking-rgb:var(--chartreuse-rgb);

  /* type scale */
  --text-xs:10px; --text-sm:12px; --text-base:14px; --text-md:16px;
  --text-lg:19px; --text-xl:23px; --text-2xl:32px; --text-3xl:40px; --text-4xl:56px;

  /* spacing */
  --space-1:4px; --space-2:8px; --space-3:12px; --space-4:16px; --space-5:20px;
  --space-6:24px; --space-8:32px; --space-10:40px; --space-12:48px; --space-16:64px;

  /* radius — four named tiers */
  --radius-sm:8px;     /* controls, inputs, small chrome, nav boxes, tooltips */
  --radius-md:10px;    /* menus, dropdowns, popovers, flyouts */
  --radius-lg:20px;    /* cards, widgets, content sections, modals */
  --radius-pill:9999px;/* pills, dynamic island, avatar, badges */

  --shadow-sm:0 1px 2px rgba(0,0,0,.06);
  --shadow-md:0 4px 16px rgba(0,0,0,.10);
  --shadow-lg:0 10px 24px rgba(0,0,0,.14);
  --shadow-overlay:0 25px 50px rgba(0,0,0,.28);

  --t-fast:150ms ease; --t-base:200ms ease; --t-slide:350ms cubic-bezier(.25,.46,.45,.94);
  --ease-spring:340ms cubic-bezier(.34,1.56,.64,1); /* overshoot — native, product-grade */

  /* Where page content + flyouts begin (below the top chrome). One knob for the
     whole site — .app-main and .flyout both read it; nav + island keep the 20px
     viewport gutter above this. */
  --content-top:100px;

  --font-primary:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
  --font-mono:'Space Mono',ui-monospace,'SF Mono',monospace;

  --ink-rgb:38 38 38;

  /* system chrome — surfaces that stay black in BOTH themes (nav, island, context
     menu). Intentional non-theme-flipping values, tokenized rather than bare hex. */
  --chrome-black:#141413;
  --chrome-black-2:#2c2c2e;
  --chrome-text:#e8e8e8;
  --chrome-text-dim:#7d7d7d;
}

/* LIGHT (secondary option) */
[data-theme="light"]{
  --bg:#F7F6F4; --surface:#FFFFFF; --muted:#EFEEEB;
  --border:#DBDBDB; --secondary:#757575; --primary:#262626; --dark:#141413;
  --hairline:rgba(0,0,0,.07);
  --shadow-md:0 4px 16px rgba(0,0,0,.08);
  --card-rest-shadow:var(--shadow-sm);
  --ink-rgb:38 38 38;
  /* nav + dynamic island chrome (2026-07-13: theme-aware by request — was
     pinned always-black; --chrome-* stays pinned black for the context menu,
     which wasn't part of this change). */
  --nav-chrome:#FFFFFF; --nav-chrome-2:#EFEEEB;
  --nav-chrome-text:#262626; --nav-chrome-text-dim:#757575;
}
/* DARK (primary) */
[data-theme="dark"]{
  --bg:#0E0E0E; --surface:#212123; --muted:#2d2d2d;
  --border:#3d3d3d; --secondary:#9a9a9a; --primary:#e8e8e8; --dark:#f5f5f5;
  --hairline:rgba(255,255,255,.06);
  --shadow-md:0 6px 22px rgba(0,0,0,.5);
  /* pixel-identical to the old always-black --chrome-* values (no dark-mode
     regression from this change). */
  --nav-chrome:#141413; --nav-chrome-2:#2c2c2e;
  --nav-chrome-text:#e8e8e8; --nav-chrome-text-dim:#7d7d7d;
  --card-rest-shadow:none;
  --ink-rgb:232 232 232;
}

/* ===== BASE ============================================================
   Minimal app base — converted pages rely on this instead of a page-local
   reset. Added with the app shell (#19). */
*,*::before,*::after{box-sizing:border-box}
html,body{margin:0;padding:0}
body{font-family:var(--font-primary);font-size:var(--text-base);line-height:1.45;background:var(--bg);color:var(--primary);-webkit-font-smoothing:antialiased}
img,svg,video{display:block;max-width:100%}
button,input,select,textarea{font:inherit;color:inherit}
a{color:inherit;text-decoration:none}

/* ===== COMPONENTS ===================================================== */

/* --- Category hue modifiers (cross-component: pill, applet-card, loved-applet) ---
   Sets --cat (solid hue) + --cat-rgb (triplet) so a component tints itself with no
   inline style. Replaces the old inline --h/--hs, --ah/--ahr, --lh schemes. */
.cat-create  { --cat:var(--cat-create);   --cat-rgb:var(--cat-create-rgb); }
.cat-research{ --cat:var(--cat-research); --cat-rgb:var(--cat-research-rgb); }
.cat-strategy{ --cat:var(--cat-strategy); --cat-rgb:var(--cat-strategy-rgb); }
.cat-testing { --cat:var(--cat-testing);  --cat-rgb:var(--cat-testing-rgb); }
.cat-writing { --cat:var(--cat-writing);  --cat-rgb:var(--cat-writing-rgb); }
.cat-thinking{ --cat:var(--cat-thinking); --cat-rgb:var(--cat-thinking-rgb); }
.cat-blue    { --cat:var(--accent);       --cat-rgb:var(--accent-rgb); }  /* selection / "All" */

/* --- Surface card base (shared borderless-at-rest -> hover stroke+shadow) ---
   Was copy-pasted across card/applet/loved/module. Owns bg + transparent border +
   radius + resting shadow. Consumers add size/padding and their hover stroke color. */
.surface-card{
  background:var(--surface);
  border:1px solid transparent;
  border-radius:var(--radius-lg);
  box-shadow:var(--card-rest-shadow);
  transition:box-shadow var(--t-fast), border-color var(--t-fast);
}

/* ==========================================================================
   TEMPLATE UTILITIES — graduated here from the page sheets (#1273)
   ==========================================================================
   These lived in assets/css/pages/*.css, copied into SEVEN sheets: admin,
   agents, applet-run, chat, media, projects, settings. settings.css said what
   should happen to them —

     "ponytail: utilities stay page-local until a second surface needs them
      (then they graduate to design-system.css, not get copied again)"

   — and then six more surfaces needed them and copied them anyway.

   What that cost, and why this is a bug fix rather than tidying: `.text-2xl`
   and `.text-lg` were only ever defined in settings.css. So on /projects,
   `<h1 class="text-2xl font-semibold">` got the font-WEIGHT (which projects.css
   happened to carry) and not the font-SIZE, landing on the browser's default
   h1 of 28px against 32px everywhere else. Measured: Teams 32px/y=100,
   Projects 28px/y=119. Tucker reported it twice — once as a bug (#1252) and
   again as "still slightly off from those".

   The 19px was the same root cause: settings.css also carries the h1 margin
   reset, scoped `.app__content > header h1`, and /projects wraps in
   `.projects-browser` instead. Generalised below.

   Values are unchanged — the seven copies agreed on every one (four differed
   only in whitespace), so nothing restyles. */
.text-xs{font-size:var(--text-xs)}
.text-sm{font-size:var(--text-sm)}
.text-lg{font-size:var(--text-lg)}
.text-2xl{font-size:var(--text-2xl)}
.font-semibold{font-weight:600}
.text-secondary{color:var(--secondary)}
.mt-1{margin-top:var(--space-1)}
.mt-2{margin-top:var(--space-2)}
.mt-3{margin-top:var(--space-3)}
.mt-4{margin-top:var(--space-4)}
.mb-2{margin-bottom:var(--space-2)}
.mb-4{margin-bottom:var(--space-4)}
.flex{display:flex}
.gap-2{gap:var(--space-2)}
.gap-3{gap:var(--space-3)}
.gap-4{gap:var(--space-4)}

/* A page title sits on the --content-top line, whatever wraps it. Was
   `.app__content > header h1` in settings.css, so any page using a different
   wrapper kept the UA h1 margin and sat ~19px low (#1273). */
header > h1{margin:0}

/* --- Buttons (controls use radius-sm) --- */
.btn{display:inline-flex; align-items:center; gap:8px; height:38px; padding:0 18px; border-radius:var(--radius-sm); font:inherit; font-size:var(--text-sm); font-weight:600; border:1px solid transparent; cursor:pointer; transition:background var(--t-fast),border-color var(--t-fast),color var(--t-fast)}
.btn-primary{background:var(--accent); color:#fff}
.btn-primary:hover{background:var(--accent-hover)}
.btn-dark{background:var(--dark); color:var(--bg)}
.btn-dark:hover{filter:brightness(1.12)}
/* Border at rest, not just on hover: --surface fill matches a white card, so on
   one the button was invisible until you hovered it. The same --border the
   .control select uses, so a select + secondary-button pair reads as one control
   group. Hover deepens the stroke (like .btn-outline) to keep the cue. */
.btn-secondary{background:var(--surface); color:var(--primary); border-color:var(--border)}
.btn-secondary:hover{border-color:var(--secondary); background:var(--muted)}
.btn-ghost{background:transparent; color:var(--accent)}
.btn-ghost:hover{background:rgb(var(--accent-rgb)/.12)}
/* Outline: a real action, but not THE action. Where several buttons sit together
   and only one should carry weight, the rest are borders — .btn-secondary fills
   with --surface, which reads as a second filled button, and a bare .btn has no
   background at all so the browser paints its own (illegible on dark). */
.btn-outline{background:transparent; color:var(--primary); border-color:var(--border)}
.btn-outline:hover{border-color:var(--secondary); background:var(--muted)}
/* A disabled button has to LOOK disabled — the same reasoning (and the same
   values) as .switch[disabled] below. Without this a blocked primary action
   renders identically to a live one and only reveals itself on the click that
   does nothing; #704's Push-to-Figma button is the case that found it.
   pointer-events, not :not(:disabled) on four hover rules, because a disabled
   button's click is already inert — it is only the hover paint that lies. */
.btn:disabled{opacity:.45; pointer-events:none}
/* A class-level `display` beats the [hidden] attribute's UA rule, so `btn.hidden
   = true` left the button on screen. Fixed once for .affirm-defer and then hit
   again on Memory's back button — so it belongs on the kit, not on each caller. */
.btn[hidden]{display:none}

/* --- CTA circle (consolidated: was .btn-send + .a-cta + .arrow-cta) ---
   One round arrow button. Default neutral; .accent = send, .cat-fill = category-
   colored applet "use", .ink = dark on light (module cta). Per DESIGN.md rule 4
   (no hover transforms on the element) the arrow nudges via the inner svg, not the
   button — a rule-compliant harmonization of the old element translateX. */
.cta-circle{width:36px; height:36px; border-radius:50%; display:grid; place-items:center; border:none; padding:0; cursor:pointer; background:var(--muted); color:var(--primary); transition:background var(--t-fast),color var(--t-fast)}
.cta-circle svg{transition:transform var(--t-fast)}
.cta-circle:hover svg{transform:translateX(2px)}
.cta-circle.accent{background:var(--accent); color:#fff}
.cta-circle.accent:hover{background:var(--accent-hover)}
.cta-circle.cat-fill{background:var(--cat,var(--accent)); color:#fff}
.cta-circle.ink{background:var(--primary); color:var(--bg)}

/* --- Pills (4 fills x md/sm, dot motif). Hue comes from a .cat-* modifier. --- */
.pill{display:inline-flex; align-items:center; gap:7px; height:30px; padding:0 13px; border-radius:var(--radius-pill); font-size:var(--text-sm); font-weight:500; border:1px solid transparent; cursor:default; white-space:nowrap; transition:background var(--t-fast),border-color var(--t-fast),color var(--t-fast); line-height:1}
.pill.sm{height:24px; padding:0 10px; font-size:11px; gap:6px}
.pill .dot{width:7px; height:7px; border-radius:50%; flex:0 0 auto}
.pill.sm .dot{width:6px; height:6px}
.pill.tonal{background:rgb(var(--cat-rgb)/.15); border-color:rgb(var(--cat-rgb)/.5); color:var(--cat)}
.pill.tonal:hover{background:rgb(var(--cat-rgb)/.22)}
.pill.tonal .dot{background:var(--cat)}
.pill.outline{background:transparent; border-color:rgb(var(--cat-rgb)/.55); color:var(--primary)}
.pill.outline .dot{background:var(--cat)}
.pill.outline:hover{background:rgb(var(--cat-rgb)/.08)}
.pill.solid{background:var(--cat); border-color:transparent; color:#fff}
.pill.solid.on-light{color:#141413}
.pill.solid:hover{filter:brightness(1.06)}
.pill.solid .dot{background:currentColor}
.pill.ghost{background:transparent; border-color:transparent; color:var(--cat)}
.pill.ghost:hover{background:rgb(var(--cat-rgb)/.1)}
.pill.ghost .dot{background:currentColor}
.pill.neutral.tonal{background:rgb(var(--ink-rgb)/.08); border-color:rgb(var(--ink-rgb)/.15); color:var(--primary)}
.pill.neutral.tonal .dot{background:var(--secondary)}
.pill.neutral.outline{border-color:var(--border); color:var(--primary)}
.pill.neutral.outline .dot{background:var(--secondary)}
.pill.neutral.ghost{color:var(--secondary)}
.pill.neutral.ghost .dot{background:var(--secondary)}

/* Accessible visually-hidden (sr-only) — in the base sheet so every page has it
   (it was previously copied into ~6 page stylesheets; design-system pages lacked it). */
.visually-hidden{position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0}

/* --- Segmented control (sliding blue indicator), tab bar, filter bar, divider --- */
.segmented-control{position:relative; display:inline-flex; padding:4px; background:var(--muted); border-radius:var(--radius-pill)}
.segmented-control-indicator{position:absolute; left:0; top:4px; bottom:4px; border-radius:var(--radius-pill); background:var(--accent); transition:transform var(--ease-spring), width var(--ease-spring); z-index:0}
.segmented-control-item{position:relative; z-index:1; height:30px; padding:0 16px; display:inline-flex; align-items:center; justify-content:center; background:none; border:0; -webkit-appearance:none; appearance:none; font:inherit; font-size:var(--text-sm); font-weight:500; color:var(--secondary); cursor:pointer; white-space:nowrap; transition:color var(--t-base)}
.segmented-control-item.is-active{color:#fff}
/* Narrow viewports: the control can't wrap (the sliding indicator is absolutely
   positioned against one row), so it scrolls — the same treatment .applets-filters
   already uses for its pill row. Without this the last tabs are simply unreachable
   on a phone (#110). */
@media (max-width:768px){
  .segmented-control{max-width:100%; overflow-x:auto; scrollbar-width:none}
  .segmented-control::-webkit-scrollbar{display:none}
}
.tab-bar{display:flex; gap:4px; align-items:center; flex-wrap:wrap}
.tab-bar.filter-bar{gap:10px}
.tab-bar.filter-bar .pill{padding:0 24px}
.divider-vertical{width:2px; height:22px; background:var(--secondary); opacity:.4; margin:0 6px; border-radius:2px}

/* --- Switch (iOS) + toggle button — product-grade spring motion --- */
.switch{position:relative; width:44px; height:26px; border-radius:9999px; background:var(--muted); border:1px solid var(--hairline); cursor:pointer; flex:0 0 auto; transition:background var(--t-base),border-color var(--t-base)}
.switch::after{content:""; position:absolute; top:2px; left:2px; width:20px; height:20px; border-radius:50%; background:#fff; box-shadow:0 1px 3px rgba(0,0,0,.35); transition:transform var(--ease-spring)}
.switch.is-on{background:var(--accent); border-color:transparent}
.switch.is-on::after{transform:translateX(18px)}
/* A locked switch has to LOOK locked — an on-state you can't change is otherwise
   indistinguishable from one you can, and users click it repeatedly (#417). */
.switch[disabled]{opacity:.45; cursor:not-allowed}

/* Search field — pill input with a leading icon. Was page-local to applets.php
   until #404 needed it too; promoted here rather than copied a second time.
   Markup: <div class="search-field"><svg…/><input class="control"></div> */
.search-field{position:relative; flex:1; max-width:560px}
.search-field .control{width:100%; height:48px; padding-left:42px; border-radius:var(--radius-pill)}
.search-field svg{position:absolute; left:16px; top:50%; transform:translateY(-50%); color:var(--secondary); pointer-events:none}
.switch-row{display:flex; align-items:center; gap:12px; padding:9px 0; font-size:var(--text-sm); cursor:pointer; user-select:none}
.toggle-button{width:38px; height:38px; border-radius:var(--radius-sm); background:var(--surface); color:var(--secondary); display:grid; place-items:center; cursor:pointer; transition:background var(--t-fast),color var(--t-fast)}
.toggle-button:hover{background:var(--muted)}
.toggle-button.is-on{background:rgb(var(--accent-rgb)/.15); color:var(--accent)}
.toggle-button.is-on svg{animation:pop var(--ease-spring)}
@keyframes pop{0%{transform:scale(.85)}55%{transform:scale(1.18)}100%{transform:scale(1)}}

/* --- Form field (the one element that keeps a resting border) --- */
.field{max-width:380px; margin-bottom:18px}
.field label{display:block; font-size:var(--text-sm); font-weight:600; margin-bottom:7px}
/* Bare form controls read as dark-theme controls BY DEFAULT (Tucker 2026-07-15):
   a text input / textarea / select that forgets the .control/.input class is no
   longer a white browser box — the recurring "why is this input white" bug is
   solved globally at the element level, so new UI can't reintroduce it. Low
   element-specificity, so .control/.input (and any component rule) still win
   where used; native checkbox/radio ignore bg/border, range/number keep their
   dedicated rules below. */
input, textarea, select{background:var(--surface); color:var(--primary); border:1px solid var(--border); border-radius:var(--radius-sm); padding:8px 12px}
textarea{padding:10px 12px}
input[type=checkbox], input[type=radio], input[type=range]{padding:0}
input:focus, textarea:focus, select:focus{outline:none; border-color:var(--accent)}
input::placeholder, textarea::placeholder{color:var(--secondary)}
/* .control / .input layer the full sizing (width, height, focus ring) on top. */
.control, .input{width:100%; height:38px; padding:0 12px; background:var(--surface); color:var(--primary); border:1px solid var(--border); border-radius:var(--radius-sm); font:inherit; font-size:var(--text-base); transition:border-color var(--t-fast),box-shadow var(--t-fast)}
textarea.control, textarea.input{height:auto; padding:10px 12px; resize:vertical; min-height:84px}
.control:focus, .input:focus{outline:none; border-color:var(--accent); box-shadow:0 0 0 3px rgb(var(--accent-rgb)/.25)}
.control::placeholder, .input::placeholder{color:var(--secondary)}
.field .help{font-size:11px; color:var(--secondary); margin-top:6px}
.field.is-invalid .control, .field.is-invalid .input{border-color:var(--error)}
.field.is-invalid .help{color:var(--error)}

/* --- Card (unified content card) --- */
.card{padding:24px}
.card.clickable{cursor:pointer}
.card.clickable:hover{box-shadow:var(--shadow-md); border-color:var(--border)}
.card-title{font-weight:600; font-size:var(--text-base)}
.card-description{color:var(--secondary); font-size:var(--text-sm); margin-top:6px; line-height:1.5}
.card-cta{display:inline-flex; align-items:center; gap:6px; margin-top:16px; height:30px; padding:0 12px; border-radius:var(--radius-pill); background:rgb(var(--accent-rgb)/.15); color:var(--accent); font-size:var(--text-sm); font-weight:600; cursor:pointer; border:1px solid rgb(var(--accent-rgb)/.32); transition:background var(--t-fast)}
.card-cta:hover{background:rgb(var(--accent-rgb)/.24)}

/* --- Applet library card — rest face is the HEADLINE ONLY (vertically
   centered, left-aligned; no icon, no category label — Tucker 2026-07-06).
   Hover: category stroke + name cross-fades to description + actions; icons
   live only on the reveal side. Category hue via .cat-* on the root. --- */
.applet-card{position:relative; width:200px; height:200px; overflow:hidden; cursor:pointer; transition:box-shadow var(--t-base)}
/* hover: thick FADED category stroke, inset (no layout shift), inside the radius */
.applet-card:hover{box-shadow:inset 0 0 0 6px rgb(var(--cat-rgb, var(--gray-rgb)) / .25)}
.applet-card-rest, .applet-card-reveal{position:absolute; inset:18px; display:flex; flex-direction:column; transition:opacity 160ms ease}
.applet-card-rest{justify-content:center}
.applet-card-name{font-weight:600; font-size:var(--text-md); line-height:1.3}
.applet-card-reveal{opacity:0; transition:opacity 160ms ease 40ms}
.applet-card:hover .applet-card-rest{opacity:0}
.applet-card:hover .applet-card-reveal{opacity:1}
.applet-card-description{font-size:var(--text-sm); color:var(--primary); line-height:1.45; display:-webkit-box; -webkit-line-clamp:5; -webkit-box-orient:vertical; overflow:hidden}
.applet-card-actions{margin-top:auto; display:flex; align-items:center; justify-content:flex-end; gap:12px}
.applet-card-action{display:grid; place-items:center; color:var(--secondary); cursor:pointer; transition:color var(--t-fast); background:none; border:none; padding:0}
.applet-card-action:hover{color:var(--primary)}

/* --- Loved applet — small square, two-tone name, hover = stroke only --- */
.loved-applet{width:128px; height:104px; padding:16px; display:flex; align-items:flex-start; cursor:pointer; transition:box-shadow var(--t-base); overflow:hidden}
.loved-applet:hover{box-shadow:inset 0 0 0 6px rgb(var(--cat-rgb, var(--gray-rgb)) / .25)}
.loved-applet-name{font-size:var(--text-base); font-weight:600; line-height:1.22}
.loved-applet-name .dim{color:var(--secondary)}

/* --- Menu / dropdown (radius-md, theme-aware) --- */
.menu{width:230px; background:var(--surface); color:var(--primary); border-radius:var(--radius-md); padding:8px; box-shadow:var(--shadow-overlay)}
.menu-header{padding:10px 12px 12px}
.menu-name{font-size:var(--text-base); font-weight:600}
.menu-role{font-size:11px; color:var(--secondary); margin-top:2px}
.menu-group{padding:6px 0; border-top:1px solid var(--hairline)}
.menu-item{display:flex; align-items:center; gap:10px; padding:8px 12px; border-radius:var(--radius-sm); font-size:var(--text-sm); cursor:pointer; color:var(--primary); transition:background var(--t-fast); background:none; border:none; width:100%; text-align:left}
.menu-item:hover{background:var(--muted)}
/* Third time for this exact trap — see the note above .btn[hidden]. The author
   display:flex above beats the UA sheet's [hidden]{display:none}, so #953 retired
   the Affirmations row by marking it `hidden` and it stayed on screen for four
   days: the account menu showed Memory AND Affirmations, which is the one thing
   that commit existed to stop. The badge vanished correctly because
   .menu-count[hidden] was already here, so it read as an ordinary empty row. */
.menu-item[hidden]{display:none}

/* --- Context menu — horizontal, right-click, always dark (system chrome) --- */
.context-menu{display:inline-flex; align-items:center; gap:2px; background:var(--chrome-black); border-radius:var(--radius-md); padding:6px; box-shadow:var(--shadow-overlay); width:fit-content}
.context-menu-item{display:inline-flex; align-items:center; gap:9px; height:32px; padding:0 14px; border-radius:var(--radius-sm); color:var(--chrome-text); font-size:var(--text-sm); font-weight:500; cursor:pointer; transition:background var(--t-fast); white-space:nowrap}
.context-menu-item:hover{background:rgba(255,255,255,.09)}
.context-menu-item.is-primary::before{content:""; width:13px; height:13px; border-radius:50%; border:2px solid var(--accent); flex:0 0 auto}
.context-menu-divider{width:1px; height:18px; background:rgba(255,255,255,.16); margin:0 4px; flex:0 0 auto}

/* --- Dynamic island — THEME-AWARE: --nav-chrome is white in light and near-black
   in dark (#1080 — the old "always black in both themes" line here was wrong).
   Pill radius, 380px on desktop, fluid on phones (#110). Island pills are
   island-scoped (NOT the theme-aware .pill) because they sit on the nav-chrome
   surface, not on the page. --- */
.dynamic-island{position:relative; display:inline-flex; align-items:center; gap:12px; height:48px; width:380px; max-width:380px; padding:0 8px 0 18px; border-radius:var(--radius-pill); background:var(--nav-chrome); color:var(--nav-chrome-text); box-shadow:var(--shadow-md)}
.dynamic-island-dot{width:8px; height:8px; border-radius:50%; background:var(--accent); flex:0 0 auto}
.dynamic-island-label{flex:1 1 auto; min-width:0; overflow:hidden}
.dynamic-island-label-inner{display:inline-block; white-space:nowrap; font-size:var(--text-sm); font-weight:500}
.dynamic-island.ticker .dynamic-island-label{-webkit-mask-image:linear-gradient(to right,#000 80%,transparent 100%); mask-image:linear-gradient(to right,#000 80%,transparent 100%)}
.dynamic-island.ticker .dynamic-island-label-inner{transition:transform 3.8s linear}
.dynamic-island.ticker:hover .dynamic-island-label-inner,.dynamic-island.ticker.cue .dynamic-island-label-inner{transform:translateX(var(--shift,0))}
/* .ticker/.cue/--shift are driven by the island ticker block in assets/js/shell.js
   (#1081). They shipped here with the showcase and sat unused for months — nothing
   applied either class or set --shift — so a long label just ended in a hard clip
   mid-word, with the mask gated behind .ticker and therefore also absent. */
@media (prefers-reduced-motion: reduce){
  /* Never animate — but keep the mask above, and shell.js keeps the full string
     on the label's title so it stays reachable. Same shape as the
     .dynamic-island-progress.indeterminate rule below. */
  .dynamic-island.ticker .dynamic-island-label-inner{transition:none}
  .dynamic-island.ticker:hover .dynamic-island-label-inner,.dynamic-island.ticker.cue .dynamic-island-label-inner{transform:none}
}
.dynamic-island-meta{margin-left:auto; display:flex; align-items:center; gap:10px; flex:0 0 auto}
.dynamic-island-progress{width:54px; height:4px; border-radius:9999px; background:rgb(var(--ink-rgb)/.18); overflow:hidden}
.dynamic-island-progress i{display:block; height:100%; width:62%; background:var(--accent)}
/* Indeterminate: a background task is running but reports no percentage (#399). */
.dynamic-island-progress.indeterminate i{width:38%; animation:island-indeterminate 1.5s ease-in-out infinite}
@keyframes island-indeterminate{from{transform:translateX(-100%)} to{transform:translateX(264%)}}
@media (prefers-reduced-motion: reduce){.dynamic-island-progress.indeterminate i{width:100%; animation:none; opacity:.55}}
.dynamic-island-timer{font-family:var(--font-mono); font-size:12px; color:var(--cat-create)}
.dynamic-island-soft{font-family:var(--font-mono); font-size:11px; color:rgb(var(--ink-rgb)/.5)}
.dynamic-island-pill{height:22px; padding:0 11px; border-radius:9999px; font-size:11px; font-weight:600; display:inline-flex; align-items:center}
/* The pill is an <a> for navigation and a <button> for an action (Clear). Reset
   the UA button chrome so both render identically; .solid/.outline are (0,2,0)
   so they still win over this (0,1,1) rule and keep their own background. */
button.dynamic-island-pill{background:none; border:0; font-family:inherit; cursor:pointer}
.dynamic-island-pill.solid{background:var(--accent); color:#fff}
.dynamic-island-pill.outline{border:1px solid rgba(var(--accent-rgb)/.7); color:#6f90ff}
.dynamic-island-avatar{width:34px; height:34px; border-radius:50%; background:var(--nav-chrome-2); display:grid; place-items:center; color:var(--nav-chrome-text-dim); flex:0 0 auto; border:none; padding:0; cursor:pointer; overflow:hidden; font-size:12px; font-weight:600}
.dynamic-island-avatar img{width:100%; height:100%; object-fit:cover}
.dynamic-island-avatar:hover{color:#fff}
/* Unread Activity jewel (#467). Lives on the AVATAR, not in the island label —
   the island is a transient "now" surface that tasks.js and the meeting nudge
   both rewrite on every poll, so a count in there would be clobbered and would
   also compete for the island's single slot. The avatar is persistent. */
.island-avatar-wrap{position:relative; display:inline-flex; flex:0 0 auto}
/* The nudge link covers the pill INSTEAD of wrapping it (#853) — wrapping made
   the account-menu button a descendant of an <a>, so clicking your own avatar
   followed the link. .dynamic-island-meta is lifted above the overlay so the
   avatar and any action pill stay clickable. */
/* The stack is 1 (overlay) < 2 (meta), NOT 0 < 1 (#1081). Wiring the ticker put
   a mask and a transform on the label, and both create a stacking context — so a
   z-index:auto label painted OVER a z-index:0 overlay (it comes later in the DOM)
   and swallowed every click on the nudge. Playwright caught it as "label-inner …
   intercepts pointer events". At z-index:1 the overlay clears the label; meta
   stays above the overlay, which is the whole point of #853. */
.dynamic-island-hit{position:absolute; inset:0; border-radius:inherit; z-index:1}
.dynamic-island-meta{position:relative; z-index:2}
.island-jewel{position:absolute; top:-5px; right:-6px; min-width:15px; height:15px; padding:0 3.5px;
  border-radius:9999px; background:var(--accent); color:#fff; font-size:9px; font-weight:700;
  line-height:15px; text-align:center; pointer-events:none;
  /* Sits OUTSIDE the avatar's edge rather than over the face — nudged up/right
     so it clears the portrait (Tucker). The ring is the island's own chrome, so
     it reads as attached to the avatar instead of floating behind it. */
  box-shadow:0 0 0 2px var(--nav-chrome)}
.island-jewel[hidden]{display:none}
/* Count beside the Activity row in the avatar menu. */
.menu-item--count{display:flex; align-items:center; justify-content:space-between; gap:var(--space-3)}
.menu-count{min-width:18px; height:18px; padding:0 5px; border-radius:9999px; background:var(--accent);
  color:#fff; font-size:10px; font-weight:700; line-height:18px; text-align:center}
.menu-count[hidden]{display:none}
/* Shortcuts reference (#468) */
.shortcuts-modal{width:min(420px, calc(100vw - 40px)); padding:var(--space-6); display:flex; flex-direction:column; gap:var(--space-3)}
.shortcuts-list{display:flex; flex-direction:column; gap:2px; margin:var(--space-3) 0 0}
.shortcuts-list > div{display:flex; align-items:center; gap:var(--space-4); padding:var(--space-2) 0}
.shortcuts-list dt{flex:0 0 96px; display:flex; gap:4px; align-items:center}
.shortcuts-list dd{margin:0; font-size:var(--text-sm); color:var(--secondary)}
kbd{display:inline-block; min-width:20px; padding:2px 6px; border-radius:var(--radius-sm);
  background:var(--muted); border:1px solid var(--hairline); color:var(--primary);
  font-family:var(--font-mono); font-size:11px; line-height:16px; text-align:center}
/* Activity rows. Generic on purpose — Bookmarks (#469) reuses this list. */
.activity-item{display:flex; gap:var(--space-3); align-items:flex-start; padding:var(--space-3) var(--space-2);
  border-radius:var(--radius-sm); color:inherit; text-decoration:none; transition:background var(--t-fast)}
a.activity-item:hover{background:var(--muted)}
.activity-item__dot{width:7px; height:7px; margin-top:6px; border-radius:50%; background:transparent; flex:0 0 auto}
.activity-item.is-unread .activity-item__dot{background:var(--accent)}
.activity-item__main{display:flex; flex-direction:column; gap:2px; min-width:0}
.activity-item__title{font-size:var(--text-sm); color:var(--secondary)}
.activity-item.is-unread .activity-item__title{color:var(--primary); font-weight:600}
.activity-item__body{font-size:var(--text-xs); color:var(--secondary)}
.activity-item__meta{font-size:var(--text-xs); color:var(--tertiary,var(--secondary)); opacity:.75}
/* Fly-out tabs (Bookmarks #469; generic so any panel can use them). */
.flyout-tabs{flex:0 0 auto; display:flex; gap:var(--space-1); padding:0 var(--space-5) var(--space-2)}
.flyout-tab{background:none; border:0; padding:var(--space-2) var(--space-3); border-radius:var(--radius-sm);
  font-family:inherit; font-size:var(--text-xs); color:var(--secondary); cursor:pointer; white-space:nowrap;
  transition:background var(--t-fast), color var(--t-fast)}
.flyout-tab:hover{background:var(--muted)}
.flyout-tab.is-active{background:var(--muted); color:var(--primary); font-weight:600}
/* A bookmark row: the title navigates, the actions don't. */
.bookmark-item{display:flex; gap:var(--space-2); align-items:flex-start; padding:var(--space-3) var(--space-2);
  border-radius:var(--radius-sm)}
.bookmark-item:hover{background:var(--muted)}
.bookmark-item__link{flex:1; min-width:0; color:inherit; text-decoration:none; display:flex; flex-direction:column; gap:2px}
.bookmark-item__title{font-size:var(--text-sm); color:var(--primary)}
.bookmark-item.is-done .bookmark-item__title{color:var(--secondary); text-decoration:line-through}
.bookmark-item__meta{font-size:var(--text-xs); color:var(--secondary); opacity:.75}
.bookmark-item__actions{display:flex; gap:2px; flex:0 0 auto; opacity:0; transition:opacity var(--t-fast)}
.bookmark-item:hover .bookmark-item__actions, .bookmark-item__actions:focus-within{opacity:1}
.bookmark-act{width:26px; height:26px; display:grid; place-items:center; background:none; border:0;
  border-radius:var(--radius-sm); color:var(--secondary); cursor:pointer; transition:background var(--t-fast), color var(--t-fast)}
.bookmark-act:hover{background:var(--surface); color:var(--primary)}
/* Reminder presets. Above .flyout (300) since it's opened from inside one. */
.bm-remind-menu{width:210px}

/* Shell-level island (#46): fixed top-right on every authed page, carrying the
   status/nudge + the account avatar & menu. */
.app-island{position:fixed; top:20px; right:20px; z-index:350} /* above .flyout (300) so the avatar menu opens in front of an open flyout, below modals (400) */
/* Something just landed. Replaces the completion toast — one surface, not two.
   Glow only (no transform), per the DESIGN.md no-hover-transform rule; a moving
   island would also shift the avatar under the cursor. */
@keyframes island-pulse{
    0%{box-shadow:0 0 0 0 rgba(var(--accent-rgb)/0)}
    18%{box-shadow:0 0 22px 5px rgba(var(--accent-rgb)/.55)}
    100%{box-shadow:0 0 0 0 rgba(var(--accent-rgb)/0)}
}
.app-island.is-pulsing .dynamic-island{animation:island-pulse 3s ease-out 1}
@media (prefers-reduced-motion:reduce){.app-island.is-pulsing .dynamic-island{animation:none}}
.app-island-menu{position:absolute; top:calc(100% + 10px); right:0; display:none; z-index:250}
.app-island-menu.is-open{display:block}
/* PHONES (#110 → #1090, Tucker on staging 2026-09-02): the island is a DOCK —
   full width, flush to the bottom edge, top corners on the sheet's radius,
   painted in the sheet's surface — because the sheet IS this surface grown
   taller. One object; no pill-to-panel shape change mid-swipe. NavSheet (shell.js)
   sets --dock-h (row + safe area) on :root and --island-lift while the sheet is
   open. While the summon bar is up the dock stays put and the composer rises
   out of its top edge, so nothing overlaps. Desktop keeps the nav-chrome pill. */
@media (max-width:768px){
  .app-island{top:auto; left:0; right:0; bottom:0; padding-bottom:env(safe-area-inset-bottom, 0px);
    background:transparent; transition:transform var(--t-base)}
  /* Two layers behind the row, both inside the dock's own stacking context:
     ::after is the SURFACE — rounded, stroked, shadowed; ::before is the page
     colour, running from 96px above the dock all the way down behind it. The
     surface sits on the fade, so the wedges outside the rounded corners are page
     colour, not the last content card showing through — that wedge was the
     "sharp bit" Tucker circled (the fade used to stop at the dock's top edge).
     Weight (Tucker, same note): 72px row, a 1px --border stroke, a heavier fade. */
  .app-island::after{content:""; position:absolute; inset:0; z-index:-1; pointer-events:none;
    background:var(--surface); border:1px solid var(--border); border-bottom:0; border-radius:20px 20px 0 0;
    box-shadow:0 -12px 32px rgba(0,0,0,.10); transition:border-radius var(--t-base), box-shadow var(--t-base), border-color var(--t-base)}
  .app-island::before{content:""; position:absolute; left:0; right:0; top:-96px; bottom:0; z-index:-2; pointer-events:none;
    background:linear-gradient(to top, var(--bg) calc(100% - 96px), transparent)}
  /* 88 tall with the extra 16 ABOVE the row (Tucker, #1102: "more visual
     prominence to the dock" — the margin he asked for was inside it). */
  .app-island .dynamic-island{width:100%; max-width:100%; height:88px; padding:16px 14px 0 20px; border-radius:0; background:transparent; color:var(--primary); box-shadow:none}
  /* Never smaller than the page's body copy (16px on a phone, #1091), and an
     avatar that is also a 44pt touch target. */
  .app-island .dynamic-island-label-inner{font-size:var(--text-md)}
  .app-island .dynamic-island-avatar{width:44px; height:44px; font-size:14px; background:var(--muted); color:var(--secondary)}
  .app-island .dynamic-island-soft, .app-island .dynamic-island-timer{color:var(--secondary)}
  /* The fade hides whenever something is sitting on the dock — the nav sheet,
     the summon bar, or ANY fly-out sheet (#1084): the dock is z 350 and the fade
     lives in its stacking context, so left on it painted page colour over the
     bottom 96px of every sheet and washed out their last row (#1109 follow-up). */
  .app-island.is-sheet::before, body.summon-open .app-island::before, body.flyout-open .app-island::before{opacity:0}
  /* (The chat page's own case — its composer sits on the dock permanently, so
     the fade has nothing to fade — is pages/chat.css's rule, #1309 part 2.) */
  /* Open: the dock is the top of the sheet. No shadow — it is one surface now. */
  .app-island.is-sheet{transform:translateY(calc(-1 * var(--island-lift, 0px)))}
  .app-island.is-sheet::after{box-shadow:none}
  .app-island.is-dragging{transition:none}
  /* While the dock is the sheet's handle the nudge's overlay link must be inert
     (#853 in a new spot): a tap on the header can never navigate. The action pill
     and the avatar inside .dynamic-island-meta stay live. */
  .app-island.is-sheet .dynamic-island-hit{pointer-events:none}
  .app-island .dynamic-island{touch-action:none}
  /* Viewing as someone: the ring is the ambient cue; the island's idle label and
     the sheet's pinned exit pill carry the words. Never colour alone. */
  .app-island[data-viewing-as] .dynamic-island-avatar{box-shadow:0 0 0 2.5px var(--warning)}
  /* Summon bar up: the dock squares off and becomes the base the composer sits on. */
  body.summon-open .app-island::after{border-radius:0; box-shadow:none; border-top-color:transparent}
  /* The desktop dropdown never opens on a phone — the menu node itself is moved
     into the sheet's account slot by shell.js. */
  .app-island-menu.is-open{display:none}
}

/* Admin view-as indicator (#768). Its own chrome rather than a mode on the
   island: tasks.js owns the island outright while a job runs, and an
   impersonation warning that disappears for a minute is how an admin forgets.
   Bottom-left is the one corner nothing else claims — the rail is top-left, the
   island top-right, the chat FAB bottom-right. Fixed dark ink on the amber, the
   same way .dynamic-island-pill.solid pins #fff on the accent: the fill does not
   follow the theme, so the text on it must not either. */
.view-as-bar{position:fixed; left:110px; bottom:24px; z-index:360;
  display:flex; align-items:center; gap:12px; height:44px; padding:0 8px 0 16px;
  border-radius:var(--radius-pill); background:var(--warning); color:#141413;
  box-shadow:var(--shadow-md); font-size:var(--text-sm); font-weight:600}
.view-as-bar-dot{width:8px; height:8px; border-radius:50%; background:#141413; flex:0 0 auto}
.view-as-bar .btn{height:30px; padding:0 14px; background:rgba(0,0,0,.16); color:inherit}
.view-as-bar .btn:hover{background:rgba(0,0,0,.28)}
/* Above the chat FAB (bottom:32px + 56px tall), which is the only thing it
   could collide with once the left gutter collapses. */
@media (max-width:768px){
  .view-as-bar{left:12px; right:12px; bottom:100px}
  /* The viewing-as bar is replaced on phones by the island ring + label and the
     sheet's pinned exit pill (#110). Desktop keeps the bar — do not unify. The
     test-drive bar (#769) keeps its place on every viewport. */
  .view-as-bar--viewing{display:none}
}

/* Chat summon — a bottom-right FAB + a hidden omni bar any page can raise via the
   FAB or "/" (shell-level, every authed page except /chat). Sends deep-link to /chat. */
.chat-fab{position:fixed; right:32px; bottom:32px; width:56px; height:56px; border-radius:50%; background:var(--accent); color:#fff; border:none; display:grid; place-items:center; cursor:pointer; box-shadow:var(--shadow-md); z-index:180; transition:transform var(--t-base), opacity var(--t-base)}
.chat-fab:hover{transform:translateY(-2px)}
.chat-fab.is-hidden{opacity:0; pointer-events:none; transform:scale(.8)}
.chat-summon-bar{position:fixed; left:110px; right:0; bottom:0; padding:0 48px 28px; z-index:181; background:linear-gradient(to top, var(--bg) 40%, transparent); transition:transform var(--t-base), opacity var(--t-base)}
.chat-summon-bar .chat-bar{max-width:760px; margin:0 auto}
.chat-summon-bar.is-collapsed{transform:translateY(130%); opacity:0; pointer-events:none}
/* Phones (#110): no FAB — Quick chat is a pill in the nav sheet's head and hands
   off to this bar; the bar spans the full edge (the rail is gone below 768). */
@media (max-width:768px){
  .chat-fab{display:none}
  /* The composer is part of the dock (#1090): same surface, same corners, sitting
     on the dock's top edge (--dock-h is the dock row + safe area). It rises from
     behind the dock — the dock's z-index is higher — so it reads as the dock
     growing, not a second thing sliding over it. */
  .chat-summon-bar{left:0; right:0; bottom:var(--dock-h, 64px); padding:8px 8px 0; background:var(--surface); border-radius:20px 20px 0 0;
    box-shadow:0 -1px 0 var(--hairline), 0 -8px 24px rgba(0,0,0,.06)}
}

/* --- Scrollbars — thin and quiet in both themes; the hover only deepens the
   same ink tint (never the browser's white default). --- */
*{scrollbar-width:thin; scrollbar-color:rgb(var(--ink-rgb)/.22) transparent}
::-webkit-scrollbar{width:8px; height:8px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:rgb(var(--ink-rgb)/.2); border-radius:9999px}
::-webkit-scrollbar-thumb:hover,::-webkit-scrollbar-thumb:active{background:rgb(var(--ink-rgb)/.38)}
::-webkit-scrollbar-corner{background:transparent}

/* --- Flow layout (DESIGN.md §8: reflow is animated, globally) — pairs with
   assets/js/flow-layout.js (ported from design-system/index.html). Items are
   transform-positioned and SLIDE to new spots on re-pack; masonry (shortest-
   column) for the dashboard, row-major grid for uniform card grids. --- */
.reflow-container{position:relative}
.reflow-container > .reflow-item{position:absolute; top:0; left:0; transition:transform .48s cubic-bezier(.2,.8,.2,1), width .48s cubic-bezier(.2,.8,.2,1)}
@media (prefers-reduced-motion:reduce){ .reflow-container > .reflow-item{transition:none} }

/* --- Skeleton loading state — a shimmer bar standing in for content still in
   flight (async widget fills, LLM-backed summaries). Compose from --skeleton-*
   line classes; swap innerHTML for real content once the fetch resolves. --- */
.skeleton-lines{display:flex; flex-direction:column; gap:10px}
.skeleton-line{height:13px; border-radius:4px; background:linear-gradient(100deg, var(--muted) 40%, rgb(var(--ink-rgb)/.14) 50%, var(--muted) 60%); background-size:250% 100%; animation:skeleton-shimmer 1.6s ease-in-out infinite}
.skeleton-line.sm{height:10px}
.skeleton-line.lg{height:20px}
.skeleton-line.w-60{width:60%} .skeleton-line.w-75{width:75%} .skeleton-line.w-90{width:90%} .skeleton-line.w-40{width:40%}
@keyframes skeleton-shimmer{0%{background-position:200% 0} 100%{background-position:-200% 0}}
@media (prefers-reduced-motion:reduce){.skeleton-line{animation:none; background:var(--muted)}}

/* --- Stat tile — big number + label (Asana widget, pre-header metrics).
   Sits ON a surface card, so it uses the page bg for contrast. --- */
.stat-tile{display:flex; flex-direction:column; gap:2px; padding:14px 16px; background:var(--bg); border-radius:var(--radius-md); min-width:104px}
.stat-tile-value{font-size:var(--text-2xl); font-weight:700; line-height:1.1; color:var(--primary); font-variant-numeric:tabular-nums}
.stat-tile-label{font-size:var(--text-sm); color:var(--secondary)}

/* --- Report block (bullets + toggleable sections) --- */
.report{max-width:640px; border-radius:var(--radius-lg); background:var(--surface); overflow:hidden; box-shadow:var(--card-rest-shadow)}
.report-section{border-bottom:1px solid var(--hairline)}
.report-section:last-child{border-bottom:none}
.report-section-header{display:flex; align-items:center; gap:10px; padding:15px 20px; cursor:pointer; user-select:none}
.report-section-header:hover{background:var(--muted)}
.report-section-header .report-chevron{margin-left:auto; color:var(--secondary); transition:transform var(--t-fast)}
.report-section.is-open .report-chevron{transform:rotate(90deg)}
.report-section-header h5{margin:0; font-size:var(--text-base); font-weight:600}
.report-section-body{padding:0 20px 18px; display:none}
.report-section.is-open .report-section-body{display:block}
.report-section-body p{color:var(--secondary); font-size:var(--text-sm); margin:0 0 12px}
.report-list{list-style:none; padding:0; margin:0}
.report-list li{position:relative; padding:6px 0 6px 20px; font-size:var(--text-base); line-height:1.5}
.report-list li::before{content:""; position:absolute; left:2px; top:13px; width:6px; height:6px; border-radius:50%; background:var(--accent)}

/* --- Pipeline stage (state feedback) + metric row (data valence).
   Split from the old overloaded .stage — same hues, two distinct components. --- */
.pipeline-stage{display:flex; align-items:center; gap:12px; padding:13px 16px; border-radius:var(--radius-md); background:var(--surface); margin-bottom:10px; max-width:440px; box-shadow:var(--card-rest-shadow)}
.pipeline-stage-icon{width:20px; height:20px; border-radius:50%; display:grid; place-items:center; flex:0 0 auto; color:#fff; font-size:12px; font-weight:700}
.pipeline-stage.is-done .pipeline-stage-icon{background:var(--success)}
.pipeline-stage.is-failed .pipeline-stage-icon{background:var(--error)}
.pipeline-stage.is-running .pipeline-stage-icon{background:var(--accent)}
.pipeline-stage-name{font-size:var(--text-base); font-weight:500}
.pipeline-stage-status{margin-left:auto; font-size:var(--text-sm)}
.pipeline-stage.is-done .pipeline-stage-status{color:var(--success)}
.pipeline-stage.is-failed .pipeline-stage-status{color:var(--error)}
.pipeline-stage.is-running .pipeline-stage-status{color:var(--accent)}
.metric-row{display:flex; align-items:center; gap:12px; padding:13px 16px; border-radius:var(--radius-md); background:var(--surface); margin-bottom:10px; max-width:440px; box-shadow:var(--card-rest-shadow)}
.metric-dot{width:12px; height:12px; border-radius:50%; flex:0 0 auto; background:var(--secondary)}
.metric-dot.is-positive{background:var(--success)}
.metric-dot.is-negative{background:var(--error)}
.metric-name{font-size:var(--text-base); font-weight:500}
.metric-value{margin-left:auto; font-size:var(--text-sm); color:var(--secondary)}

/* --- Navigation — the CoSi Bar. A floating, fully-rounded pill (always-black
   chrome): iridescent Smile logo at top, blue-circle selector on the active
   item, a divider between tool groups. Reboot design — see DESIGN.md §11.3. --- */
.nav-item{width:32px; height:32px; border-radius:var(--radius-pill); display:grid; place-items:center; background:transparent; color:var(--secondary); cursor:pointer; transition:color var(--t-fast),background var(--t-fast)}
.nav-item:hover{color:var(--primary); background:var(--muted)}
.nav-item.is-active{background:var(--accent); color:#fff}
.sidebar-nav{display:inline-flex; flex-direction:column; align-items:center; gap:30px; width:fit-content; padding:15px; background:var(--nav-chrome); border-radius:var(--radius-pill); box-shadow:var(--shadow-md)}
.sidebar-nav-group{display:flex; flex-direction:column; align-items:center; gap:8px}
.sidebar-nav .nav-item{color:var(--nav-chrome-text-dim)}
.sidebar-nav .nav-item:hover{color:var(--nav-chrome-text); background:rgb(var(--ink-rgb)/.08)}
.sidebar-nav .nav-item.is-active{background:var(--accent); color:#fff}
/* Cross-document View Transitions (Tucker 2026-07-17): the browser holds the old
   frame until the next page is ready — kills the white/blank flash on every
   navigation — and the shared name below makes the active nav pill SLIDE to its
   new position instead of blinking. CSS-only progressive enhancement (Chrome
   126+/Safari 18.2+); other browsers keep plain navigation. */
@view-transition{navigation:auto}
.sidebar-nav .nav-item.is-active{view-transition-name:nav-active}
::view-transition-group(nav-active){animation-duration:.25s; animation-timing-function:var(--ease-spring, ease)}
/* NO root cross-fade (Tucker: content shouldn't fade — the quick global blend
   read as a flash). Opting root OUT of capture is the correct recipe (adversarial
   review: animation:none alone leaves the UA's plus-lighter blend → overbright
   flash): the old frame holds via cross-doc render-blocking, the new page paints
   directly underneath, and only the nav pill rides the transition overlay. */
:root{view-transition-name:none}
/* Reduced motion: keep the held-frame/instant-swap (it's LESS motion than a
   flashing reload — disabling VT entirely would bring the flash back for
   exactly the users who want calm); only the sliding pill morph is disabled. */
@media (prefers-reduced-motion: reduce){ ::view-transition-group(nav-active){animation:none} }
.sidebar-nav-divider{width:100%; height:1px; background:var(--nav-chrome-2); flex:0 0 auto}

/* Smile logo — iridescent conic fill + dark face. Eyes blink (4s) + follow the
   cursor; JS toggles .smiley--wink / .smiley--spin on interaction. */
.smiley{width:32px; height:32px; border-radius:50%; display:grid; place-items:center; color:var(--chrome-black); flex:0 0 auto; cursor:pointer; -webkit-tap-highlight-color:transparent;
  background:conic-gradient(from 210deg, #fab5db, #ccbafc, #a8d4ff, #9ef5e8, #b8fabf, #fdf2a8, #ffd1b3, #fab5db)}
/* Eyes blink in lockstep — no per-eye delay (the old .05s offset + wink
   restarts made the eyes drift; JS re-syncs both timelines after a wink). */
.smiley__eye{transform-box:fill-box; transform-origin:center; animation:smiley-blink 4s ease-in-out infinite}
.smiley__eyes{transition:transform .1s ease-out}
@keyframes smiley-blink{0%,92%,100%{transform:scaleY(1)} 95%,97%{transform:scaleY(.05)}}
.smiley--wink .smiley__eye--l{animation:smiley-wink .25s ease-in-out forwards}
@keyframes smiley-wink{0%,100%{transform:scaleY(1)} 40%,60%{transform:scaleY(.05)}}
.smiley--spin{animation:smiley-spin .6s cubic-bezier(.34,1.56,.64,1) forwards}
@keyframes smiley-spin{0%{transform:rotate(0)} 100%{transform:rotate(360deg)}}
/* Counter-roll when the bar tucks away. */
.smiley--spin-back{animation:smiley-spin-back .6s cubic-bezier(.34,1.56,.64,1) forwards}
@keyframes smiley-spin-back{0%{transform:rotate(0)} 100%{transform:rotate(-360deg)}}
/* Delight: an occasional squash-and-hop. */
.smiley--squish{animation:smiley-squish .7s cubic-bezier(.34,1.56,.64,1)}
@keyframes smiley-squish{0%{transform:scale(1) translateY(0)} 30%{transform:scale(1.15,.78) translateY(2px)} 55%{transform:scale(.92,1.08) translateY(-5px)} 78%{transform:scale(1.05,.96) translateY(0)} 100%{transform:scale(1) translateY(0)}}
/* Delight: a sleepy yawn when the user goes idle — mouth stretches open, eyes
   droop. Both eyes' blink restarts together when the class lifts (auto-resync). */
.smiley__mouth{transform-box:fill-box; transform-origin:center}
.smiley--yawn .smiley__mouth{animation:smiley-yawn-mouth 1.4s ease-in-out}
@keyframes smiley-yawn-mouth{0%,100%{transform:scaleY(1)} 35%,70%{transform:scaleY(1.75) translateY(.5px)}}
.smiley--yawn .smiley__eye{animation:smiley-yawn-eyes 1.4s ease-in-out}
@keyframes smiley-yawn-eyes{0%,100%{transform:scaleY(1)} 35%,70%{transform:scaleY(.3)}}
@media (prefers-reduced-motion:reduce){.smiley__eye{animation:none} .smiley__eyes{transition:none} .smiley--spin,.smiley--spin-back,.smiley--squish{animation:none} .smiley--yawn .smiley__mouth,.smiley--yawn .smiley__eye{animation:none}}

/* --- Feedback chrome (modal / toast / tooltip) --- */
.modal{width:320px; background:var(--surface); border-radius:var(--radius-lg); box-shadow:var(--shadow-overlay); overflow:hidden}
.modal-header{padding:18px 20px 4px; font-weight:600; font-size:var(--text-md)}
.modal-body{padding:6px 20px 16px; color:var(--secondary); font-size:var(--text-sm)}
.modal-footer{padding:14px 20px; display:flex; gap:10px; justify-content:flex-end; border-top:1px solid var(--hairline)}
.toast{display:inline-flex; align-items:center; gap:10px; padding:12px 16px; background:var(--dark); color:var(--bg); border-radius:var(--radius-md); box-shadow:var(--shadow-lg); font-size:var(--text-sm); font-weight:500}
.toast--success{border-left:3px solid var(--success)}
.toast--error{border-left:3px solid var(--error)}
.tooltip{display:inline-flex; padding:6px 10px; background:#000; color:#fff; border-radius:var(--radius-sm); font-size:11px}

/* --- Module composition (one kit of ingredients) --- */
.module{width:300px; padding:20px; display:flex; flex-direction:column; gap:12px}
.module:hover{box-shadow:var(--shadow-md); border-color:var(--border)}
.module.mini{width:216px; padding:16px; gap:10px}
.module.mini .module-headline{font-size:var(--text-base)}
.module-element.is-hidden{display:none !important}
.module-eyebrow{font-size:11px; font-weight:600; letter-spacing:.08em; text-transform:uppercase; color:var(--secondary);
  /* inline-flex so an optional eyebrow_icon() mark sits on the cap-height of the
     label rather than the baseline — a 14px glyph next to 11px uppercase looks
     dropped otherwise. An eyebrow with no icon is unaffected. */
  display:inline-flex; align-items:center; gap:6px}
.module-eyebrow-icon{flex:0 0 auto; opacity:.85}
.module-image{width:100%; aspect-ratio:16/10; border-radius:var(--radius-md); background:var(--muted); display:grid; place-items:center; color:var(--secondary)}
.module-headline{font-size:var(--text-md); font-weight:600}
.module-copy{font-size:var(--text-sm); color:var(--secondary); line-height:1.5}
.module-tag{display:flex}
.module-cta{display:flex; justify-content:flex-end; margin-top:2px}
.module-stat{font-size:var(--text-2xl); font-weight:600; letter-spacing:-.02em}

/* --- Global flow layout — the production reflow hooks. Anything that reflows on
   resize positions children by transform + transitions them (FLIP), never snaps.
   The JS engine reads data-flow/colw/rowh/gap/pad/w/h off the container/items. --- */
.reflow-container{position:relative; width:100%}
.reflow-container > .reflow-item{position:absolute; top:0; left:0; will-change:transform; transition:transform 480ms cubic-bezier(.2,.8,.2,1), width 480ms cubic-bezier(.2,.8,.2,1), height 480ms cubic-bezier(.2,.8,.2,1)}
/* flowLayout() packs only :not([hidden]) items, so filters hide by setting the
   hidden attribute. But a reflow-item whose own class sets `display` (.cosi-card
   and .loved-applet both use display:flex) beats the UA [hidden]{display:none}
   rule — and since every reflow-item is position:absolute, it then stays painted
   at 0,0 instead of disappearing. That broke /agents search entirely. Guard it
   here, once, rather than per-card: this applies to every current and future
   reflow-item, whatever display its class sets. */
.reflow-container > .reflow-item[hidden]{display:none}

/* Card entrance (#81, light pass): tiles rise into place while fading in —
   applet grid, dashboard widgets, project/agent/directory cards, all of which
   are already reflow-items, so this is one rule for every grid on the site.
   Uses the standalone `translate` PROPERTY, not `transform`: flow-layout.js owns
   the inline transform that positions each tile, and `translate` composes ahead
   of it instead of clobbering it. Stagger caps at 6 so a long grid doesn't
   trickle in. NOT the full motion pass (page transitions, type-in, skeleton
   direction) — that's still open on #81. */
@media (prefers-reduced-motion:no-preference){
  .reflow-container > .reflow-item{animation:card-in 380ms cubic-bezier(.2,.8,.2,1) both}
  .reflow-container > .reflow-item:nth-child(2){animation-delay:40ms}
  .reflow-container > .reflow-item:nth-child(3){animation-delay:80ms}
  .reflow-container > .reflow-item:nth-child(4){animation-delay:120ms}
  .reflow-container > .reflow-item:nth-child(5){animation-delay:160ms}
  .reflow-container > .reflow-item:nth-child(n+6){animation-delay:200ms}
  @keyframes card-in{from{opacity:0; translate:0 14px} to{opacity:1; translate:0 0}}
}

/* Widget size tiers (masonry dashboard grids — locked 2026-07-14, full
   rationale in DESIGN.md's "reflow is animated, globally" section).
   Column-count based, not percentage-based — data-colw on the container is
   the one global density knob; per-item data-w never needs to change when
   you tune it.
     x-small  data-w=1  data-h=~120        headline + one big number
     small    data-w=1  data-h=~180-220    a few lines of text, no numbers
     medium   data-w=2  data-h=~300-420    the default "real widget" size
     large    data-w=3  data-h=~340-420    ~3/4 width on common screens, not a literal %
     x-large  data-w=99 data-h="auto"      full width any viewport; auto-height ONLY
                                           safe at this tier — see flow-layout.js */

/* --- Avatar --- */
.avatar{width:32px; height:32px; border-radius:50%; background:var(--dark); color:var(--bg); display:grid; place-items:center}
.avatar img{width:100%; height:100%; border-radius:50%; object-fit:cover}

/* --- Shell (app layout) — the floating CoSi Bar + content area (#19).
   Pill hugs the left with generous margins and fills the viewport height per
   DESIGN.md §11.3. ponytail: static/expanded shell — the hidden-by-default
   spring-open interaction lands in #5. --- */
.app-nav{position:fixed; left:20px; top:20px; bottom:20px; z-index:100; display:flex}
.app-nav .sidebar-nav{height:100%}
/* (nav-user avatar/menu moved into the shell-level .app-island — #46) */
/* 110px = 20 left margin + 62 pill (15px padding ×2 + 32 icon) + 28 gutter */
/* Content starts at --content-top — consistent across every page, and clear of
   the top-right island. The nav + island keep the 20px viewport gutter above. */
.app-main{min-height:100vh; margin-left:110px; padding:var(--content-top) 48px 40px}
@media (max-width:768px){
  /* 24, not 72: the 72 reserve existed for the desktop island top-right, which
     no longer sits there on a phone (Tucker on staging, #1088: "gap at the top
     needs to go away and shift to the bottom"). #1098 briefly made this 40 on a
     misread of "another 10–20 pixels on top" — he meant inside the dock (#1102).
     Fly-out sheets hang from this same line. */
  :root{ --content-top:24px; }
  /* #1031 divergence, confirmed 2026-09-02: on a phone the AVATAR owns navigation
     (the nav sheet) and the smiley has no job — desktop keeps it as the CoSi Bar
     toggle. No left gutter; the reserve moves to the BOTTOM, sized to clear the
     island (48) and the gradient that rises behind it (~56) plus the safe area. */
  .app-nav{display:none}
  .app-main{margin-left:0; padding:var(--content-top) 16px calc(24px + 184px + env(safe-area-inset-bottom, 0px))}
}

/* --- Native feel on touch (#1091, Tucker on staging: "it needs to feel as much
   like a native mobile app as possible"). iOS zooms the whole page when a focused
   field renders under 16px — and --text-base is 14px, so every .control and the
   chat textarea did exactly that. The fix is the threshold itself, not a
   maximum-scale hack (pinch zoom stays allowed). !important on purpose: this has
   to beat every page and component sheet loaded after this one, and there is no
   legitimate reason for a phone field to be smaller. --- */
@media (max-width:768px){
  input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]), textarea, select, [contenteditable="true"], [contenteditable=""]{font-size:16px !important}
  /* No double-tap-to-zoom on anything (pinch still works), no grey tap flash. */
  html{touch-action:manipulation}
  a, button, [role="button"], label, .nav-sheet-tile, .menu-item{-webkit-tap-highlight-color:transparent}
}

/* --- Mobile nav sheet (#110 / #1083) — markup in site/components/nav-sheet.php,
   behaviour in shell.js (NavSheet). Head = actions, pinned; body = destinations,
   scrolls. Height is --sheet-h, set by JS to a snap stop (32 / 47 / 60% / full). */
.nav-sheet{display:none}
@media (max-width:768px){
  .nav-sheet{display:block; position:fixed; inset:0; z-index:340; pointer-events:none}   /* below the island (350), above fly-outs (300) */
  .nav-sheet[hidden]{display:none}
  .nav-sheet-scrim{position:absolute; inset:0; background:rgba(0,0,0,.32); opacity:0; transition:opacity var(--t-base); pointer-events:auto}
  .nav-sheet.is-open .nav-sheet-scrim{opacity:1}
  .nav-sheet-panel{position:absolute; left:0; right:0; bottom:0; height:var(--sheet-h, 40vh); display:flex; flex-direction:column;
    background:var(--surface); color:var(--primary); border-radius:20px 20px 0 0; box-shadow:var(--shadow-overlay);
    transform:translateY(100%); transition:transform var(--t-base), height var(--t-base); pointer-events:auto}
  .nav-sheet.is-open .nav-sheet-panel{transform:none}
  .nav-sheet.is-dragging .nav-sheet-panel{transition:none}
  .nav-sheet.is-dragging .nav-sheet-scrim{transition:none}
  /* The head starts UNDER the dock row (the dock is lifted into the sheet's top
     --dock-h pixels); no slot, no ✕ — the avatar toggles, scrim/swipe/Esc close. */
  .nav-sheet-head{position:relative; flex:0 0 auto; display:flex; flex-direction:column; gap:12px; padding:calc(var(--dock-h, 64px) + 4px) 16px 12px;
    border-bottom:1px solid var(--hairline); touch-action:none}
  .nav-sheet-quick{height:36px; display:flex; align-items:center; justify-content:center; gap:8px; border-radius:var(--radius-pill);
    background:rgb(var(--accent-rgb)/.10); color:var(--accent); border:1px solid var(--accent); font:inherit; font-size:var(--text-sm); font-weight:600; cursor:pointer}
  .nav-sheet-exit{height:36px; display:flex; align-items:center; justify-content:space-between; padding:0 14px 0 16px; border-radius:var(--radius-pill);
    background:var(--warning); color:#141413; border:0; font:inherit; font-size:var(--text-sm); font-weight:600; cursor:pointer}
  .nav-sheet-exit-label{display:flex; align-items:center; gap:10px}
  .nav-sheet-exit-dot{width:8px; height:8px; border-radius:50%; background:#141413}
  /* Content at N=7 with the Quick chat pill: head 92 + 36 + 12 + 12 + 1 = 153, body
     8 + 167 + 16 + 36 + 24 = 251 → 404 on an 844 phone. The 88px dock (#1102) took
     it past the 47% stop (397), so the middle stop moved to 49% (414) ON PURPOSE —
     the alternative was the sheet silently opening at 60% (Figma R2b). */
  .nav-sheet-body{flex:1 1 auto; min-height:0; overflow-y:auto; -webkit-overflow-scrolling:touch; padding:8px 16px 24px}
  .nav-sheet-grid{display:grid; grid-template-columns:repeat(4, minmax(0, 1fr)); gap:7px}
  .nav-sheet-tile{height:80px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px; border-radius:12px;
    background:var(--bg); color:var(--primary); text-decoration:none; font-size:12px; font-weight:500; line-height:1; text-align:center; overflow:hidden}
  .nav-sheet-tile svg{color:var(--secondary)}
  .nav-sheet-tile.is-active{background:var(--muted); box-shadow:inset 0 0 0 1.5px var(--accent); font-weight:600}
  .nav-sheet-tile.is-active svg{color:var(--accent)}
  .nav-sheet-tile-label{max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding:0 4px}
  .nav-sheet-account{margin:16px auto 0; height:36px; display:flex; align-items:center; gap:10px; padding:0 12px; background:none; border:0;
    color:var(--secondary); font:inherit; font-size:var(--text-sm); font-weight:500; cursor:pointer}
  .nav-sheet-account svg{transition:transform var(--t-base)}
  .nav-sheet.is-expanded .nav-sheet-account svg{transform:rotate(180deg)}
  .nav-sheet-account-slot{display:none; margin-top:12px; padding-top:12px; border-top:1px solid var(--hairline)}
  .nav-sheet.is-expanded .nav-sheet-account-slot{display:block}
  /* The relocated #nav-user-menu: a list, not a floating dropdown. */
  .nav-sheet-account-slot .menu{display:block; position:static; width:auto; padding:0; background:none; box-shadow:none}
  .nav-sheet-account-slot .menu-item{padding:12px; font-size:var(--text-base)}
  .nav-sheet-account-slot .menu-header{padding:4px 12px 12px}
  body.nav-sheet-open{overflow:hidden}
}

/* --- Phone layout (#110), the parts the bottom island + nav sheet did not replace. */
@media (max-width:480px){
  .app-main{padding-left:12px; padding-right:12px}
  /* Popup menus anchor left, so one opened near the right edge (the applet
     Share menu) hung off the screen — and pushed the document 4px wide even
     while hidden, since it's visibility-hidden, not display-none. */
  .select-menu .select-menu-list{left:auto; right:0}
  /* Near-full-bleed panel until the full-screen sheet chassis lands (#1084). */
  .flyout{left:12px; right:12px; width:auto}
}

/* --- CoSi Bar spring-open (#5) — hidden by default: only the Smile shows.
   Click springs the black pill out of the smile and down the screen; items
   pop in staggered. State = .is-open on .app-nav (persisted in localStorage,
   preloaded pre-paint so a kept-open bar doesn't re-animate every load). --- */
.app-nav .sidebar-nav{transition:height var(--ease-spring), background-color 260ms ease}
.app-nav:not(.is-open) .sidebar-nav{height:62px; background:transparent}
.nav-smile{background:none; border:none; padding:0; cursor:pointer; border-radius:50%; flex:0 0 auto; -webkit-tap-highlight-color:transparent}
.app-nav .sidebar-nav-group, .app-nav .sidebar-nav-divider, .app-nav .sidebar-nav-bottom{
  transition:opacity 160ms ease, transform var(--ease-spring)}
.app-nav:not(.is-open) .sidebar-nav-group,
.app-nav:not(.is-open) .sidebar-nav-divider,
.app-nav:not(.is-open) .sidebar-nav-bottom{opacity:0; transform:scale(.55); pointer-events:none}
/* stagger the pop as the pill expands downward */
.app-nav.is-open .sidebar-nav-group:nth-of-type(1){transition-delay:50ms}
.app-nav.is-open .sidebar-nav-divider{transition-delay:100ms}
.app-nav.is-open .sidebar-nav-group:nth-of-type(3){transition-delay:130ms} /* group 2 is the 3rd div (divider is 2nd) */
.app-nav.is-open .sidebar-nav-bottom{transition-delay:170ms}
@media (prefers-reduced-motion:reduce){
  .app-nav .sidebar-nav, .app-nav .sidebar-nav-group,
  .app-nav .sidebar-nav-divider, .app-nav .sidebar-nav-bottom{transition:none}
}

/* Fixed stack for shell toasts (bottom-right per DESIGN.md feedback spec). */
.toast-stack{position:fixed; right:24px; bottom:24px; z-index:300; display:flex; flex-direction:column; align-items:flex-end; gap:10px}

/* Backdrop for the shell's shared Modal dialog (shell.js window.Modal). Own
   class — legacy pages use .modal-overlay with direct display toggling. */
.shell-modal-overlay{position:fixed; inset:0; background:rgba(0,0,0,.5); display:flex; align-items:center; justify-content:center; z-index:400; opacity:0; visibility:hidden; transition:opacity var(--t-base), visibility var(--t-base)}
.shell-modal-overlay.open{opacity:1; visibility:visible}
.shell-modal-overlay .modal{width:380px; max-width:90vw; transform:none}
.modal--danger .btn-primary{background:var(--error)}
.modal--danger .btn-primary:hover{filter:brightness(1.1)}

/* ==========================================================================
   PROSE — global markdown content system (#92)
   Any surface rendering markdown (chat replies, builder chat, reports,
   applet output) wraps it in .prose. Styles the rendered HTML only —
   never raw markdown.
   ========================================================================== */
.prose{font-size:var(--text-base); line-height:1.55; color:var(--primary)}
.prose > *:first-child{margin-top:0}
.prose > *:last-child{margin-bottom:0}
.prose p{margin:0 0 10px}
.prose h1,.prose h2{font-size:var(--text-lg); font-weight:600; margin:16px 0 8px}
.prose h3,.prose h4{font-size:var(--text-md); font-weight:600; margin:14px 0 6px}
.prose ul,.prose ol{margin:0 0 10px; padding-left:20px}
.prose li{margin:3px 0}
.prose li > ul,.prose li > ol{margin-bottom:0}
.prose strong{font-weight:600}
.prose a{color:var(--accent); text-decoration:none}
.prose a:hover{text-decoration:underline}
.prose code{font-family:var(--font-mono); font-size:.86em; background:var(--muted); padding:1px 5px; border-radius:4px}
.prose pre{background:var(--muted); border-radius:var(--radius-sm); padding:12px; overflow-x:auto; margin:0 0 10px}
.prose pre code{background:none; padding:0}
.prose blockquote{border-left:2px solid var(--border); margin:0 0 10px; padding:2px 0 2px 12px; color:var(--secondary)}
.prose hr{border:0; border-top:1px solid var(--hairline); margin:14px 0}
.prose table{border-collapse:collapse; width:100%; margin:0 0 10px; font-size:var(--text-sm)}
.prose th{text-align:left; font-weight:600; border-bottom:1px solid var(--border); padding:6px 10px 6px 0}
.prose td{border-bottom:1px solid var(--hairline); padding:6px 10px 6px 0}

/* ==========================================================================
   SELECT MENU — styled replacement for native <select> (#92)
   Rule (DESIGN.md): never system menus. Button + .menu popup composed from
   the existing menu kit. JS wiring is per-consumer (see builder.js).
   ========================================================================== */
.select-menu{position:relative; display:inline-block}
.select-menu-btn{display:inline-flex; align-items:center; gap:8px; height:32px; padding:0 12px; background:var(--surface); color:var(--primary); border:1px solid var(--border); border-radius:var(--radius-sm); font-size:var(--text-sm); font-family:inherit; cursor:pointer; transition:border-color var(--t-fast)}
.select-menu-btn:hover{border-color:var(--secondary)}
.select-menu-btn svg{color:var(--secondary); transition:transform var(--t-fast)}
.select-menu.open .select-menu-btn svg{transform:rotate(180deg)}
.select-menu-list{position:absolute; bottom:calc(100% + 6px); left:0; min-width:140px; background:var(--surface); border-radius:var(--radius-md); padding:6px; box-shadow:var(--shadow-overlay); opacity:0; visibility:hidden; transform:translateY(4px); transition:opacity var(--t-fast), transform var(--t-fast), visibility var(--t-fast); z-index:50}
.select-menu.open .select-menu-list{opacity:1; visibility:visible; transform:translateY(0)}
.select-menu.drop-down .select-menu-list{bottom:auto; top:calc(100% + 6px); transform:translateY(-4px)}
.select-menu.drop-down.open .select-menu-list{transform:translateY(0)}
/* Portaled to <body> on open (shell.js) so a transformed / overflow:hidden tile
   can't trap or clip the menu (#1212). Fixed, above everything; JS sets top/left/
   width. .open drives visibility here because the list is no longer a descendant
   of .select-menu once portaled, so the selectors above can't reach it. */
.select-menu-list.is-portal{position:fixed; z-index:1000}
.select-menu-list.is-portal.open{opacity:1; visibility:visible; transform:translateY(0)}
.select-menu-list .menu-item.is-selected{background:var(--muted); font-weight:600}
/* Cap it. Nothing bounded this, so a long list (the ~400-entry timezone select,
   or a full team in the defer picker) rendered every row and ran off the
   viewport — and the drop-up measurement in shell.js has to know the most a
   menu can take. */
.select-menu-list{max-height:260px; overflow-y:auto}

/* ==========================================================================
   FLYOUT — universal side-panel pattern (#44/#45, Tucker 2026-07-07)
   Aligns below the dynamic island's home (top-right), FIXED against the
   viewport (page scrolls under it), internal vertical-only scroll with
   fade masks both ends. One shell for chat history, model settings,
   media settings, project settings. JS: window.Flyout (shell.js).
   ========================================================================== */
.flyout{position:fixed; top:var(--content-top); right:20px; width:min(380px, calc(100vw - 40px)); max-height:calc(100vh - var(--content-top) - 20px); display:flex; flex-direction:column; background:var(--surface); border-radius:var(--radius-lg); box-shadow:var(--shadow-overlay); z-index:300; opacity:0; visibility:hidden; transform:translateY(-6px); transition:opacity var(--t-base), transform var(--t-base), visibility var(--t-base)}
.flyout.open{opacity:1; visibility:visible; transform:translateY(0)}
.flyout-head{flex:0 0 auto; display:flex; align-items:center; gap:var(--space-3); padding:var(--space-5) var(--space-5) var(--space-3)}
.flyout-title{font-size:var(--text-md); font-weight:600; flex:1}
.flyout-close{width:28px; height:28px; display:flex; align-items:center; justify-content:center; background:none; border:0; border-radius:var(--radius-sm); color:var(--secondary); cursor:pointer; transition:background var(--t-fast), color var(--t-fast)}
.flyout-close:hover{background:var(--muted); color:var(--primary)}
.flyout-back{display:none}          /* phones only — see the sheet block (#1128) */
.flyout-head--phone{display:none}   /* a head that exists for the phone sheet only (#1128) */
.flyout-scroll{flex:1; min-height:0; overflow-y:auto; overflow-x:hidden; padding:var(--space-2) var(--space-5) var(--space-5);
  /* Bottom fade always; the TOP fade only appears once scrolled (.is-scrolled),
     so content at rest (e.g. the flyout's first field) isn't covered by a
     gradient. shell.js toggles is-scrolled on scrollTop>0 (Tucker 2026-07-15). */
  mask-image:linear-gradient(to bottom, black 0, black calc(100% - 28px), transparent 100%);
  -webkit-mask-image:linear-gradient(to bottom, black 0, black calc(100% - 28px), transparent 100%)}
.flyout-scroll.is-scrolled{
  mask-image:linear-gradient(to bottom, transparent 0, black 28px, black calc(100% - 28px), transparent 100%);
  -webkit-mask-image:linear-gradient(to bottom, transparent 0, black 28px, black calc(100% - 28px), transparent 100%)}

/* ==========================================================================
   RANGE SLIDER — kit definition (Tucker 2026-07-07: sliders were undefined;
   no white bars). Accent fill left of thumb (painted by enhanceRanges),
   muted remainder, accent dot thumb. Matches the Response Style comp.
   ========================================================================== */
input[type=range]{-webkit-appearance:none; appearance:none; height:4px; border-radius:var(--radius-pill); background:var(--muted); outline:none; cursor:pointer}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none; appearance:none; width:14px; height:14px; border-radius:50%; background:var(--accent); border:none; cursor:pointer; transition:transform var(--t-fast)}
input[type=range]::-webkit-slider-thumb:hover{transform:scale(1.15)}
input[type=range]::-moz-range-thumb{width:14px; height:14px; border-radius:50%; background:var(--accent); border:none; cursor:pointer}
input[type=range]::-moz-range-track{height:4px; border-radius:var(--radius-pill); background:transparent}

/* --- Fly-outs on PHONES (#110 item 3 → #1084): every .flyout is a near-full-
   screen SHEET sitting on the dock — full width, top at --content-top, bottom on
   the dock's top edge, the same 20px corners as the dock and the nav sheet, a
   scrim behind it. One chassis; the nine consumers (Activity, Memory,
   Affirmations, Bookmarks, chat history, model settings, Asana task, project
   settings, applet controls) inherit it and keep their own scroll/foot. Flyout in
   shell.js adds focus, swipe-down and desktop-only persistence. The dock (350)
   stays above the sheet (300), so the avatar still opens navigation over it. --- */
@media (max-width:768px){
  .flyout{top:var(--content-top); left:0; right:0; bottom:var(--dock-h, 64px); width:auto; max-height:none; height:auto;
    border-radius:20px 20px 0 0; opacity:1; transform:translateY(100%); transition:transform var(--t-base), visibility var(--t-base)}
  .flyout.open{transform:translateY(0)}
  .flyout.is-dragging{transition:none}
  /* --sheet HUGS its contents instead of running from --content-top: for a menu
     whose contents are near-fixed (the chat bar's + sheet: three tiles, four to
     six rows) a sheet with dead space below itself reads as broken. This is a
     deliberate difference from the NAV sheet, which snaps to stops because its
     row count varies per person (hidden_nav) and must land in the same place for
     everyone. Two sheets, two rules, both on purpose — do not unify. */
  .flyout--sheet{top:auto; height:auto; max-height:calc(100dvh - var(--content-top) - var(--dock-h, 64px))}
  /* A hugging sheet rarely scrolls, and the chassis's bottom fade made its last
     row look disabled. No mask here; the fade returns only if it overflows. */
  .flyout--sheet .flyout-scroll{mask-image:none; -webkit-mask-image:none; padding-bottom:var(--space-4)}
  .flyout--sheet .flyout-scroll.is-scrolled{mask-image:linear-gradient(to bottom, transparent 0, black 28px); -webkit-mask-image:linear-gradient(to bottom, transparent 0, black 28px)}
  /* The one consumer that sets its own height (home.php, the Asana task panel)
     did so for the desktop column; on a phone the sheet's box is the height.
     Any future page height rule belongs behind (min-width:769px). */
  .flyout--tall, #asana-task-flyout.flyout{height:auto}
  /* The head is the drag handle: a grab pip, and no browser panning under it. */
  .flyout-head{position:relative; padding-top:calc(var(--space-5) + 8px); touch-action:none}
  .flyout-head::before{content:""; position:absolute; top:8px; left:50%; width:36px; height:4px; margin-left:-18px; border-radius:2px; background:var(--border)}
  /* No ✕ on a phone (#1128): the head closes — tap it or swipe it down — and
     Escape and the scrim still work. Desktop fly-outs keep theirs. Focus lands
     on the head when a sheet opens, so no ring for a programmatic focus. */
  .flyout .flyout-close{display:none}
  .flyout-head:focus{outline:none}
  .flyout-head--phone{display:flex}
  .flyout-back{display:inline-flex; align-items:center; gap:2px; height:28px; margin-left:calc(var(--space-2) * -1); padding:0 var(--space-2); background:none; border:0; border-radius:var(--radius-sm); color:var(--secondary); font:inherit; font-size:var(--text-sm); cursor:pointer}
  .flyout-foot{border-radius:0}
  /* Scrim: under the sheet (300) and the dock (350), over everything else.
     Flyout's outside-click closer treats a tap here as "outside" on every sheet. */
  body.flyout-open::before{content:""; position:fixed; inset:0; background:rgba(0,0,0,.32); z-index:299}
}

/* Flyout content-yield (Tucker 2026-07-07): an open flyout narrows the page
   content — nothing flows underneath. Flyout.open/close manages the class.
   DESKTOP ONLY (#110): the reserve is 436px, more than a phone or a portrait
   tablet HAS — it pushed the page clean off the screen. Below the breakpoint
   the panel overlays instead. */
.app-main{transition:padding-right var(--t-slide)}
@media (min-width:769px){
  body.flyout-open .app-main{padding-right:calc(380px + 32px + var(--space-6))}
}

/* ============================================================
   Docked chat panel (#1200) — a persistent right-hand chat that survives
   navigation. Agreed on the Alpha call (Nick, Angie, Megan, Vee); comp 405:1545.

   Architecture: the panel is parent-shell CHROME (surface + collapse handle);
   its body is an <iframe> of the real /chat page in panel mode (?panel=1). The
   whole chat engine (chat.js streaming + rendering + the mobile chat bar and
   its menus) is REUSED, never forked — the iframe renders at 418px, so /chat's
   own max-width:768px rules fire and the composer is the mobile bar for free.
   The Dynamic Island is NOT moved: it stays fixed top-right (z 350) and the
   panel surface (z 300) simply slides in behind it, landing the island in the
   panel's top exactly as the comp shows. Width is fixed (418px) — no draggable
   divider that would make every masonry surface a per-user moving target.
   DESKTOP ONLY (min-width:769px); on phones the launcher keeps its existing
   summon → /chat behaviour (a 418px side panel on a phone is the opposite of
   what was asked). Fixed at 418 so the persistence half is the hard part, not
   the width. */
:root{ --docked-w:418px; }
.docked-chat{position:fixed; top:0; right:0; bottom:0; width:var(--docked-w);
  display:flex; flex-direction:column; background:var(--surface);
  border-left:1px solid var(--border); box-shadow:var(--shadow-overlay);
  z-index:300; transform:translateX(100%); transition:transform var(--t-slide)}
.docked-chat[hidden]{display:none}
body.docked-open .docked-chat{transform:translateX(0)}
/* Restore without the slide: the pre-paint script marks the body so a kept-open
   panel is simply present on load, not animated in on every navigation. */
body.docked-instant .docked-chat{transition:none}
/* Flyout over the docked chat (#1205, Option B — Tucker, comp 500:109). A
   universal side-panel flyout (Memory, Activity, Asana task, chat history … —
   everything Flyout.open drives, the only thing that sets body.flyout-open) is
   right-anchored at z 300 and lands over the 418px panel, also z 300. The
   exploration weighed HIDING the chat vs keeping it; Tucker chose KEEP. So the
   chat STAYS in place and the whole background recedes under a 0.5 page scrim,
   which reads as ONE focal layer instead of a second bright right-hand panel
   (the "two panels" trap the exploration flagged). The flyout is raised above the
   scrim; the Dynamic Island (z 350) stays above it too, so live status keeps
   showing, undimmed. The panel stays DOCKED (body.docked-open kept, so .app-main's
   reserve is unchanged and the masonry never re-tiers). The iframe is only
   covered, never unloaded, so a half-typed draft and scroll survive (nothing
   fires pagehide). The chat is also made `inert` (shell.js) so tab-stops and
   clicks can't reach it while it's visible; the scrim is the outside-click close
   target. Short popovers (tile ⋯, avatar menu, context menus) use .menu, never set
   body.flyout-open, so they never dim the chat. (docked-open is desktop-only by
   construction — the pre-paint bails below 769px — so no media query needed.) */
body.flyout-open.docked-open .docked-chat{pointer-events:none}
body.flyout-open.docked-open::after{content:""; position:fixed; inset:0;
  background:rgba(0,0,0,.5); z-index:305; pointer-events:auto;
  animation:flyout-dim-in var(--t-base) ease}
body.flyout-open.docked-open .flyout.open{z-index:306}
@keyframes flyout-dim-in{from{opacity:0} to{opacity:1}}
/* The iframe FILLS the panel (#1205 white-bar fix): the old margin-top:64px left
   a bare strip of panel --surface for the fixed island to sit in — but that strip
   lives OUTSIDE the iframe, so the iframe's own + sheet scrim could never darken it
   and it flashed as a white bar above the sheet. Fill the panel so the scrim
   covers the whole height (the island floats over it, z 350); the transcript
   reserves the island's room with its own padding-top instead (chat.css).
   Surface-coloured so a fresh iframe load reads as the panel refreshing, not a
   white flash. */
.docked-chat__frame{flex:1 1 auto; min-height:0; width:100%; border:0;
  background:var(--surface)}
/* Collapse — a slim drawer pull on the panel's left edge, clear of the island
   (top row) and the composer (bottom). Chevron points right = "push it away".
   Half-size and sitting fully OUTSIDE the panel edge (right edge flush at x=0,
   not overlapping it) — Tucker #1200 review. A padded transparent ::after keeps
   the click target comfortable while the visible pull stays slim. */
.docked-chat__collapse{position:absolute; top:50%; left:-13px; transform:translateY(-50%);
  width:13px; height:26px; display:grid; place-items:center; padding:0;
  background:var(--surface); color:var(--secondary); border:1px solid var(--border);
  border-right:0; border-radius:var(--radius-sm) 0 0 var(--radius-sm);
  box-shadow:var(--shadow-sm); cursor:pointer; z-index:1;
  transition:color var(--t-fast), background var(--t-fast)}
.docked-chat__collapse::after{content:""; position:absolute; inset:-11px -8px}
.docked-chat__collapse svg{width:12px; height:12px}
.docked-chat__collapse:hover{color:var(--primary); background:var(--muted)}
/* When the panel is open, the launcher FAB is redundant — the collapse handle
   is the way back. */
body.docked-open .chat-fab{opacity:0; pointer-events:none; transform:scale(.8)}
/* Content-yield: the open panel narrows the page so nothing flows under it and
   the masonry grids re-tier (flow-layout's ResizeObserver repacks off the width
   change). Flush-right 418 + the page's own right gutter. Desktop only. */
@media (min-width:769px){
  body.docked-open .app-main{padding-right:calc(var(--docked-w) + var(--space-6))}
}
/* The chat page's PANEL-MODE rules (.chat--panel, ?panel=1 inside the docked
   iframe) live in pages/chat.css with the rest of that page's vocabulary
   (#1309 part 2). */

/* Number inputs: no native spinners anywhere (round 3) */
input[type=number]{-moz-appearance:textfield; appearance:textfield}
input[type=number]::-webkit-outer-spin-button,input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none; margin:0}

/* Feedback modal (#117) */
.feedback-modal{width:440px; max-width:92vw; padding:var(--space-6); display:flex; flex-direction:column; gap:var(--space-3)}
.feedback-modal-head{display:flex; align-items:center; justify-content:space-between}
.feedback-modal-head h2{font-size:var(--text-lg); font-weight:600}
.feedback-modal-sub{color:var(--secondary); font-size:var(--text-sm)}
.feedback-modal textarea{width:100%; resize:vertical; min-height:110px; font:inherit; font-size:var(--text-base); background:var(--bg); color:var(--primary); border:1px solid var(--border); border-radius:var(--radius-sm); padding:var(--space-3)}
.feedback-modal textarea:focus{outline:none; border-color:var(--secondary)}
.feedback-modal textarea::placeholder{color:var(--secondary)}
.feedback-modal-foot{display:flex; align-items:center; justify-content:space-between; gap:var(--space-3)}
.feedback-modal-note{font-size:var(--text-sm); color:var(--secondary)}
/* Screenshot in the reporter (#1035): a drop/paste/pick target, then a bounded
   preview so a tall image does not push Send off-screen — the point is
   recognition, not legibility. The target is a <label> around a real file
   input; :focus-within is the keyboard's version of .is-dragover.
   .drop-zone is the same target for any page (skills install, #1063). */
.feedback-shot-drop,.drop-zone{display:block; padding:var(--space-4); border:1px dashed var(--border); border-radius:var(--radius-sm); color:var(--secondary); font-size:var(--text-sm); text-align:center; cursor:pointer; transition:border-color var(--t-fast),color var(--t-fast)}
.feedback-shot-drop:hover,.feedback-shot-drop:focus-within,.feedback-shot-drop.is-dragover,.drop-zone:hover,.drop-zone:focus-within,.drop-zone.is-dragover{border-color:var(--accent); color:var(--primary)}
.feedback-shot-drop.is-dragover,.drop-zone.is-dragover{background:rgb(var(--accent-rgb)/.08)}
/* [hidden] loses to a display rule (see the note above .btn[hidden]), so say it. */
.feedback-shot-drop[hidden],.feedback-shot-preview[hidden]{display:none}
.feedback-shot-preview{position:relative}
.feedback-shot-img{width:100%; max-height:150px; object-fit:cover; object-position:top center; border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--bg); display:block}
.feedback-shot-remove{position:absolute; top:var(--space-2); right:var(--space-2); height:30px; padding:0 12px; background:var(--surface)}


/* Text color helpers (status messaging) */
.text-success{color:var(--success)}
.text-error{color:var(--error)}
.text-warning{color:var(--warning)}
.text-info{color:var(--info)}


/* ── Designed output blocks (#377) ────────────────────────────────────────
   The shared "Lego" vocabulary applets AND widgets render from. Each block is a
   .reflow-item on the masonry grid (flow-layout.js sets width/height/transform);
   these classes are the INNER styling. Kit-owned so the sandbox (full-kit inject),
   design-lint, widgets, and the run page all share one source. Generalized from
   the sentiment-specific report-* set. */
.block-card{padding:var(--space-5); display:flex; flex-direction:column; overflow:hidden}
.block-card--intro{overflow:visible}

/* section (intro / header, x-large data-w 99) */
.block-section__head{display:flex; justify-content:space-between; align-items:flex-start; gap:var(--space-4)}
.block-section__title{font-family:var(--font-primary); font-size:var(--text-xl); font-weight:600; color:var(--primary); letter-spacing:-0.01em; margin:0; line-height:1.2}
.block-section__sub{font-size:var(--text-sm); color:var(--secondary); margin:4px 0 0}
.block-section__lead{font-size:var(--text-md); color:var(--primary); line-height:1.55; margin:var(--space-4) 0 0}

/* badge (tone-coded) */
/* white-space:nowrap is right for "Failed" and wrong for a sentence, and a badge
   cannot know which it was handed. Guiding Principles puts a whole trap in one
   ("Treating every thread as a conversion opportunity and steering every answer
   toward a Chime product") and it ran straight out of the card — the card clips
   with overflow:hidden, so the text was not merely ugly, it was UNREADABLE
   (#871, Rhiannon). Wrapping costs a short badge nothing: it only breaks when it
   would otherwise exceed the space it has.

   NO text-transform. It was `capitalize`, which is only ever right for a bare
   one-word status and silently mangles everything else — it rendered a whole
   sentence as "Treating Every Thread As A Conversion Opportunity". Authors
   already write their labels the way they want them read ("Looks clean",
   "Review needed"); the badge's job is to style them, not to re-case them.
   Sentence case everywhere, title case nowhere (Tucker, 2026-08-27). */
.block-badge{display:inline-flex; align-items:center; font-size:var(--text-xs); font-weight:600; padding:4px 10px; border-radius:var(--radius-pill); white-space:normal; max-width:100%; overflow-wrap:anywhere; text-align:left; line-height:1.35}
.block-badge--sm{padding:2px 8px; font-size:11px}
.block-badge--good{background:rgba(var(--teal-rgb) / 0.14); color:var(--success)}
.block-badge--bad{background:rgba(var(--red-rgb) / 0.12); color:var(--red)}
.block-badge--neutral{background:var(--muted); color:var(--secondary)}
.block-badge--info{background:rgba(var(--blue-rgb) / 0.12); color:var(--accent)}

/* Lockdown control (#883) — the one destructive-looking button on the admin
   page, and the only red one, because it is the only action that stops everyone
   else working. When engaged it inverts, so the page cannot look normal while
   the platform is locked. */
.btn-lock{display:inline-flex; align-items:center; gap:6px; background:transparent; color:var(--red); border:1px solid rgba(var(--red-rgb) / .45)}
.btn-lock:hover{background:rgba(var(--red-rgb) / .08); border-color:var(--red)}
.btn-lock.is-locked{background:var(--red); color:#fff; border-color:var(--red)}
.btn-lock.is-locked:hover{background:var(--red); opacity:.9}
/* Kind chip on a diagnostics row (#886) — one list holds clients, people and
   projects, and a bare slug never said which. All-caps deliberately: this is an
   eyebrow, and eyebrows stay uppercase (Tucker). */
.diag-kind{display:inline-block; margin-left:8px; font-size:10px; font-weight:600; letter-spacing:.06em; text-transform:uppercase; color:var(--secondary); vertical-align:middle}
.lockdown-banner{margin-bottom:var(--space-4); padding:10px 14px; border-radius:var(--radius-sm); font-size:var(--text-sm); line-height:1.5; color:var(--primary); background:rgba(var(--red-rgb) / .08); border:1px solid rgba(var(--red-rgb) / .35)}

/* stat-tiles (x-small data-w 1) */
.block-card--stat{align-items:center; justify-content:center; text-align:center; gap:6px}
.block-stat__value{font-family:var(--font-primary); font-size:var(--text-2xl); font-weight:600; color:var(--primary); line-height:1; letter-spacing:-0.02em}
.block-stat__label{font-size:11px; font-weight:600; letter-spacing:0.04em; text-transform:uppercase; color:var(--secondary)}
.block-stat__delta{font-size:var(--text-xs); font-variant-numeric:tabular-nums}
.block-stat__delta--up{color:var(--success)} .block-stat__delta--down{color:var(--red)}

/* segmented-bar (proportional, N labeled segments) */
.block-seg{margin-top:var(--space-5)}
.block-seg__bar{display:flex; height:10px; border-radius:var(--radius-pill); overflow:hidden; background:var(--muted)}
.block-seg__fill--good{background:var(--success)} .block-seg__fill--bad{background:var(--red)}
.block-seg__fill--neutral{background:var(--border)} .block-seg__fill--info{background:var(--accent)}
.block-seg__legend{display:flex; flex-wrap:wrap; gap:var(--space-4); margin-top:var(--space-3); font-size:var(--text-xs); color:var(--secondary)}
.block-seg__legend span{display:inline-flex; align-items:center; gap:6px}
.block-dot{width:8px; height:8px; border-radius:50%; display:inline-block}
.block-dot--good{background:var(--success)} .block-dot--bad{background:var(--red)}
.block-dot--neutral{background:var(--border)} .block-dot--info{background:var(--accent)}

/* panel head (bars / lists) */
.block-panel__head{margin-bottom:var(--space-4)}
.block-panel__sub{font-size:var(--text-sm); color:var(--secondary); line-height:1.5; margin:var(--space-2) 0 0}

/* bars (medium data-w 2) */
.block-bars{display:flex; flex-direction:column; gap:var(--space-4)}
.block-bar__label{display:flex; justify-content:space-between; font-size:var(--text-sm); color:var(--primary); margin-bottom:6px}
.block-bar__pct{color:var(--secondary); font-variant-numeric:tabular-nums}
.block-bar__track{height:8px; background:var(--muted); border-radius:var(--radius-pill); overflow:hidden}
.block-bar__fill{height:100%; background:var(--secondary); border-radius:var(--radius-pill)}
.block-bar__fill--brand{background:var(--accent)}

/* key-values (overview style) */
.block-kv{display:flex; flex-direction:column; gap:var(--space-3)}
.block-kv__row{display:flex; justify-content:space-between; gap:var(--space-4); align-items:baseline}
.block-kv__k{font-size:var(--text-sm); color:var(--secondary)}
.block-kv__v{font-size:var(--text-sm); color:var(--primary); font-weight:600; text-align:right; font-variant-numeric:tabular-nums}

/* card-grid card (medium data-w 2) — media/title/body/rows/number/actions */
/* Media preview sits on --bg so a resized ad's true frame edge is visible;
   contain (not cover) so the real aspect ratio reads honestly. */
/* Fixed box height, NOT auto: flow-layout.js packs tiles before images load, so
   an auto-height media box measures 0 and the card gets clipped. A constant box
   also gives every size a common frame to be compared against. */
.block-card__media{display:flex; align-items:center; justify-content:center; height:180px; background:var(--bg); border:1px solid var(--border); border-radius:var(--radius-sm); padding:var(--space-3); margin-bottom:var(--space-3); overflow:hidden}
.block-card__media img{max-width:100%; max-height:100%; width:auto; height:auto; object-fit:contain; display:block}
.block-card__num{font-family:var(--font-primary); font-size:var(--text-2xl); font-weight:700; color:var(--border); line-height:1; margin-bottom:var(--space-2); flex-shrink:0}
.block-card__title{font-size:var(--text-md); font-weight:600; color:var(--primary); margin:0 0 var(--space-3); line-height:1.3}
.block-card__body{font-size:var(--text-sm); color:var(--secondary); margin:0; line-height:1.5}
.block-card__row{margin-bottom:var(--space-3)}
.block-card__k{display:block; font-size:10px; font-weight:700; letter-spacing:0.06em; text-transform:uppercase; color:var(--accent); margin-bottom:2px}
.block-card__row p{font-size:var(--text-sm); color:var(--secondary); margin:0; line-height:1.5}
.block-card__quote{font-size:var(--text-sm); color:var(--primary); font-style:italic; margin:8px 0 0; padding-left:var(--space-3); border-left:2px solid var(--border)}
.block-card__meta{display:flex; align-items:flex-start; gap:var(--space-2); margin-bottom:6px; min-width:0; flex-wrap:wrap}
.block-card__count{margin-left:auto; font-size:var(--text-xs); color:var(--secondary); font-variant-numeric:tabular-nums}
.block-card__actions{display:flex; gap:var(--space-2); margin-top:var(--space-4); padding-top:var(--space-4); border-top:1px solid var(--border); flex-shrink:0}
.block-card__actions .btn{height:36px; padding:0 16px}
/* btn-secondary is transparent-bordered → invisible on a card; give it a resting edge */
.block-btn-outline{border-color:var(--border)}

/* markdown / prose (full-width data-w 99) */
.block-card--prose{overflow-y:auto}
.block-card--prose h2{font-family:var(--font-primary); font-size:var(--text-lg); font-weight:600; color:var(--primary); margin:0 0 var(--space-3)}


/* ── Choice prompt (#386) ─────────────────────────────────────────────────
   The shared multiple-choice primitive: a question + tappable options + an
   optional free-text "something else" escape. Interactive sibling of the
   display blocks. Used by builder planning AND applets (question-loops).
   Batch mode adds a "1 of N" stepper header + Back. Kit-owned so every
   surface renders it identically (window.ChoicePrompt drives it). */
/* No own fill/padding — it groups on whatever surface hosts it (the applet
   result card, the builder thread), so it never nests a card-in-a-card (#2d2d2d
   on #2d2d2d). The option rows carry the only surfaces. */
.choices{display:flex; flex-direction:column; gap:var(--space-3)}
.choices-head{display:flex; align-items:center; justify-content:space-between; gap:var(--space-3)}
.choices-progress{font-size:var(--text-xs); font-weight:600; letter-spacing:0.04em; text-transform:uppercase; color:var(--secondary)}
.choices-back{background:none; border:0; color:var(--secondary); font:inherit; font-size:var(--text-xs); cursor:pointer; padding:0}
.choices-back:hover{color:var(--primary)}
.choices-q{font-weight:600; font-size:var(--text-base); color:var(--primary)}
.choices-opt{display:flex; align-items:center; gap:var(--space-3); width:100%; text-align:left; background:var(--surface); border:1px solid var(--hairline); border-radius:var(--radius-sm); padding:var(--space-3) var(--space-4); color:var(--primary); font:inherit; font-size:var(--text-sm); cursor:pointer; transition:border-color var(--t-fast), background var(--t-fast)}
.choices-opt:hover{border-color:var(--secondary)}
.choices-opt.is-picked{border-color:var(--accent); background:rgb(var(--accent-rgb) / .1)}
.choices-num{flex:0 0 22px; height:22px; border-radius:var(--radius-pill); background:var(--muted); color:var(--secondary); font-size:var(--text-xs); display:flex; align-items:center; justify-content:center}
.choices-other{display:flex; gap:var(--space-2)}
.choices-other input{flex:1}
.choices.is-answered .choices-opt:not(.is-picked){opacity:.45; pointer-events:none}
.choices.is-answered .choices-other{display:none}

/* Blocks rendered INSIDE a surface card (dashboard widgets, pinned applets)
   need a contrasting fill — .surface-card tiles on a .surface-card host are the
   same colour, so the content reads as floating text (Tucker 2026-07-28). Inset
   tiles drop to --bg, matching the black stat boxes the hand-built widgets use.
   The section header stays flush: it's the host card's own title, not a tile. */
.blocks-inset .block-card{background:var(--bg)}
.blocks-inset .block-card--intro{background:none; padding:0 0 var(--space-2)}
.blocks-inset .block-card--prose{background:none; padding:0}

/* --- Markdown tables. marked emits <table> for GFM tables and the sanitizer
   keeps them, but nothing styled them — so a table rendered as unformatted
   run-together text wherever markdown appears (chat replies, applet markdown
   blocks). Tucker hit this on a brief's "still open" table, 2026-08-11.
   Scoped to rendered-markdown surfaces so it can't touch layout tables. --- */
.chat__message-content table,
.chat__text-part table,
.block-card table {
    width: 100%;
    border-collapse: collapse;
    margin: var(--space-3) 0;
    font-size: var(--text-sm);
    /* Wide tables scroll inside their own box rather than stretching the
       message bubble past the column. */
    display: block;
    overflow-x: auto;
}

.chat__message-content th,
.chat__text-part th,
.block-card th {
    text-align: left;
    font-weight: 600;
    color: var(--primary);
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
}

.chat__message-content td,
.chat__text-part td,
.block-card td {
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--border);
    vertical-align: top;
}

.chat__message-content tr:last-child td,
.chat__text-part tr:last-child td,
.block-card tr:last-child td { border-bottom: 0; }

/* Affirmations (#790) — the record is the thing being attested to, so it is
   shown as stored rather than prettified into something it isn't. */
.affirm-progress { font-size: 12px; margin-bottom: var(--space-3); }
.affirm-claim { margin: 0 0 var(--space-1); font-size: 16px; line-height: 1.35; }
.affirm-meta { font-size: 12px; margin-bottom: var(--space-3); }
.affirm-record {
    white-space: pre-wrap;
    word-break: break-word;
    /* <pre> for the line breaks, NOT for the look — this is prose someone has to
       read and stand behind, and monospace makes a paragraph read like code. */
    font: inherit;
    font-size: 13px;
    line-height: 1.55;
    max-height: 46vh;
    overflow-y: auto;
    padding: var(--space-3);
    background: var(--muted);
    border-radius: var(--radius-sm);
    margin: 0 0 var(--space-4);
}
.affirm-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.flyout-sources:not(:empty) { padding: 0 var(--space-5) var(--space-3); }
.affirm-from {
    font-size: 12px; padding: var(--space-2) var(--space-3);
    background: var(--muted); border-radius: var(--radius-sm);
    margin-bottom: var(--space-3);
}
.affirm-note {
    /* No drag handle: the box grows with what is typed (tasks.js). A resize grip
       in a pinned footer only ever lets someone push the buttons off-screen. */
    width: 100%; margin-bottom: var(--space-3); resize: none; overflow-y: auto;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border); border-radius: var(--radius-sm);
    background: transparent; color: var(--primary);
}
.affirm-defer { display: flex; gap: var(--space-2); margin-top: var(--space-3); align-items: center; }
/* A class-level `display` beats the [hidden] attribute's UA rule, so without
   this the defer picker is open before anyone asks for it. */
.affirm-defer[hidden] { display: none; }
.affirm-defer select { flex: 1; min-width: 0; }
/* Prior answers: present but folded away — re-answering should be informed,
   not led. */
.affirm-history { margin: 0 0 var(--space-3); font-size: 12px; }
.affirm-history summary { cursor: pointer; color: var(--secondary); }
.affirm-history pre { white-space: pre-wrap; font: inherit; font-size: 12px; margin-top: var(--space-2); }
.affirm-actions[hidden] { display: none; }
.affirm-bundle {
    display: inline-block; font-size: 11px; text-transform: uppercase;
    letter-spacing: .06em; color: var(--secondary); margin-bottom: var(--space-2);
}

/* A flyout whose controls are pinned: full height, so the footer sits at the
   bottom of the screen rather than wherever the content happens to end. */
.flyout--tall { height: calc(100vh - var(--content-top) - 20px); }
.flyout-foot {
    flex: 0 0 auto;
    padding: var(--space-3) var(--space-5) var(--space-5);
    background: var(--surface);
    border-top: 1px solid var(--border);
    border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
.flyout-foot:empty { display: none; }

/* Memory (#926) — subjects first, then the statements inside one. */
.mem-intro { font-size: 12px; margin: 0 0 var(--space-3); }
.mem-subject {
    display: flex; align-items: center; gap: var(--space-3); width: 100%;
    padding: var(--space-3); margin-bottom: var(--space-2);
    background: transparent; border: 1px solid var(--border);
    border-radius: var(--radius-sm); cursor: pointer; text-align: left;
    font: inherit; color: var(--primary);
    transition: border-color var(--t-fast), background var(--t-fast);
}
.mem-subject:hover { border-color: var(--secondary); background: var(--muted); }
.mem-subject-name { flex: 1; min-width: 0; font-size: var(--text-sm); }
.mem-subject-count {
    flex: 0 0 auto; min-width: 22px; padding: 0 6px; text-align: center;
    font-size: 11px; font-weight: 700; line-height: 18px;
    border-radius: 9px; background: var(--muted); color: var(--secondary);
}
.mem-claim { padding: var(--space-3) 0; border-bottom: 1px solid var(--hairline); }
.mem-claim:last-child { border-bottom: 0; }
.mem-claim-text { margin: 0 0 var(--space-1); font-size: 13px; line-height: 1.5; }
.mem-claim-meta { font-size: 11px; margin: 0 0 var(--space-2); }
.mem-claim-verdict { font-size: 11px; margin: 0; }
.mem-claim-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }
/* A decided statement stays visible but recedes — this is a record, not an inbox. */
.mem-claim.is-kept .mem-claim-text { color: var(--primary); }
.mem-claim.is-discarded .mem-claim-text { color: var(--secondary); text-decoration: line-through; }
.mem-done { font-size: 12px; margin: 0 0 var(--space-3); }

/* .app-main--flush — a template that owns the whole main box (chat: the
   transcript scrolls edge to edge) sets $main_class = "app-main--flush" and
   the shell puts it on <main>. Last in this sheet on purpose: it has to come
   after every .app-main padding rule above, including the responsive ones,
   and it deliberately stays at one class of specificity so body.flyout-open
   and body.docked-open still win their padding-right, as they did when this
   was an inline <style> in chat.php (#1309). */
.app-main--flush{padding:0}
