/* Ellington Clerk Workspace - server-rendered rebuild (Task R1, KAN-86).
   Values below are pixel-truth extracted directly from the committed,
   content-hashed reference bundle (hub/appui/index.html's inline styles +
   hub/appui/assets/dc-logic-precompiled.50a09bf7.js's TAG_COLORS/navItems
   objects), not hand-guessed from the general prose description in
   .superpowers/sdd/2026-08-13-lore-hub-workspace-rebuild/ui-fidelity-spec.md
   - that doc's "13px/700/0.39px" micro-label rule turns out to be the exact
   table-column-header style (13px, letter-spacing .03em == 0.39px at 13px);
   the Home screen's two card titles (TODAY'S SCHEDULE / RECENT ACTIVITY)
   are a related but distinct 15px/700/.03em rule in the real bundle - both
   are ported below as separate rules rather than force-unified to 13px.

   Zero inline JS/CSS: every color/spacing value here lives in this
   stylesheet, referenced from markup only via class names - never a
   style="..." attribute (tag/schedule pill colors are per-label CSS
   classes for exactly this reason; see hub.townflows.home.TAG_CSS_CLASS).

   Task 1 (docs/superpowers/plans/2026-08-21-workspace-reskin.md): the
   :root token block below and the shared shell/component-recipe rules
   that follow it are the Ellington Clerk Workspace MOCK's design instead
   (cited as TOKENS - that plan's Design Token Sheet section), under a new
   wsk-* class prefix so the two skins can coexist mid-migration without
   colliding. The R1/R2/R3 ws-* rules further below are UNCHANGED content
   styling, kept live until the task that owns each screen migrates it
   (plan Global Constraints); Task 7 deletes whatever ws-* remains once
   nothing references it. Task 2 migrated login/register/forgot-password/
   join onto their own wsk-auth-* section (below the shell/component-recipe
   rules) - .ws-form-error survives that migration on purpose, narrowed to
   its one remaining consumer (workspace/fragments/admin_users.html, out of
   Task 2's scope); see that rule's own comment. (Corrected, Task 4: this
   comment used to also carry .ws-wordmark as a same-reason survivor via
   workspace/admin.html - stale the moment Task 3 migrated admin.html onto
   the shared shell fragment and deleted .ws-wordmark alongside .ws-topbar/
   .ws-topbar-row, its last consumer having moved off all three at once;
   see the "page layout" section below for that deletion's own comment.) */

/* --- fonts (Task R1: copied from hub/appui/assets/ - see README's "Font
   provenance" section for source + sha256 of each file) ------------------ */
@font-face {
  font-family: "Atkinson Hyperlegible";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/atkinson-hyperlegible-regular.woff2") format("woff2");
}
@font-face {
  font-family: "Atkinson Hyperlegible";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/static/fonts/atkinson-hyperlegible-bold.woff2") format("woff2");
}
@font-face {
  font-family: "Atkinson Hyperlegible";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/atkinson-hyperlegible-italic.woff2") format("woff2");
}
@font-face {
  font-family: "Atkinson Hyperlegible";
  font-style: italic;
  font-weight: 700;
  font-display: swap;
  src: url("/static/fonts/atkinson-hyperlegible-bolditalic.woff2") format("woff2");
}

:root {
  /* Task 1 token layer (docs/superpowers/plans/2026-08-21-workspace-reskin.md
     Design Token Sheet, cited below as TOKENS), CORRECTED post-close (fix
     round 2, owner-spotted live + confirmed against the mock's own source):
     TOKENS was extracted from the mock's PRE-PATCH consts, which default to
     theme "red" ("Ellington Red"). The mock ALSO carries a replaceOnce patch
     layer ("Ellington Clerk Workspace.html", ~line 2334) applied on top of
     those consts, verified directly:
       "const ACCENT_THEMES = {\n  red: { label: 'Ellington Red', ... },"
       -> "const ACCENT_THEMES = {\n  indigo: { label: 'Indigo', ... },
           \n  red: { label: 'Brick Red', ... },"   (red's hex is UNCHANGED,
                                                       only its label moves)
       "const ACCENT_THEME_ORDER = ['red', 'green', 'navy', 'slate'];"
       -> "const ACCENT_THEME_ORDER = ['indigo', 'red', 'green', 'navy', 'slate'];"
       "accentTheme: 'red',"  ->  "accentTheme: 'indigo',"
     The mock the hub is actually gated against therefore ships with Indigo
     as its real default, not Ellington Red - --accent/-hover/-light below
     are Indigo accordingly.

     Full theme map (mock's ACCENT_THEME_ORDER, patched) - Task 6's theme
     switcher should read ITS options from this table, not re-derive them:
       indigo (DEFAULT) "Indigo"       accent #5b4fd6 / hover #473dbd / light #eeecfc
       red               "Brick Red"    accent #7a2428 / hover #5c1a1d / light #f0e2e2
       green             "Forest Green" accent #2f4a3a / hover #22352a / light #e7ede7
       navy              "Harbor Navy"  accent #2f3f5c / hover #243149 / light #e4e7ee
       slate             "Slate"        accent #4a4740 / hover #37342f / light #e9e6de
     Only the default (indigo) is wired up as :root custom properties below.
     Task 6 built the theme switcher (live html.wsk-theme-* class rules
     ~160-182 below) - this map is the switcher's data source, consumed by
     hub/townflows/prefs.py THEME_OPTIONS and the Settings page's swatch
     controls. The non-default four themes are referenced by CSS rules that
     apply their override custom properties conditionally, not dead weight. */
  --accent: #5b4fd6;
  --accent-hover: #473dbd;
  --accent-light: #eeecfc;

  /* Neutrals (TOKENS) - the wsk-* namespace every later reskin task
     consumes. Don't add a raw hex value to a new wsk-* rule below when one
     of these already names it. */
  --wsk-bg: #f8f8f8;
  --wsk-cream: #f3f0e6;
  --wsk-card: #ffffff;
  --wsk-border: #cfcfcf;
  --wsk-border-warm: #d8d3c8;
  --wsk-border-light: #e5e5e5;
  --wsk-border-row: #d9d6cc;
  --wsk-text: #242424;
  --wsk-heading: #1d1c17;
  --wsk-text-secondary: #495156;
  --wsk-meta: #636363;
  --wsk-icon: #8a8a8a;
  --wsk-disabled: #a39d8c;

  /* Phase-A aliases - kept ONLY so the R1/R2/R3 ws-* content rules further
     below (not yet migrated to wsk-* - see this file's own header comment)
     keep resolving to the exact same colors. Delete these eight lines in
     Task 7 alongside their last ws-* consumer, never before: "no old
     elements survive" (plan Global Constraints) is this task's mandate for
     the SHELL it replaces, not for content this task explicitly leaves
     alone ("Screen-content rules from R1/R2/R3 stay for now"). */
  --brand: var(--accent);
  --ink: var(--wsk-text);
  --muted: var(--wsk-text-secondary);
  --line: var(--wsk-border);
  --row-line: var(--wsk-border-row);
  --page-bg: var(--wsk-bg);
  --card-bg: var(--wsk-card);
  --header-band: var(--wsk-cream);
}

/* Task 6 (docs/superpowers/plans/2026-08-21-workspace-reskin.md): the four
   non-default Town Theme overrides - values read verbatim from this
   file's own :root comment above (the authoritative five-theme map; this
   is that map's first consumer, per the Task 6 dispatch instruction).
   Applied as an <html> class (hub.redirects.html_class_for) so a chosen
   theme recolors the ENTIRE app, not just the Settings page itself -
   live-verified against the mock: switching Town Theme there also
   recolors Home's active-tab underline, task action buttons, etc.
   (task-6-report.md). Higher specificity than the plain :root selector
   above (html.foo vs :root, both matching the same <html> element) is
   what lets these three custom properties actually override the indigo
   defaults - kept as three overridden custom properties per theme, not
   five duplicated component rulesets, so every existing
   var(--accent)/var(--accent-hover)/var(--accent-light) consumer already
   in this file (dozens of rules) picks up the swap for free.

   html.wsk-theme-indigo has no properties to override (indigo already IS
   the :root default) but still needs a real rule here, not just this
   comment mentioning it: hub.townflows.prefs.html_class renders the
   literal class "wsk-theme-indigo" on every page whose user hasn't chosen
   a different theme (the common case), and tests/test_css_coverage.py's
   dead-class scan requires every class token actually rendered in HTML to
   have a matching CSS selector somewhere in this file - a comment
   mentioning the name doesn't count (that scanner strips comments before
   tokenizing, on purpose, for exactly this reason). */
html.wsk-theme-indigo {
  /* intentionally empty - matches :root's own indigo defaults */
}
html.wsk-theme-red {
  --accent: #7a2428;
  --accent-hover: #5c1a1d;
  --accent-light: #f0e2e2;
}
html.wsk-theme-green {
  --accent: #2f4a3a;
  --accent-hover: #22352a;
  --accent-light: #e7ede7;
}
html.wsk-theme-navy {
  --accent: #2f3f5c;
  --accent-hover: #243149;
  --accent-light: #e4e7ee;
}
html.wsk-theme-slate {
  --accent: #4a4740;
  --accent-hover: #37342f;
  --accent-light: #e9e6de;
}

/* Task 6: Accessibility text-size steps - hub.townflows.prefs.
   TEXTSIZE_OPTIONS' own css_key values, applied as an <html> class the
   same way the theme classes above are. `zoom` (not a font-size/rem
   scale) - a CORRECTION over the brief's own speculative "font-size
   scaling classes" suggestion: this stylesheet uses raw px throughout
   (zero rem/em usage anywhere in this file, confirmed by grep), so a
   font-size change on <html> would visibly affect nothing; the mock
   itself achieves this same effect via an inline `zoom:` style on its own
   root app div (live-measured, task-6-report.md - min="1" max="1.5"
   step="0.125" slider driving a zoom ratio directly). `zoom` is an
   ordinary CSS property, legal here under this repo's strict CSP (which
   forbids INLINE style="..."/<style> tags, not which properties a
   LINKED stylesheet may declare) and reproduces the mock's actual
   whole-layout scaling exactly, rather than only scaling text - verified
   supported in the Chromium build this project's own Puppeteer fidelity
   harness runs. wsk-textsize-100 (zoom:1, the default) is listed
   explicitly rather than omitted so every html_class string
   hub.townflows.prefs.html_class can ever produce has a real, matching
   CSS rule (tests/test_css_coverage.py's dead-class scan requires this
   for every rendered class). */
html.wsk-textsize-100 { zoom: 1; }
html.wsk-textsize-112 { zoom: 1.125; }
html.wsk-textsize-125 { zoom: 1.25; }
html.wsk-textsize-137 { zoom: 1.375; }
html.wsk-textsize-150 { zoom: 1.5; }

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--page-bg);
  color: var(--ink);
  font-family: "Atkinson Hyperlegible", system-ui, sans-serif;
  font-size: 16px;
  line-height: 1.4;
}

a { color: inherit; }

/* --- shared shell (Task 1, 2026-08-21-workspace-reskin.md) ------------------
   workspace/fragments/shell_top.html, included by every workspace page.
   Replaces the old .ws-tabnav/.ws-topbar-actions/.ws-icon-btn/
   .ws-topbar-admin-link/.ws-tab* rules that used to live here (deleted, not
   kept alongside - "no old elements survive", plan Global Constraints) and
   the old bottom-floating .ws-searchbar-wrap (also deleted below; the
   search form now lives in row 1 here). At Task 1 close, .ws-topbar/
   .ws-topbar-row themselves were NOT yet deleted - workspace/admin.html
   (out of Task 1's scope, chartered into Task 3) still rendered its own
   header with them. (Corrected, Task 4: stale the moment Task 3 landed -
   admin.html now includes this same shared shell fragment too, and
   .ws-topbar/.ws-topbar-row/.ws-wordmark were deleted alongside that
   migration, their last consumer gone; see the "page layout" section
   below for that deletion's own comment, and tests/test_shell.py's
   admin-shell tests for the absence pin.) Layout/shape values here are
   TOKENS' (Design Token Sheet) Layout section, verbatim. */

.wsk-shell { background: var(--wsk-card); border-bottom: 1px solid var(--wsk-border); }

.wsk-shell-row1 {
  padding: 14px 40px 0;
  display: flex; align-items: center; gap: 24px;
}

/* Task 5 correction (shell polish, 2026-08-21-workspace-reskin.md): measured
   #1a1a1a, a fixed dark neutral - NOT var(--accent) (the mock's own inline
   style sets this literally, independent of whichever accent theme is
   active). No existing wsk-* neutral token names this exact value (closest
   is --wsk-heading #1d1c17, a distinct value) - recorded directly rather
   than adding a new token for a single one-off use. */
.wsk-logo {
  font-size: 19px; font-weight: 700; color: #1a1a1a; letter-spacing: .2px;
  white-space: nowrap;
}

/* DEMO badge (owner directive 2026-08-30, towns.is_demo - hub/control.py):
   renders beside the wordmark on EVERY workspace screen of a demo town
   (workspace/fragments/shell_top.html, server-side conditional - absent
   entirely for a production town). Same loud pill recipe as
   .ws-badge-breakglass above ("the loudest thing in the table" - a state
   nobody should be able to miss), restated under the wsk-* namespace with
   the accent TOKEN instead of that legacy rule's --brand so it recolors
   with the town's own theme like every other wsk-* accent surface.
   flex-shrink:0 because it's a row1 flex item sitting against
   .wsk-search-col's flex:1 - the label must never be crushed into an
   ellipsis, that's the whole point of it. */
.wsk-demo-badge {
  display: inline-block; flex-shrink: 0; padding: 2px 9px;
  border-radius: 999px; background: var(--accent); color: #fff;
  font-size: 12px; font-weight: 700; letter-spacing: .8px;
  white-space: nowrap;
}

/* Fix round 1 (coordinator-gated, 2026-08-21-workspace-reskin.md Task 5):
   the search bar was left-anchored right after the logo (flex:1 + max-width
   on .wsk-search ITSELF, with .wsk-shell-icons' own margin-left:auto
   absorbing all the leftover space AFTER the bar instead of splitting it
   around it) - live re-measurement via getBoundingClientRect (not inferred
   from inline-style text this time) found the mock wraps its search pill in
   a SEPARATE centering column: `<div style="flex:1 1 0%; display:flex;
   justify-content:center">` containing the actual `width:100%;
   max-width:560px` pill - the column absorbs ALL of row1's leftover flex
   space and centers the pill WITHIN it (pill center 616.7px at 1280
   viewport width = the center of the space between the logo and the icon
   cluster, not the page's own center - confirmed by the numbers: row1
   padding 40px each side, logo 83.4px wide, icon cluster 130px wide, one
   938.6px-wide centering column absorbs everything else, gap 24px x2
   already included). .wsk-search-col (shell_top.html) is that column;
   .wsk-shell-icons' own margin-left:auto is REMOVED below - redundant and
   wrong once the column does the real work, it was only ever compensating
   for the missing column's absence. */
.wsk-search-col { flex: 1 1 0%; display: flex; justify-content: center; min-width: 0; }
.wsk-search { position: relative; width: 100%; max-width: 560px; display: flex; align-items: center; }
/* Mobile task (owner queue Q13, 2026-08-22): hit area enlarged 16x16 ->
   24x24 (the a11y floor - "Touch targets >=24px" - this control was under it
   at every viewport, not just narrow ones, so the fix is unconditional, not
   media-gated). Provably zero pixel diff for the fidelity board: the box
   grows by +8px on each axis but stays CENTERED on the exact same point
   (left 16->12 is -4px, half of +8, so the box's own center is unmoved;
   top:50%+translateY(-50%) already self-centers on any height, so it holds
   automatically as height grows too) and the child <svg> stays a literal
   16x16, now centered inside the larger box via flex - only the invisible
   box grew, not a single rendered icon pixel. */
.wsk-search-icon {
  position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  width: 24px; height: 24px; padding: 0; margin: 0; border: none; background: none;
  appearance: none; color: var(--wsk-icon); cursor: pointer; line-height: 0;
  display: flex; align-items: center; justify-content: center;
}
/* padding 10px 14px 10px 42px (Task 5 correction: measured exactly off the
   mock's own input; was 0/16/0/44, one of Task 1's own disclosed
   inferences, reconciled here per that task's note to revisit at Task 5).
   height 40px, NOT 42 (fix round 1 correction, getBoundingClientRect this
   time, not inferred from padding+line-height arithmetic): the search pill
   is row1's OWN tallest child, so row1's entire auto-height (padding-top
   14 + tallest-child 40 = 54px measured, matching exactly) and therefore
   EVERY other row1 child's own align-items:center vertical position
   (logo, icon buttons) cascades from this one number - a 2px error here
   was silently inflating row1 by 2px and, since row2 (.wsk-tabs) starts
   flush against row1's own bottom with no gap between them, pushing the
   ENTIRE tab row down by the same 2px on top of its own separate error
   (see .wsk-tab below). */
/* color: #000, not var(--wsk-text) (fix round 1 correction): the mock's own
   input carries NO color property at all in its inline style (verified
   directly against the captured style attribute) - it renders on the
   browser's own default form-control text color, which pixel-sampling
   confirms is pure black (0,0,0), not this app's #242424 body-text token.
   A real, provable color difference (not a focus-state artifact): sampled
   at a fully-covered glyph pixel, hub read 36,36,36 where mock read
   0,0,0 - a summed distance well inside "real difference" territory. */
.wsk-search-input {
  width: 100%; height: 40px; font-size: 15px; font-family: inherit;
  color: #000; background: var(--wsk-card);
  border: 1.5px solid var(--wsk-border); border-radius: 999px;
  padding: 10px 14px 10px 42px;
}
.wsk-search-input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-light);
}

.wsk-shell-icons { display: flex; align-items: center; gap: 12px; }
/* Shared layout only (shape, border, centering) - size/background/color are
   NOT uniform across the three buttons (Task 5 correction: live
   measurement found three distinct recipes, not one shared 38px/white
   treatment), so those live on the three modifier classes below instead of
   here. */
.wsk-icon-btn {
  border-radius: 50%; border: 1px solid var(--wsk-border);
  display: inline-flex; align-items: center; justify-content: center;
}
/* Task 6.5 (Q8, owner queue 2026-08-22): renamed from .wsk-icon-help - same
   button (size/bg/color recipe unchanged), the icon inside it (shell_top.html)
   is now a chat/speech-bubble glyph instead of a help-circle "?". */
.wsk-icon-chat { width: 38px; height: 38px; background: var(--wsk-card); color: var(--accent); }
.wsk-icon-bell { width: 34px; height: 34px; background: none; color: var(--wsk-text-secondary); }
/* The third row1 icon slot (account) - accent-light tint distinguishes it
   from the two plain neutral icon buttons beside it, using only tokens
   already declared above (never a new hex value). Task 5 correction: 34px
   (was 38px, uniform with the other two before this task's own live
   measurement told them apart) - the icon itself also swapped to the
   mock's solid/filled person glyph (shell_top.html), not the outline one. */
.wsk-avatar { width: 34px; height: 34px; background: var(--accent-light); border-color: var(--accent-light); color: var(--accent); cursor: pointer; }

/* Account menu (fix round 1, post-Task-7): the mock's own avatar popover -
   <details>/<summary>, zero JS, strict CSP - see shell_top.html's own
   comment for the full rationale. .wsk-account-menu is the <details>'
   positioning anchor; .wsk-avatar (above) is now also the <summary>, so
   list-style/marker suppression lives here rather than touching that
   shared rule. Panel recipe (border/radius/shadow) measured directly off
   the live mock's own popover (2026-08-22, not TOKENS' auth-card shadow,
   which is a different, larger card): border 1px var(--wsk-border), r10,
   shadow 0 4px 16px rgba(38,36,29,.12), width 160px (narrower than the
   mock's own 200px - this panel holds one control, not a name/role header
   too; "Profile settings" has no hub route and is never stubbed). Sign
   out's own text color (#a33e3e) is measured off the mock's own popover
   (rgb(163,62,62) - byte-identical) - already this file's one established
   danger/destructive red (.wsk-btn-danger, .priority-high below), reused
   here rather than a new token. */
.wsk-account-menu { position: relative; }
.wsk-account-menu > summary { list-style: none; }
.wsk-account-menu > summary::-webkit-details-marker { display: none; }
/* Explicit, not relied-on-implicitly: a plain position:absolute rule on
   .wsk-account-menu-panel (below) has higher selector specificity than the
   UA stylesheet's own details:not([open]) > *:not(summary) hiding rule in
   at least some engines, which can leave the panel occupying layout space
   (and worse, visible) even while collapsed. This rule's own specificity
   (class + attribute) beats the plain class rule below, so collapsed is
   unambiguously hidden regardless of engine-specific UA-stylesheet
   behavior - verified needed, not decorative (live-checked against the
   running dev server: without this rule getComputedStyle reported
   display:block on the panel while the <details> had no [open] attribute). */
.wsk-account-menu:not([open]) > .wsk-account-menu-panel { display: none; }
.wsk-account-menu-panel {
  position: absolute; right: 0; top: 42px; width: 160px;
  background: var(--wsk-card); border: 1px solid var(--wsk-border);
  border-radius: 10px; box-shadow: 0 4px 16px rgba(38, 36, 29, .12);
  overflow: hidden; z-index: 5;
}
.wsk-account-menu-signout {
  display: block; width: 100%; text-align: left; background: none;
  border: none; padding: 12px 16px; font-size: 15px; font-weight: 700;
  font-family: inherit; color: #a33e3e; cursor: pointer;
}
.wsk-account-menu-signout:hover { background: var(--wsk-bg); }

/* Task 5 correction (shell polish): live-measured tab row is CONTENT-SIZED
   (gap:4px, no flex:1 stretch) with NO dividers between tabs at all - a
   correction over TOKENS' own documented "tabs flex:1 ... 2px dividers
   except last" layout note, which this investigation found stale against
   the CURRENT mock capture (same pattern as the Indigo default/auth-kicker
   letter-spacing corrections earlier tasks already made - TOKENS was
   extracted once, 2026-08-21, and the mock has visibly moved since for at
   least these elements). Padding 8px 32px 0 (was 12px 40px 0); each tab
   padding 10px 16px 12px (was 0 0 12px) with a leading icon (gap:8px,
   shell_top.html) and rounded top corners (radius 8px 8px 0 0) - all
   measured directly.

   Task 6.5 (Q12, owner queue 2026-08-22) FIRST PASS, RE-VERIFIED LIVE: the
   queue item asks to "verify tabs spread evenly across the full width...
   flex:1 should already do it; fix if uneven." Live re-measurement against
   the STILL-CURRENT mock (2026-08-22, same day) via getBoundingClientRect +
   getComputedStyle at the real 1280x900 gate viewport found the mock's own
   nav row's authored inline style is still literally `display: flex; gap:
   4px; padding: 8px 32px 0px` with NO flex-grow on any tab (each tab's own
   inline style carries no `flex` property at all, i.e. the CSS default
   `flex: 0 1 auto`) - content-sized, NOT evenly spread, confirmed byte-
   identical to the hub's own rendering: both sides measured tab widths
   [101.2, 100.4, 121.2, 106.8]px (Home/Email/Settings/Admin) at IDENTICAL
   x-offsets, leaving the same ~800px of unused nav-row width on the right
   in BOTH. This technical finding was correct and independently confirmed
   (auditor + tester); see below for why the DISPOSITION built on it was
   overturned.

   APPROVED DEVIATION #7 (controller adjudication, fix round 1, same day):
   Q12 is an OWNER DESIGN DIRECTIVE, not a fidelity question the mock gets
   to settle - double provenance: an annotated screenshot ("expand the 4
   buttons across") plus the dictated queue item's own "spread that evenly
   across the plane." Same posture as R-1 (El-bot -> indigo) and Q8 (chat
   icon): the mock loses. `.wsk-tab` now carries `flex: 1 1 0%` +
   `justify-content: center` (below) so each tab shares the nav row's full
   width evenly, at 3 tabs (non-admin) and 4 (admin) alike - the SAME
   unconditional rule governs both counts (no :nth-child/[href=...] override
   exists anywhere in this file - tests/test_shell.py::test_admin_tab_
   absent_for_non_admin_user already exercises the 3-tab markup shape
   structurally, so a second live capture at 3 tabs isn't needed to trust
   this generalizes). This is an INTENDED, NAMED difference from the mock on
   every shell-bearing screen from here on (home/email/email_detail/search/
   ask/settings/admin all include this same shared shell) - it will widen
   the measured chrome diff on all of them vs the mock's own left-packed
   tabs; that is the point of the override, never a defect to chase back
   down. See tools/e2e/fidelity.mjs's own settings/home driver-pair comments
   for the fidelity-report attribution this now carries, and
   test_shell.py::test_wsk_tab_flexes_evenly_and_carries_no_fixed_width_
   override for the pin. */
.wsk-tabs { display: flex; gap: 4px; padding: 8px 32px 0; }
/* height: 44px, EXPLICIT (fix round 1 correction, 2026-08-21-workspace-
   reskin.md Task 5): the earlier "line-height:normal" attempt (still kept,
   harmless) assumed the mismatch was body's own line-height:1.4 leaking in
   - it barely moved the number (8291->8274 diff px in that round's own
   measurement) because Atkinson Hyperlegible's OWN 'normal' line-height for
   16px/700 apparently already lands close to 1.4x anyway, so the override
   was correcting for a cause that wasn't the dominant one. getBoundingClientRect
   against the live mock (not inferred from inline-style padding text this
   time) measured the ACTUAL tab height directly: 44.4px (y=62 to
   bottom=106.4, tabsRow itself starting at y=54 with padding-top 8 ->
   content starts at 62). Setting height explicitly removes any dependency
   on this font's own line-height metrics entirely, rather than continuing
   to guess at them - align-items:center still centers the icon+label
   within it. Combined with the row1 fix above (row1 bottom now lands at
   the same y=54 on both sides, since .wsk-tabs starts flush against it),
   this is what actually closes the "whole tab row 3px lower" finding - the
   line-height change alone never could. */
.wsk-tab {
  display: inline-flex; align-items: center; justify-content: center;
  /* gap/font-size/padding read from --wsk-tab-* custom properties, each with
     a fallback equal to the ORIGINAL literal value (8px/16px/16px) - Mobile
     task (owner queue Q13): the mobile media block far below this file
     redefines these three on :root under its two breakpoints so this ONE
     rule (never duplicated - see test_shell.py::
     test_wsk_tab_flexes_evenly_and_carries_no_fixed_width_override, which
     asserts exactly one `.wsk-tab {` rule exists in the whole file) can
     shrink at narrow widths without a second selector. Outside those
     breakpoints the var()s resolve to their fallback, i.e. byte-identical
     to the values this rule always had - zero desktop/fidelity drift. */
  gap: var(--wsk-tab-gap, 8px);
  /* flex: 1 1 0% - approved deviation #7 (owner directive, this section's
     own header comment above): grow/shrink evenly, zero basis, so N tabs
     always split the row's available width equally regardless of each
     tab's own content width. justify-content:center (added alongside -
     text-align:center on its own does nothing for a flex container's own
     flex-item children, which is what the icon+label pair here are) keeps
     the icon+label centered WITHIN each tab's now-wider box; the 3px
     active border-bottom needs no separate change - a border-bottom
     inherently spans its own element's full width, so it automatically
     stretches to match. */
  flex: 1 1 0%;
  /* overflow: hidden (Mobile task, Q13) - NOT `min-width: 0` (a fixed-width
     regression risk this rule is explicitly pinned against by name - the
     literal substring "width:" is banned from this rule's body by the same
     test cited above, `assert "width:" not in body`): per the flexbox spec,
     a flex item's automatic min-width floor (its own content's min-content
     size, which is what stops `flex: 1 1 0%` from ever shrinking a tab
     below "Settings"'s own unbroken text width) resolves to 0 whenever the
     item's own overflow is anything other than `visible` - this achieves
     the identical "let this tab shrink below its label's width instead of
     forcing the row wider than the viewport" effect without ever writing
     that banned substring. Invisible at every width this file's own
     fidelity board measures (1280px): tabs never come close to their own
     min-content floor there, so nothing is ever actually clipped. */
  overflow: hidden;
  background: none; border: none; border-bottom: 3px solid transparent;
  border-radius: 8px 8px 0 0;
  font-size: var(--wsk-tab-font, 16px); font-weight: 700; color: var(--wsk-text-secondary);
  line-height: normal;
  height: 44px;
  padding: 0 var(--wsk-tab-pad-x, 16px); text-align: center; text-decoration: none;
  transition: transform .15s ease;
}
.wsk-tab-active { color: var(--accent); border-bottom-color: var(--accent); }
.wsk-tab:active { transform: scale(.96); }

/* --- shared component recipes (Task 1) ---------------------------------------
   wsk-* building blocks for later reskin tasks (buttons/chips/inputs/table
   header/card - TOKENS' Component recipes + Shape sections). NOT consumed
   by any screen-content markup yet: the R1/R2/R3 ws-* rules further below
   stay as the live content styling this task (plan Global Constraints:
   "Screen-content rules from R1/R2/R3 stay for now"); Tasks 2-6 migrate
   their own screen onto these one at a time, Task 7 deletes whatever ws-*
   is left. .wsk-btn-outline's padding/font-size are inferred from its
   sibling .wsk-btn-neutral (TOKENS states r8px explicitly for both, but
   gives neither an explicit padding, and only states 14/700 for
   .wsk-btn-neutral) rather than invented outright from nothing - recorded
   here since TOKENS itself doesn't spell it out. */

.wsk-btn-primary {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  background: var(--accent); color: #fff; border: 1px solid var(--accent);
  border-radius: 9px; padding: 10px 18px; font-size: 15px; font-weight: 700;
  font-family: inherit; text-decoration: none; cursor: pointer;
  transition: background-color .15s ease, border-color .15s ease, transform .15s ease;
}
.wsk-btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.wsk-btn-primary:active { transform: scale(.96); }

.wsk-btn-outline {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  background: #fff; color: var(--accent); border: 1.5px solid var(--accent);
  border-radius: 8px; padding: 10px 18px; font-size: 14px; font-weight: 700;
  font-family: inherit; text-decoration: none; cursor: pointer;
  transition: background-color .15s ease, transform .15s ease;
}
.wsk-btn-outline:hover { background: var(--accent-light); }
.wsk-btn-outline:active { transform: scale(.96); }

.wsk-btn-neutral {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  background: #fff; color: var(--wsk-text-secondary); border: 1px solid var(--wsk-border);
  border-radius: 8px; padding: 10px 18px; font-size: 14px; font-weight: 700;
  font-family: inherit; text-decoration: none; cursor: pointer;
  transition: border-color .15s ease, transform .15s ease;
}
.wsk-btn-neutral:hover { border-color: var(--accent); }
.wsk-btn-neutral:active { transform: scale(.96); }

.wsk-chip {
  display: inline-flex; align-items: center;
  background: var(--wsk-cream); border: 1px solid var(--wsk-border);
  border-radius: 999px; padding: 6px 14px; font-size: 14px;
  font-family: inherit; text-decoration: none; cursor: pointer;
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.wsk-chip-active { background: var(--accent); color: #fff; border-color: var(--accent); }

.wsk-input {
  width: 100%; font-family: inherit; font-size: 15px; color: var(--wsk-text);
  background: var(--wsk-card); border: 1.5px solid var(--wsk-border);
  border-radius: 8px; padding: 10px 12px;
}
.wsk-input:focus { border-color: var(--accent); }

.wsk-thead {
  background: var(--wsk-cream); border-bottom: 1px solid var(--wsk-border);
  padding: 12px 20px; font-size: 13px; font-weight: 700;
  color: var(--wsk-text-secondary); text-transform: uppercase; letter-spacing: .03em;
}

.wsk-card {
  background: var(--wsk-card); border: 1px solid var(--wsk-border);
  border-radius: 14px; padding: 20px;
  box-shadow: 0 2px 10px rgba(38, 36, 29, .05);
  transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease;
}
.wsk-card:hover {
  border-color: var(--accent); box-shadow: 0 2px 8px rgba(38, 36, 29, .08);
  transform: translateY(-1px);
}

/* Global a11y focus ring (TOKENS: "fixed, a11y" - not themeable, layered
   on top of any component's own focus styling, e.g. .wsk-search-input's
   accent-light glow above). :focus-visible so it shows for keyboard
   navigation, not every mouse click. */
:focus-visible { outline: 3px solid #0b57d0; outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  /* .wsk-auth-sso-btn added (Task 5 rider, from Task 2's own deferred
     minor): it declares a transition (border-color/transform, see the auth
     screens section below) just like the five listed beside it, and was
     missed from this kill-switch when Task 2 introduced it.
     .wsk-theme-swatch-btn added (Task 6): declares its own transform
     transition (settings section above) - added in the SAME commit that
     introduces it, not deferred to a later rider like .wsk-auth-sso-btn
     above had to be. */
  .wsk-tab, .wsk-btn-primary, .wsk-btn-outline, .wsk-btn-neutral, .wsk-chip, .wsk-card,
  .wsk-auth-sso-btn, .wsk-auth-town-btn, .wsk-theme-swatch-btn {
    transition: none;
  }
}

/* --- page layout ------------------------------------------------------------
   Task 3 (2026-08-21-workspace-reskin.md) migrated workspace/admin.html
   onto the shared shell fragment (workspace/fragments/shell_top.html,
   Task 1) - its own hand-rolled <header class="ws-topbar"><div
   class="ws-topbar-row"> is gone, and with it the last consumer of BOTH
   that pair AND .ws-wordmark (admin.html's <span class="ws-wordmark"> lived
   inside that same header; every other template that once rendered
   .ws-wordmark directly - login/register/forgot_password/join - already
   moved onto .wsk-auth-kicker/.wsk-auth-title in Task 2). All three rules
   are deleted here, not left "for Task 7" as their retention comments used
   to say - "dead CSS is removed in the same task that retires its last
   consumer" (plan Global Constraints), and Task 3 is that task now that
   admin.html's own shell migration was chartered into it. */

.ws-main {
  max-width: 1280px; margin: 0 auto; padding: 36px 40px 40px;
  display: flex; gap: 32px; align-items: flex-start;
}
.ws-content { flex: 1; display: flex; flex-direction: column; gap: 24px; min-width: 0; }
.ws-side { width: 300px; flex-shrink: 0; display: flex; flex-direction: column; gap: 20px; }

.ws-greeting { margin: 0 0 4px; font-size: 26px; font-weight: 700; }
.ws-date { margin: 0; font-size: 16px; color: var(--muted); }

.ws-card {
  background: var(--card-bg); border: 1px solid var(--line);
  border-radius: 14px; padding: 20px;
}
.ws-card-title {
  margin: 0 0 12px; font-size: 15px; font-weight: 700;
  letter-spacing: .03em; text-transform: uppercase; color: var(--muted);
}
.ws-muted { margin: 0; font-size: 15px; color: #636363; }

/* --- Home: page layout, greeting, generic card title/empty (Task 3,
   2026-08-21-workspace-reskin.md) --------------------------------------------
   wsk-* rewrite of R1's page shell for the one page that owns this content
   now. ws-main/ws-content/ws-side/ws-greeting/ws-date (further below,
   unchanged) stay defined too - admin.html (out of Task 3's charter beyond
   its own shell migration) is still their only remaining consumer.
   max-width 1400px is Home's own measured value (direct DOM measurement
   against the mock, 2026-08-21), not TOKENS' generic 1280px Layout figure -
   the four Task-1-migrated screens (email/email_detail/search/ask) keep
   1280 via the ws-main rule below, which they share with admin.html; only
   Home differs. */
.wsk-main {
  max-width: 1400px; margin: 0 auto; padding: 36px 40px 40px;
  display: flex; gap: 32px; align-items: flex-start;
}
.wsk-content { flex: 1; display: flex; flex-direction: column; gap: 24px; min-width: 0; }
.wsk-side { width: 300px; flex-shrink: 0; display: flex; flex-direction: column; gap: 20px; }

.wsk-greeting { margin: 0 0 4px; font-size: 26px; font-weight: 700; }
.wsk-date { margin: 0; font-size: 16px; color: var(--wsk-text-secondary); }

.wsk-card-title {
  margin: 0 0 12px; font-size: 15px; font-weight: 700;
  letter-spacing: .03em; text-transform: uppercase; color: var(--wsk-text-secondary);
}
.wsk-empty { margin: 0; font-size: 15px; color: var(--wsk-meta); }

/* --- My Tasks (Task 3) -------------------------------------------------------
   R1 rendered "My Tasks" as table rows (KAN-111 comment below, written
   against an OLDER reference than this task's own mock). The mock actually
   gated against here (verified live 2026-08-21) renders one CARD per task
   instead - this replaces the whole #tasks-card/ws-tasks-*/ws-select/
   ws-viewer-banner/ws-thead/ws-th-*/ws-row/ws-td-*/ws-task-text/
   ws-task-more/ws-due-input/ws-hide-inert/ws-suggestion-panel family for
   Home; ws-table-wrap/ws-table-scroll/ws-btn-ghost/ws-btn-primary/
   ws-btn-suggestion/ws-empty (further below) stay defined - email.html/
   fragments/email_list.html/admin_users.html/admin_invite.html still
   consume them. */
.wsk-tasks-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 12px;
}
.wsk-tasks-heading { margin: 0; font-size: 19px; font-weight: 700; color: var(--wsk-heading); }
.wsk-tasks-controls { display: flex; align-items: center; gap: 8px; }
.wsk-sort-select {
  font-size: 14px; font-family: inherit; padding: 0 10px; height: 38px;
  border-radius: 8px; border: 1.5px solid var(--wsk-border); background: var(--wsk-card);
  color: var(--wsk-text); cursor: pointer; min-width: 190px; flex-shrink: 0;
}
/* Full-strength, not dimmed: matches the Task 1 shell's own inert-control
   convention (wsk-icon-btn's help/notification/account controls carry no
   opacity reduction either) and the mock's own "+ Add task" rendering
   (measured live, 2026-08-21 - no opacity property at all) - R1's OLDER
   ws-btn-ghost[aria-disabled] pattern (further below, opacity .5) predates
   both and is NOT the convention this task's new wsk-* inert controls
   follow. pointer-events:none is kept (harmless on the <span> this
   modifies - neither natively interactive nor actually clickable either
   way) purely as a second, redundant signal of non-interactivity. */
.wsk-btn-outline[aria-disabled="true"], .wsk-btn-neutral[aria-disabled="true"] {
  pointer-events: none;
}

.wsk-delegated-notice {
  margin: 0 0 10px; font-size: 14px; color: #8c5d26; background: #faf1de;
  border: 1px solid #8c5d26; border-radius: 8px; padding: 4px 6px 4px 12px;
  display: flex; align-items: center; gap: 8px;
}
.wsk-delegated-notice-text { flex: 1; min-width: 0; }
.wsk-notice-dismiss {
  width: 28px; height: 28px; flex: 0 0 28px; display: inline-flex;
  align-items: center; justify-content: center; border-radius: 6px;
  color: #6f5217; font-size: 20px; font-weight: 700; line-height: 1; cursor: not-allowed;
}

.wsk-tasks-list { display: flex; flex-direction: column; gap: 16px; }

.wsk-task-card { position: relative; padding: 20px 24px; }
.wsk-card-dismiss {
  position: absolute; top: 14px; right: 14px; width: 28px; height: 28px;
  border-radius: 50%; display: flex; align-items: center; justify-content: center;
  color: var(--wsk-icon); font-size: 18px; line-height: 1; cursor: not-allowed;
}
.wsk-task-tags { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 14px; padding-right: 32px; }
.wsk-task-body { padding-right: 32px; }
.wsk-task-title {
  margin: 0 0 8px; font-size: 19px; font-weight: 700; line-height: 1.35; color: var(--wsk-text);
  /* reference clamps titles at 3 lines; "Show more" (below) un-clamps - the
     mock's own scripted titles never run long enough to trigger this, but
     the hub's real corpus-derived ones do (KAN-111, ported from R1). */
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.wsk-task-body:has(.wsk-task-more[open]) .wsk-task-title { -webkit-line-clamp: unset; }
.wsk-task-more { margin-top: 4px; }
.wsk-task-more summary {
  display: inline; cursor: pointer; list-style: none;
  font-size: 14px; font-weight: 700; color: var(--wsk-text); text-decoration: underline;
}
.wsk-task-more summary::-webkit-details-marker { display: none; }
.wsk-task-more[open] summary { display: none; } /* reference swaps the link away once expanded */

.wsk-task-meta { display: flex; align-items: center; gap: 6px; margin-top: 12px; }
.wsk-task-received, .wsk-task-meta-sep { font-size: 14px; color: var(--wsk-icon); white-space: nowrap; }
.wsk-task-due-label { font-size: 14px; font-weight: 700; color: var(--wsk-text); white-space: nowrap; }
.wsk-task-due-input {
  /* Fixed styling for every row rather than the mock's conditional overdue
     red: hub.townflows.bootstrap.derive_tasks() already computes a
     per-task dueSort offset that would make real overdue detection
     possible, but hub.townflows.home.task_row() doesn't surface it on the
     row shape this template receives, and adding that is a townflow
     change outside this task's "townflows untouched" scope - see
     task-3-report.md. Bold + dark (not muted) still gives the field more
     visual weight than the plain received-date beside it, without
     asserting an urgency status this row shape can't back up. */
  width: 90px; text-align: left; font-size: 14px; font-weight: 700; color: var(--wsk-text);
  padding: 2px 26px 2px 6px; border: none; background: transparent no-repeat right 4px center;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%238a8a8a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E");
  font-family: inherit;
}

.wsk-task-divider { height: 1px; background: var(--wsk-border-light); margin: 16px 0; }
.wsk-task-actions { display: flex; align-items: center; gap: 10px; }
.wsk-task-action {
  background: var(--accent); color: #fff; border: none; border-radius: 8px;
  padding: 10px 20px; font-size: 14px; font-weight: 700; text-decoration: none;
  display: inline-flex; align-items: center; cursor: pointer;
}
.wsk-task-action:hover { background: var(--accent-hover); }
.wsk-task-action-secondary {
  background: #fff; color: var(--accent); border: 1.5px solid var(--accent); border-radius: 8px;
  padding: 8.5px 16px; font-size: 14px; font-weight: 700; font-family: inherit;
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
}
.wsk-task-action-secondary:hover { background: var(--accent-light); }

.wsk-suggestion-panel:empty { display: none; }
.wsk-suggestion-panel {
  margin-top: 14px; padding: 10px 14px; font-size: 13.5px; color: var(--wsk-text-secondary);
  background: var(--wsk-bg); border: 1px solid var(--wsk-border-row); border-radius: 8px;
}

/* --- Today's Schedule (Task 3) -----------------------------------------------
   The mock's hour grid uses continuous style="top:Npx" placement (its own
   inline-style idiom - strictly forbidden here, see this file's header
   comment and workspace/fragments/macros.html's schedule_slot_key). Each
   event's vertical position is instead one of 21 fixed half-hour classes
   below; the 11 on-the-hour ones double as the gutter labels/gridlines
   (static, hardcoded in home.html) and any event landing exactly on the
   hour. 22px = half of the 44px/hour scale measured directly off the mock
   (8 AM-6 PM spans 440px there). */
.wsk-sched-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.wsk-sched-title-wrap { display: flex; align-items: center; gap: 8px; }
.wsk-sched-icon { flex-shrink: 0; color: var(--accent); }
.wsk-sched-title { margin: 0; font-size: 17px; font-weight: 700; color: var(--wsk-heading); }
.wsk-sched-nav { display: flex; align-items: center; gap: 4px; }
.wsk-sched-nav-btn {
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--wsk-border); border-radius: 8px; color: var(--wsk-text-secondary);
  font-size: 14px; font-weight: 700; padding: 4px 8px; line-height: 1; cursor: not-allowed;
}

.wsk-sched-grid { position: relative; display: flex; gap: 10px; }
.wsk-sched-gutter { position: relative; width: 52px; flex-shrink: 0; height: 440px; }
.wsk-sched-hour-label {
  position: absolute; left: 0; transform: translateY(-50%);
  font-size: 11px; color: var(--wsk-icon); white-space: nowrap;
}
.wsk-sched-body { position: relative; flex: 1; height: 440px; border-left: 1px solid var(--wsk-border-light); }
.wsk-sched-gridline { position: absolute; left: 0; right: 0; border-top: 1px solid #ececec; }
.wsk-sched-event {
  position: absolute; left: 6px; right: 2px; height: 40px; border-left: 3px solid currentColor;
  border-radius: 4px; padding: 3px 8px; box-sizing: border-box; overflow: hidden;
}
.wsk-sched-event-time { margin: 0; font-size: 11px; font-weight: 700; color: inherit; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wsk-sched-event-title {
  margin: 0; font-size: 13px; font-weight: 700; color: var(--wsk-heading); line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical;
}

.wsk-sched-t-0800 { top: 0px; }
.wsk-sched-t-0830 { top: 22px; }
.wsk-sched-t-0900 { top: 44px; }
.wsk-sched-t-0930 { top: 66px; }
.wsk-sched-t-1000 { top: 88px; }
.wsk-sched-t-1030 { top: 110px; }
.wsk-sched-t-1100 { top: 132px; }
.wsk-sched-t-1130 { top: 154px; }
.wsk-sched-t-1200 { top: 176px; }
.wsk-sched-t-1230 { top: 198px; }
.wsk-sched-t-1300 { top: 220px; }
.wsk-sched-t-1330 { top: 242px; }
.wsk-sched-t-1400 { top: 264px; }
.wsk-sched-t-1430 { top: 286px; }
.wsk-sched-t-1500 { top: 308px; }
.wsk-sched-t-1530 { top: 330px; }
.wsk-sched-t-1600 { top: 352px; }
.wsk-sched-t-1630 { top: 374px; }
.wsk-sched-t-1700 { top: 396px; }
.wsk-sched-t-1730 { top: 418px; }
.wsk-sched-t-1800 { top: 440px; }

/* --- Quick Check (KAN-111), wsk-* rename (Task 3) -----------------------------
   Not part of the 2026-08-21 mock's own Home (verified live - no third
   sidebar card there today); kept as real, working, already-shipped
   functionality (hub.townflows.confirmations) rather than removed - see
   task-3-report.md's residual-attribution notes. Colors unchanged from the
   rule this replaces (amber #faf1de/#8c5d26/#e4cfa0 - the OLDER reference
   this card was originally fidelity-checked against, not this task's own
   mock capture, which has no equivalent card at all).

   Task 6.5 (Q10, owner queue 2026-08-22): the section is now absent
   entirely (workspace/home.html) when there's nothing to check, rather
   than always rendering with an empty-state line - .wsk-quickcheck-empty's
   last consumer went with it, so that rule is deleted here ("dead CSS is
   removed in the same task that retires its last consumer", plan Global
   Constraints). */
.wsk-card-quickcheck { background: #faf1de; border-color: #8c5d26; display: flex; flex-direction: column; gap: 14px; }
.wsk-quickcheck-title { margin: 0; color: #8c5d26; border-bottom: 1px solid #e4cfa0; padding-bottom: 10px; }
.wsk-quickcheck-q { margin: 0; font-size: 15px; color: #26241d; line-height: 1.45; }

/* --- shared remnants of R1's "My Tasks card" section - still consumed by
   admin_users.html/admin_invite.html (ws-btn-primary/ws-btn-suggestion/
   ws-empty) and admin_users.html alone (ws-table-wrap/ws-table-scroll)
   even though Home (their original owner) no longer uses any of them.
   Not deletable here - "dead CSS is removed in the same task that retires
   its LAST consumer" (plan Global Constraints), and none of these four is
   at zero consumers yet. (Task 4, 2026-08-21-workspace-reskin.md: email.html/
   fragments/email_list.html - this comment's other two-screen consumer
   until now - migrated onto their own wsk-btn-ghost/wsk-table-wrap/
   wsk-table-scroll in the Email screen section further below, which ARE
   genuinely new rules, not renames of these; that migration retired
   ws-btn-ghost's own last consumer, so it and its [aria-disabled] modifier
   are deleted here rather than kept alongside a fourth ws-* screen that no
   longer exists.) Everything else that used to live in this section (the
   #tasks-card reset, ws-tasks-*, ws-select, ws-viewer-banner, ws-thead/
   ws-th-*, ws-row, ws-td-*, ws-task-text/-more, ws-due-input, ws-hide-inert,
   ws-suggestion-panel) was Home-exclusive and is replaced by the wsk-*
   rules above. */

.ws-table-wrap {
  background: var(--card-bg); border: 1px solid var(--line);
  border-radius: 12px; /* measured off the reference (12px), was 14 */
  overflow: hidden;
}
.ws-table-scroll { overflow-x: auto; padding-bottom: 6px; }

.ws-btn-primary {
  flex-shrink: 0; min-width: 92px; text-align: center;
  background: var(--brand); color: #fff; border: none; border-radius: 7px;
  padding: 6px 10px; font-size: 13px; font-weight: 700; text-decoration: none;
  display: inline-flex; align-items: center; justify-content: center; gap: 4px;
}
.ws-btn-suggestion {
  flex-shrink: 0; min-width: 112px; text-align: center;
  background: var(--card-bg); color: var(--brand); border: 1px solid var(--brand);
  border-radius: 7px; padding: 6px 10px; font-size: 13px; font-weight: 700;
  font-family: inherit; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; gap: 4px;
}
.ws-empty { margin: 0; padding: 16px 20px; font-size: 15px; color: #636363; }

/* --- tag pills (hub.townflows.home.TAG_CSS_CLASS) --------------------------- */

/* wsk-tag (Task 3): the one tag-chip pill shape, wsk-* namespace - Home's
   markup was already 100% wsk-* at Task 3 close; ws-tag (the R1/R2-era
   twin this replaced) stayed alive only because email.html/email_detail.html/
   fragments/email_list.html still consumed it, until Task 4
   (2026-08-21-workspace-reskin.md) migrated those three onto this same
   rule (and the wsk_tag_chip macro, fragments/macros.html) - their last
   use retired, ws-tag is deleted here rather than kept alongside a screen
   that no longer references it. Shares the SAME bare tag-* color modifiers
   below (those never carried a ws-/wsk- prefix of their own -
   hub.townflows.home.TAG_CSS_CLASS names them generically), so there is
   exactly one color decision per label regardless of which screen renders
   it. */
.wsk-tag {
  display: inline-block; font-size: 13px; font-weight: 700; padding: 3px 9px;
  border-radius: 999px; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; max-width: 100%;
}
.tag-project        { background: #e3ece4; color: var(--brand); }
.tag-meeting-minutes{ background: #e3ece4; color: var(--brand); }
.tag-personal        { background: #f1e6d2; color: #7f612b; }
.tag-budget-finance  { background: #f1e6d2; color: #7f612b; }
.tag-vital-records   { background: #e2e8ee; color: #4c5f73; }
.tag-land-records    { background: #e8e2d3; color: #766334; }
.tag-permits-zoning  { background: #eadfe9; color: #6d4f7a; }
.tag-foia-general    { background: #f5e6d3; color: #8c5d26; }
.tag-reference        { background: #eeecdd; color: #6e6b45; }
.tag-spam             { background: #ecebe4; color: #6c685e; }
.tag-default          { background: #eeeeee; color: #666666; }

/* --- auth screens: login/register/forgot-password/join (Task 2,
   2026-08-21-workspace-reskin.md) ------------------------------------------
   Card chrome measured directly off the mock's own sign-in screen 2026-08-21
   (r20, border #cfcfcf, shadow `0 2px 16px rgba(38,36,29,.07)`, 420px card,
   padding 48px vertical / 40px horizontal - TOKENS' shorthand "padding 40"
   turned out to mean that once measured against the live computed style,
   not a uniform 40 on every side). These four pages don't use the workspace
   shell (Task 1's wsk-shell/wsk-tabs) - "consumes the token layer only" per
   this task's own plan entry - so this section owns a small card-specific
   set (wsk-auth-*) and reuses .wsk-input/.wsk-btn-primary (Task 1) for the
   actual form controls rather than re-inventing them. Replaces the old
   task-login-report.md (2026-08-13) .login-* family below this comment
   used to be - deleted, not kept alongside ("no old elements survive",
   plan Global Constraints); .ws-form-error is the one rule that family
   shared with a page OUTSIDE this task's scope
   (workspace/fragments/admin_users.html) and stays separate, unrenamed.

   Deliberately NOT literally what the mock renders, twice over: (1) the
   mock's card has no password form at all - a scripted demo with two fake
   SSO buttons and nothing else - while this hub always needs a real one
   for towns without SSO (KAN-11's show_password_form gate,
   hub/routes/pages.py, untouched by this task); (2) the mock hardcodes its
   title to its one demo town ("Ellington") - hub/templates/login.html's
   /login is shared by every municipality on the box, so its title stays
   the honest, town-agnostic "Sign in" (tests/test_pages.py::
   test_login_wordmark_is_company_brand_not_a_town_name pins "Ellington"
   never appearing there). The kicker ("Slayer Digital", company brand) and
   the card chrome itself ARE ported verbatim - both are honestly
   town-agnostic in the mock too. join.html is the one screen that DOES
   know its one specific town (the invite token already fixed it) and
   names it in the title, same wsk-auth-title class. */

.wsk-auth-page {
  background: var(--wsk-bg);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
  box-sizing: border-box;
}

.wsk-auth-shell { width: 100%; max-width: 420px; }

.wsk-auth-card {
  background: var(--wsk-card);
  border: 1px solid var(--wsk-border);
  border-radius: 20px;
  box-shadow: 0 2px 16px rgba(38, 36, 29, .07);
  padding: 48px 40px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  box-sizing: border-box;
}

/* letter-spacing .1em RECONFIRMED (Task 5 rider, 2026-08-21-workspace-
   reskin.md): TOKENS' Design Token Sheet documents kickers generically as
   .05em, and Task 2's own report deferred this exact value as an
   undisclosed discrepancy to reconcile here. Live re-measurement against
   the mock's own sign-in card kicker ("Slayer Digital", a fresh/cleared-
   localStorage session, 2026-08-21) reads letter-spacing: 0.1em directly
   off the rendered element's own inline style - Task 2's shipped value was
   already correct; TOKENS' generic .05em figure was the stale one for THIS
   specific kicker (same pattern as the Search zero-state El-bot kicker
   nearby, independently measured at .04em, not .05em either - TOKENS' one
   "kickers" figure doesn't hold uniformly across every kicker-shaped
   element the mock actually renders). No code change; this comment is the
   documented reconciliation the task's own instructions asked for. */
.wsk-auth-kicker {
  margin: 0;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--wsk-meta);
}

.wsk-auth-title {
  margin: 8px 0 0;
  font-size: 24px;
  font-weight: 700;
  color: var(--accent);
  letter-spacing: .3px;
}

/* Body copy under the header: login's subtitle, forgot-password's longer
   notice, the SSO "sign in with your work account" line. First occurrence
   in the card carries the mock's own measured subtitle rhythm (16px above,
   28px below); a second copy block later in the same card (there's at most
   one today) doesn't need that large a leading gap. */
.wsk-auth-copy {
  margin: 16px 0 28px;
  font-size: 16px;
  line-height: 1.5;
  color: var(--wsk-text-secondary);
  text-align: center;
}
.wsk-auth-copy ~ .wsk-auth-copy { margin: 0 0 20px; }

.wsk-auth-error {
  width: 100%;
  box-sizing: border-box;
  margin: 0 0 20px;
  padding: 10px 14px;
  border-radius: 8px;
  font-size: 14px;
  color: var(--accent);
  background: #f7e9e9;
  border: 1px solid #e3b9ba;
}
/* .ws-form-error: still used directly by
   workspace/fragments/admin_users.html (out of this task's scope) - same
   visual treatment, kept as its own selector rather than folded in. */
.ws-form-error {
  margin: 0;
  padding: 10px 14px;
  border-radius: 8px;
  font-size: 14px;
  color: var(--accent);
  background: #f7e9e9;
  border: 1px solid #e3b9ba;
}

/* The SSO entry point is an <a>, not a <button> - it starts a GET flow
   (/auth/sso/start) rather than submitting a form. Styled to match the
   mock's own "Continue with X" buttons exactly (measured 2026-08-21):
   neutral white/bordered, not accent-filled - on this card it's often the
   ONLY affordance, so it doesn't need a fill to read as primary. */
.wsk-auth-sso-btn {
  width: 100%;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  background: var(--wsk-card);
  border: 1.5px solid var(--wsk-border);
  border-radius: 10px;
  padding: 14px 20px;
  font-size: 18px;
  font-weight: 700;
  font-family: inherit;
  color: var(--wsk-text);
  text-decoration: none;
  cursor: pointer;
  transition: border-color .15s ease, transform .15s ease;
}
.wsk-auth-sso-btn:hover { border-color: var(--accent); }
.wsk-auth-sso-btn:active { transform: scale(.96); }
.wsk-auth-sso-btn svg { flex-shrink: 0; }

/* Municipality picker (2026-08-24, owner report "I cant login thru
   microsoft in the regular sign in"): the bare /login of a box serving
   more than one town, where there is no single town to resolve and so no
   provider button to show yet. Same neutral card-button recipe as
   .wsk-auth-sso-btn above - these ARE the same kind of affordance (one
   tap, one destination), just one step earlier in the same journey - at a
   slightly smaller size, because a list of five reads as a list rather
   than as five primary actions. Not folded into that selector because the
   two are gated independently and only one of them is ever the sign-in
   itself; no mock reference (the mock is a single-town demo screen). */
.wsk-auth-townlist {
  width: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.wsk-auth-townlist-item { margin: 0; }
.wsk-auth-town-btn {
  width: 100%;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--wsk-card);
  border: 1.5px solid var(--wsk-border);
  border-radius: 10px;
  padding: 12px 20px;
  font-size: 16px;
  font-weight: 700;
  font-family: inherit;
  color: var(--wsk-text);
  text-decoration: none;
  cursor: pointer;
  transition: border-color .15s ease, transform .15s ease;
}
.wsk-auth-town-btn:hover { border-color: var(--accent); }
.wsk-auth-town-btn:active { transform: scale(.96); }

/* Only ever rendered when the SSO link AND the password form are BOTH on
   screen at once - per show_password_form's own gate that's exactly the
   break-glass view of an SSO-configured town (see login.html's own
   comment); no counterpart in the mock (it never shows a password form to
   divide anything from), so this is original, token-only styling. */
.wsk-auth-divider {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  margin: 20px 0;
  color: var(--wsk-meta);
  font-size: 13px;
}
.wsk-auth-divider::before, .wsk-auth-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--wsk-border);
}

.wsk-auth-form { width: 100%; display: flex; flex-direction: column; gap: 20px; }
.wsk-auth-field { display: flex; flex-direction: column; gap: 6px; text-align: left; }

.wsk-auth-label {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .03em;
  text-transform: uppercase;
  color: var(--wsk-text-secondary);
}

/* Pairs with .wsk-btn-primary (Task 1) - the width/size modifier this card
   needs beyond that shared recipe's own (inline-flex, unsized) default. */
.wsk-auth-submit { width: 100%; font-size: 16px; padding: 13px 16px; }

/* register.html's "already registered? Sign in" footnote (Task B2) and
   friends (Forgot password?, Administrator sign-in, New municipality?) -
   every secondary link below the card's primary action(s). */
.wsk-auth-footnote { margin: 16px 0 0; text-align: center; font-size: 14px; color: var(--wsk-text-secondary); }
.wsk-auth-footnote-link { color: var(--accent); font-weight: 700; text-decoration: none; }
.wsk-auth-footnote-link:hover { text-decoration: underline; }

/* --- Town admin page (Task ADMIN, 2026-08-13) -------------------------------
   No demo reference (control-plane only, new-stack-native screen - see
   hub/routes/admin.py's module docstring) - the micro-label look below
   (13px/700/.03em letter-spacing/var(--muted) == the brief's "13px/700/
   0.39px #495156") independently redefines the same values .wsk-thead
   (Task 1's shared component recipe, further above) already carries,
   rather than composing it - a pre-existing duplication this rider
   doesn't resolve, just documents accurately. (Corrected, Task 4: this
   comment used to cite ".ws-thead/.ws-row" as My Tasks' still-live
   5-column table rule - stale since Task 3 replaced Home's My Tasks
   table with per-task wsk-* cards and deleted .ws-thead/.ws-row/.ws-th-*/
   .ws-td-* entirely; neither name exists in this file anymore. Email's
   own table header cells carry the same look too as of this task, see
   .wsk-eth below in the Email screen section.) */

.ws-admin-thead {
  display: flex; align-items: center; gap: 16px; padding: 12px 20px;
  background: var(--header-band); border-bottom: 1px solid var(--line);
  font-size: 13px; font-weight: 700; color: var(--muted); text-transform: uppercase;
  letter-spacing: .03em;
}
.ws-admin-row {
  display: flex; align-items: center; gap: 16px; padding: 14px 20px;
  border-bottom: 1px solid var(--row-line); flex-wrap: wrap;
}
.ws-admin-row:last-child { border-bottom: none; }
.ws-admin-col-user { flex: 1 1 160px; font-size: 15px; }
.ws-admin-col-scopes { flex: 1 1 160px; font-size: 14px; color: var(--muted); }
.ws-admin-col-actions { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; }

/* KAN-11: sign-in method + last use, per user row. The break-glass badge
   is deliberately the loudest thing in the table - it marks the ONE
   account that can still bypass the town's identity provider, and an
   admin should be able to spot it without reading. */
.ws-admin-col-signin { flex: 1 1 170px; font-size: 14px; color: var(--muted); }
.ws-admin-lastlogin { display: block; font-size: 12px; color: var(--muted); }
.ws-badge-breakglass {
  display: inline-block; padding: 2px 8px; border-radius: 999px;
  background: var(--brand); color: #ffffff; font-size: 12px; font-weight: 700;
}

.ws-pw-form { display: flex; align-items: center; gap: 6px; }
.ws-pw-status { font-size: 13px; color: var(--muted); }

.ws-input-sm {
  font-size: 14px; font-family: inherit; padding: 6px 10px; height: 34px;
  border-radius: 8px; border: 1.5px solid var(--line); background: var(--card-bg);
  color: var(--ink);
}

.ws-admin-add { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--row-line); }
.ws-admin-add-form { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 8px; }
.ws-admin-checkbox { display: flex; align-items: center; gap: 6px; font-size: 14px; color: var(--ink); }

/* "Invite employee" control (Task B2, KAN-14) - the generated link is
   shown ONCE (hub/routes/admin.py never persists the raw token, only its
   sha256), so it needs to read clearly as "copy this now", not blend in
   like ordinary body text. */
.ws-invite-note { margin: 0 0 10px; font-size: 14px; color: var(--muted); }
.ws-invite-link {
  margin: 0 0 16px; padding: 10px 14px; border-radius: 8px;
  background: var(--header-band); border: 1px solid var(--line);
  font-size: 14px; word-break: break-all;
}

/* --- Email screen (Task R2, KAN-87; reskinned onto the wsk-* namespace by
   Task 4, 2026-08-21-workspace-reskin.md) - values pixel-truth extracted
   from live DOM measurement against the mock (2026-08-21), same sourcing
   discipline as the Home rules above. Corrections this task's live walk
   made over the R2-era values it replaces: the page container measures
   1400px max-width (Email joins Home in that one Home-specific override
   over TOKENS' generic 1280px Layout figure - see that section's own
   comment); filter-pills-and-sort share ONE row via justify-content:
   space-between (measured) rather than R2's own KAN-111 finding of two
   stacked left-aligned rows - accurate for an older capture, not the
   current, owner-patched mock; the inbox grid's column template gained
   minmax(360px, 1fr) + a 1000px min-width (measured, was a bare 1fr /
   760px); Prev/Next measured NEUTRAL-toned (border/text match the page's
   ordinary ink/border tokens, no accent anywhere), while Refresh
   (email.html, reuses .wsk-btn-outline) measured genuinely accent-toned -
   two different mock button treatments R2's one shared .ws-btn-ghost never
   distinguished; the detail view's priority control gained a "Priority:"
   text label (measured) with no R2 equivalent. -------------------------- */

/* line-height: normal on the CONTAINER, not each child rule - live
   measurement (2026-08-21) found the mock's buttons/pills/headings render
   with the browser's normal line-height, while body's own line-height:1.4
   (Task 1's global reset) inherits into every wsk-* element here that
   doesn't override it, making pills ~2.6px taller than the mock's (34.6px
   vs 32px, computed style diff confirmed: "700 15px/21px ..." here vs
   "700 15px ..." there) - a per-element height difference that compounds
   down the whole page (toolbar 81.2px vs 76px, table start 8.8px lower)
   and manifests as a large false-positive fidelity diff band (measured:
   ~23% for just the header+row1 strip, both sides showing the SAME text,
   merely offset a few px vertically - a known pixel-diff pitfall, not a
   real visual defect). R2 already found and fixed this same root cause
   for .wsk-erow-subject/.wsk-erow-snippet specifically (their own
   "line-height: normal, not the body's inherited 1.4" comment, further
   below); this container-level rule extends the same fix to every OTHER
   wsk-* element Task 4 introduced (pills, toolbar labels, headings,
   buttons) without touching body's own global rule - out of this task's
   scope, and would affect every other screen too. */
.wsk-email-main, .wsk-detail-main {
  max-width: 1400px; margin: 0 auto; padding: 36px 40px 40px; line-height: normal;
}
.wsk-detail-main { max-width: 1000px; display: flex; flex-direction: column; gap: 20px; }

.wsk-email-head {
  display: flex; justify-content: space-between; align-items: center;
  margin: 0 0 18px;
}
.wsk-inbox-heading { margin: 0; font-size: 26px; font-weight: 700; color: var(--wsk-heading); }
.wsk-email-head-actions { display: flex; align-items: center; gap: 10px; }
.wsk-refreshed-label { font-size: 13px; color: #777777; white-space: nowrap; }

/* KAN-111 (carried forward): the mock does NOT wrap the email toolbar+table
   in a card - filters sit directly on the page background, and the table
   carries its own 12px-radius border (further below). #email-list-card
   (the <section> itself, unclassed) is the HTMX swap target. */
.wsk-email-toolbar {
  display: flex; justify-content: space-between; align-items: center;
  flex-wrap: wrap; gap: 12px; margin: 0 0 18px;
}
.wsk-pill-row { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.wsk-toolbar-label { font-size: 15px; color: var(--wsk-text-secondary); }

.wsk-pill {
  background: var(--wsk-card); color: var(--wsk-text); border: 1px solid var(--wsk-border);
  border-radius: 999px; padding: 6px 14px; font-size: 15px; font-weight: 700;
  font-family: inherit; text-decoration: none; cursor: pointer;
}
.wsk-pill-active { background: var(--accent); color: #fff; border-color: var(--accent); }

.wsk-table-wrap { border: 1px solid var(--wsk-border); border-radius: 12px; overflow: hidden; }
.wsk-table-scroll { overflow-x: auto; padding-bottom: 6px; }
/* KAN-112 (carried forward, corrected): the inbox table is a GRID matching
   the mock's own template - minmax(360px, 1fr) + a 1000px min-width
   (live-measured 2026-08-21; R2's own capture had a bare 1fr / 760px). */
.wsk-ethead, .wsk-erow {
  display: grid; grid-template-columns: 160px minmax(360px, 1fr) 230px 100px 150px;
  min-width: 1000px;
}
.wsk-ethead { background: var(--wsk-cream); border-bottom: 1px solid var(--wsk-border); }
/* 13px vertical, not the mock's own 10px: the mock wraps each header label
   in a <button> that carries its own padding and renders 24px tall, where
   the hub's bare <a> is 18px. Matching the padding number would leave the
   band 5px short; matching the RENDERED 45px band is what actually lines
   the two tables up (R2 finding, still true). */
.wsk-eth, .wsk-eth-tagspace {
  background: none; border: none; text-decoration: none; cursor: pointer;
  font: inherit; font-size: 13px; font-weight: 700; color: var(--wsk-text-secondary);
  text-transform: uppercase; letter-spacing: .03em; padding: 13px 16px;
}
.wsk-eth-priority { text-align: center; }
/* Vertical cell dividers (measured 2026-08-21, #e2dfd6) - :not(:last-child)
   on the grid's own direct children rather than a fifth per-cell class, so
   the date column (always last) never gets a trailing divider on either
   row type. */
.wsk-ethead > *:not(:last-child), .wsk-erow > *:not(:last-child) {
  border-right: 1px solid #e2dfd6;
}

.wsk-erow { border-bottom: 1px solid var(--wsk-border-row); text-decoration: none; color: inherit; }
/* Zebra striping - the mock alternates #fff / #fafafa (R2 finding, still
   true). nth-of-type, not nth-child: the rows share a parent with the
   header, so nth-child would invert the stripe. Hover must out-specify
   both stripes, hence it follows them. */
.wsk-erow:nth-of-type(odd)  { background: #fff; }
.wsk-erow:nth-of-type(even) { background: #fafafa; }
.wsk-erow:hover { background: #f2ede1; }
.wsk-erow-dot { display: inline-block; width: 12px; color: var(--accent); font-size: 11px; }
.wsk-erow-from {
  padding: 13px 16px; font-size: 15px; color: var(--wsk-text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wsk-erow-subject-wrap { padding: 11px 16px; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
/* line-height: normal, not the body's inherited 1.4 (R2 finding, still
   true - see git history for the exact px math this fixes). */
.wsk-erow-subject {
  font-size: 15px; line-height: normal; color: var(--wsk-text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wsk-erow-snippet {
  font-size: 15px; line-height: normal; color: var(--wsk-meta);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wsk-erow-bold { font-weight: 700; }
.wsk-erow-tag { padding: 11px 16px; display: flex; align-items: center; }
.wsk-erow-priority { padding: 11px 12px; text-align: center; }
.wsk-erow-date { padding: 13px 16px; font-size: 15px; color: var(--wsk-meta); }

/* Pairs with the bare priority-high/-medium/-low/-none modifiers directly
   below (hub.townflows.email_list.PRIORITY_META names them; unchanged
   values from the R2 .ws-priority-* rules this replaces, just renamed -
   these never carried a ws-/wsk- prefix of their own, same convention as
   the tag-* modifiers above). */
.wsk-priority { display: inline-block; font-size: 13px; font-weight: 700; padding: 3px 9px; border-radius: 999px; }
.priority-high   { background: #f4dede; color: #a33e3e; }
.priority-medium { background: #f5e6d3; color: #8c5d26; }
.priority-low    { background: #eeecdd; color: #6e6b45; }
.priority-none   { background: transparent; color: #a39d8c; }

.wsk-pagination {
  display: flex; justify-content: space-between; align-items: center;
  flex-wrap: wrap; gap: 12px; padding: 14px 20px 20px;
}
.wsk-pagination-nav { display: flex; align-items: center; gap: 14px; }
/* Neutral-toned (measured 2026-08-21): border/text match the ordinary page
   ink/border tokens, no accent - unlike Refresh (email.html's own
   .wsk-btn-outline), which measured genuinely accent-toned. R2's one
   shared .ws-btn-ghost used accent for both; this corrects Prev/Next only. */
.wsk-btn-ghost {
  background: var(--wsk-card); color: var(--wsk-text); border: 1px solid var(--wsk-border);
  border-radius: 7px; padding: 6px 14px; font-size: 15px; font-weight: 700;
  font-family: inherit; text-decoration: none; cursor: pointer; display: inline-block;
}
.wsk-btn-disabled { color: var(--wsk-disabled); pointer-events: none; cursor: default; }

/* "Show N messages filtered as spam" (Task 4, new) - a plain text-button
   below pagination, live-measured 2026-08-21: no background/border of its
   own, just a top divider on its wrapping block. */
.wsk-spam-banner { margin-top: 18px; border-top: 1px solid var(--wsk-border); padding-top: 14px; }
.wsk-spam-banner-link {
  background: none; border: none; color: var(--wsk-text-secondary); font-size: 15px;
  font-family: inherit; padding: 4px 0; cursor: pointer; text-decoration: none; display: inline-block;
}
.wsk-spam-banner-link:hover { text-decoration: underline; }

/* --- Email detail / review screen (Task 4 rename; values unchanged from
   R2 except the added "Priority:" label and the two button treatments
   below - see this section's own header comment for what live measurement
   corrected). -------------------------------------------------------- */

/* padding: 4px 0 (measured 2026-08-21) - without it this renders 8px
   shorter than the mock's own back-link, an offset that cascades down
   through the whole detail card below (confirmed via getBoundingClientRect:
   card top 181.6px here vs 189.2px there before this fix). */
.wsk-back-link {
  align-self: flex-start; font-size: 16px; color: var(--wsk-text-secondary);
  text-decoration: none; padding: 4px 0;
}
/* padding overrides .wsk-card's uniform 20px - measured 2026-08-21: this
   card is 20px vertical / 24px horizontal, not uniform; left at 20px
   uniform it shifts every text line 4px, visible as a "ghosting" diff
   against the mock at the pixel level even though nothing else about the
   card's position was wrong. border-radius likewise measured 12px, not
   .wsk-card's 14px. */
.wsk-detail-card { display: flex; flex-direction: column; gap: 0; padding: 20px 24px; border-radius: 12px; }
.wsk-detail-head {
  display: flex; justify-content: space-between; align-items: baseline;
  flex-wrap: wrap; gap: 6px; margin-bottom: 8px;
}
.wsk-detail-sender { font-size: 15px; font-weight: 700; color: var(--wsk-heading); }
.wsk-detail-date { font-size: 13px; color: var(--wsk-meta); }
.wsk-detail-subject { margin: 0 0 10px; font-size: 17px; font-weight: 700; }
.wsk-detail-meta { display: flex; justify-content: flex-end; align-items: center; gap: 10px; margin: 0 0 12px; }
.wsk-detail-priority-label { font-size: 14px; color: var(--wsk-text-secondary); }
.wsk-detail-body { margin: 0; font-size: 16px; white-space: pre-line; line-height: 1.5; }
.wsk-detail-actions {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--wsk-border-row);
}
/* No mock reference for the 4 buttons below - its own detail view exposes
   Reply/Forward compose triggers instead, neither wired here (see
   email_detail.html's own header comment for why they're not stubbed).
   Mark-as-unread/Archive/Report-spam reuse .wsk-btn-outline (Task 1's
   shared recipe) rather than a bespoke lookalike; Delete gets its own
   danger tint, same red family R2's .ws-btn-danger used. */
.wsk-btn-danger {
  background: var(--wsk-card); color: #a33e3e; border: 1px solid var(--wsk-border);
  border-radius: 7px; padding: 6px 10px; font-size: 13px; font-weight: 700;
  font-family: inherit; cursor: pointer;
}

.wsk-attachment-card { display: flex; flex-direction: column; gap: 8px; background: #faf3f3; border-color: #d9baba; }
.wsk-attachment-link { color: var(--accent); font-size: 14px; font-weight: 700; text-decoration: underline; }

.wsk-draft-panel { display: flex; flex-direction: column; gap: 8px; }
.wsk-draft-body { margin: 0; font-size: 16px; white-space: pre-line; line-height: 1.5; }



/* Search screen (Task 5, 2026-08-21-workspace-reskin.md) - live-measured
   against the mock (2026-08-21), replacing this section's earlier "no
   fidelity capture exists yet" design-judgment placeholder. Content column
   is 1400px (measured directly: the "N results for..." paragraph's own
   parent carries max-width:1400px/padding:36px 40px 40px - the SAME figure
   Home/Email use, not TOKENS' generic 1280px Layout figure and not this
   section's own earlier 900px guess, which matched nothing once actually
   checked against a live DOM measurement).

   Result cards carry NO fourth badge element on the mock's own real-corpus-
   query results (verified against two different real queries, "dog license
   renewal" and "budget" - each just title/date/snippet, three elements,
   full stop). The uppercase kicker chip on each row here is real,
   unfabricated hub metadata (r.scope, already part of the engine's result
   shape) rendered in the mock's OWN kicker visual language rather than
   omitted - a directed addition (task brief), not a mock-observed one; see
   tests/test_workspace_search_ask_style.py's module docstring for the full
   writeup, including two other corrections this same investigation made
   over the plan's prose (the zero-state card is accent-themed, not oxblood;
   the follow-up chips on the Ask page below are accent pills, not oxblood
   callouts). r.heading_path (a short breadcrumb-like string the engine
   already returns) stands in for the position the mock's own date sits in
   - an honest substitution of one real, already-available field for
   another (the engine's result shape carries no date at all), not new
   data. */

.wsk-srch-main {
  max-width: 1400px; margin: 0 auto; padding: 36px 40px 40px;
}
.wsk-srch-title { margin: 0 0 18px; font-size: 26px; font-weight: 700; color: var(--wsk-heading); }
.wsk-srch-count { margin: 0 0 16px; font-size: 15px; color: var(--wsk-text-secondary); }
/* Forced survivor of the ws-srch-* prefix this task otherwise retires
   entirely: tests/test_workspace_search.py::
   test_search_page_malformed_engine_payload_renders_friendly_error (a
   pinned R3 contract, unmodified by this task) asserts this literal class
   string - only its token values are re-pointed at wsk-* tokens here. */
.ws-srch-error {
  margin: 0 0 16px; padding: 10px 14px; border-radius: 8px; font-size: 14px;
  color: var(--accent); background: #f7e9e9; border: 1px solid #e3b9ba;
}

.wsk-srch-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.wsk-srch-row {
  background: var(--wsk-card); border: 1px solid var(--wsk-border); border-radius: 12px;
  padding: 14px 18px; display: flex; flex-direction: column; gap: 6px;
}
.wsk-srch-row-top { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.wsk-srch-row-title { font-size: 16px; font-weight: 700; color: var(--wsk-heading); }
.wsk-srch-row-meta { flex-shrink: 0; font-size: 13px; color: var(--wsk-meta); }
.wsk-srch-row-text { margin: 0; font-size: 14px; color: var(--wsk-text-secondary); line-height: 1.4; }
/* Uppercase kicker chip (TOKENS' kicker recipe: letter-spacing .05em,
   distinct from the .03em table-header family - see this file's :root
   comment) - neutral cream/meta-toned, not accent: r.scope is
   informational metadata (public/clerk visibility), not a call-to-action,
   so it doesn't visually compete with the page's one real CTA (the El-bot
   card's "Ask El-bot" button below). */
.wsk-srch-kicker {
  align-self: flex-start; display: inline-block; font-size: 12px; font-weight: 700;
  padding: 3px 9px; border-radius: 999px; background: var(--wsk-cream); color: var(--wsk-text-secondary);
  text-transform: uppercase; letter-spacing: .05em;
}

/* Zero-state "Ask El-bot instead?" card - live-measured ACCENT-themed
   throughout (kicker: accent-light bg / accent text; button: accent fill),
   NOT oxblood - oxblood is reserved for the Ask PAGE's own El-bot identity
   elements only (the user-question bubble, the follow-up callout - see the
   Ask section below), never this card. Composes .wsk-card (Task 1's shared
   recipe: white bg, 14px radius, `0 2px 10px rgba(38,36,29,.05)` shadow -
   all measured-identical here) with two small overrides this specific card
   needs (a warmer border tone, a max-width cap) rather than re-declaring
   the whole recipe. */
.wsk-srch-elbot-card {
  border-color: var(--wsk-border-warm); max-width: 680px;
  display: flex; flex-direction: column; gap: 12px; align-items: flex-start;
}
.wsk-srch-elbot-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* letter-spacing .04em (measured exactly off this element) - close to but
   not identical to the .05em general kicker figure above; recorded as
   measured, not forced to match a sibling kicker the mock itself doesn't
   unify either. */
.wsk-srch-elbot-kicker {
  display: inline-flex; align-items: center; background: var(--accent-light); color: var(--accent);
  border-radius: 999px; padding: 4px 10px; font-size: 12px; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase;
}
.wsk-srch-elbot-heading { margin: 0; font-size: 18px; font-weight: 700; color: var(--wsk-text); }
.wsk-srch-elbot-body { margin: 0; font-size: 15px; color: var(--wsk-text-secondary); line-height: 1.5; }
.wsk-srch-elbot-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.wsk-srch-elbot-hint { font-size: 13px; color: var(--wsk-meta); }

.wsk-srch-recent-heading {
  margin: 0 0 8px; font-size: 15px; font-weight: 700; letter-spacing: .03em;
  text-transform: uppercase; color: var(--wsk-text-secondary);
}
.wsk-srch-recent { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.wsk-srch-recent a {
  display: inline-block; padding: 8px 0; font-size: 15px; color: var(--wsk-text);
  text-decoration: none; border-bottom: 1px solid var(--wsk-border-row);
}
.wsk-srch-recent a:hover { color: var(--accent); }
.wsk-srch-empty { margin: 0; font-size: 15px; color: var(--wsk-meta); }

/* Ask (El-bot) screen (Task 5) - live-measured against the mock
   (2026-08-21). Content column is 900px (measured directly - unchanged
   from this section's own earlier guess, the one figure that turned out
   correct without adjustment); the mock's own thread renders each turn as
   TWO bubbles (a right-aligned user-question bubble, a left-aligned white
   bot-answer bubble, .wsk-ask-answer below) rather than a plain heading +
   unstyled response area - see workspace/ask.html and fragments/
   ask_answer.html's own header comments for the full correction writeup
   (individual follow-up chips are accent pills, not left-border callouts;
   no avatar exists anywhere in the mock). Confidence banner colors are
   unchanged from before this task (the exact demo-era honesty-banner
   values, hub/static/app.css's .banner.green/.banner.amber) - no mock
   equivalent exists for this real, hub-only feature to measure against;
   red never reaches a banner (tf_search.confidence_band's own contract),
   so no wsk-ask-banner-red rule exists.

   TASK 6.5 OWNER RULING R-1 (2026-08-22, docs/superpowers/specs/2026-08-22-
   owner-queue.md): the mock's own thread bubble/callout/label were oxblood
   #8b0000 at Task 5 close - the mock's own scripted color, live-measured
   accurate at the time and deliberately kept OUTSIDE the accent system
   (TOKENS' own Semantic section). The owner overrode this directly ("El-bot
   says it's red, change that to blue... keep everything at the indigo"):
   El-bot renders in the town accent per owner ruling R-1; the mock's
   oxblood is overridden, not followed - see the owner-queue doc. All three
   oxblood rules below (.wsk-ask-q, .wsk-ask-elbot-callout,
   .wsk-ask-elbot-label) now read var(--accent)/var(--accent-light) instead,
   so El-bot recolors with the rest of the app under any of the five Town
   Themes (Task 6) - see tests/test_workspace_search_ask_style.py's own
   test_elbot_accent_rules_carry_accent_not_oxblood for the flipped CSS-value
   pin (through Task 6 this pinned the opposite invariant). */

.wsk-ask-main {
  max-width: 900px; margin: 0 auto; padding: 24px 32px 32px;
  display: flex; flex-direction: column; gap: 18px;
}
.wsk-ask-back { align-self: flex-start; font-size: 15px; color: var(--wsk-text-secondary); text-decoration: none; }
.wsk-ask-back:hover { color: var(--accent); }
/* The query, as the user-message bubble (measured, Task 5: radius 14px 14px
   4px 14px [bottom-right squared - the "tail" corner for a right-aligned
   bubble], padding 12px 18px, font-size 16px, NORMAL weight - the mock's
   own inline style carries no font-weight at all, so this <h1> needs an
   explicit 400 to cancel the browser's default h1 bold; kept as <h1>, not
   the mock's own plain <div>, for accessible heading structure - "restyle
   only" governs the visual language, not the mock's own semantic HTML
   choices). Background var(--accent)/white text, NOT the mock's own oxblood
   #8b0000 (Task 6.5 owner ruling R-1, 2026-08-22 - see this section's own
   header comment above). */
.wsk-ask-q {
  align-self: flex-end; max-width: 75%; margin: 0;
  background: var(--accent); color: #fff; border-radius: 14px 14px 4px 14px;
  padding: 12px 18px; font-size: 16px; font-weight: 400;
}

/* The bot's own reply, in every state (loading/error/red/green/amber) -
   measured: white bg, 1px #cfcfcf border, radius 14px 14px 14px 4px
   [bottom-left squared - the tail corner for a left-aligned bubble],
   padding 16px 20px, max-width 92%. */
.wsk-ask-answer {
  align-self: flex-start; max-width: 92%; box-sizing: border-box;
  background: var(--wsk-card); border: 1px solid var(--wsk-border);
  border-radius: 14px 14px 14px 4px; padding: 16px 20px;
}
.wsk-ask-loading { margin: 0; font-size: 15px; color: var(--wsk-text-secondary); }

.wsk-ask-error {
  margin: 0; padding: 10px 14px; border-radius: 8px; font-size: 14px;
  color: var(--accent); background: #f7e9e9; border: 1px solid #e3b9ba;
}

.wsk-ask-noanswer { margin: 0; font-size: 16px; line-height: 1.6; color: var(--wsk-text); white-space: pre-line; }

/* El-bot follow-up callout - TOKENS' own callout recipe (shape confirmed
   byte-accurate by live measurement, Task 5: radius 0 8px 8px 0, 3px
   left-border). Wraps the "El-bot" name label AND the lead line as ONE unit
   (not per-chip - see the chips further below, which use the plain accent-
   pill recipe, not this callout's). Colors are var(--accent-light) bg /
   var(--accent) border, NOT the mock's own bg #f7f2e9 / border #8b0000
   (Task 6.5 owner ruling R-1, 2026-08-22 - see this section's own header
   comment above). */
.wsk-ask-elbot-callout {
  margin-top: 14px; padding: 10px 12px; background: var(--accent-light);
  border-left: 3px solid var(--accent); border-radius: 0 8px 8px 0;
}
/* Mixed-case as-is ("El-bot", not uppercase) - measured: unlike the Search
   zero-state kicker above, this label carries no text-transform. Color
   var(--accent), not the mock's own #8b0000 (R-1, as above). */
.wsk-ask-elbot-label { display: block; margin-bottom: 3px; font-size: 12px; font-weight: 700; color: var(--accent); }
.wsk-ask-followup-lead { margin: 0; font-size: 15px; line-height: 1.5; color: var(--wsk-text); }

.wsk-ask-followups { list-style: none; margin: 14px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
/* Plain accent-bordered pills - NOT oxblood, NOT left-border callouts
   (measured directly off the mock's own chip buttons; a correction over
   the plan's "follow-up chips as left-border callouts" prose - the ONE
   oxblood callout is the lead-in block above, not each chip). */
.wsk-ask-chip {
  display: inline-block; font-size: 13px; font-weight: 600; color: var(--accent);
  background: var(--wsk-card); border: 1px solid var(--accent); border-radius: 20px;
  padding: 8px 14px; text-decoration: none;
}
.wsk-ask-chip:hover { background: var(--accent-light); }

.wsk-ask-banner {
  border-radius: 8px; padding: 10px 14px; font-size: 14px; font-weight: 700;
  border: 1px solid; margin-bottom: 10px;
}
.wsk-ask-banner-green { color: #2e6b34; border-color: #2e6b34; background: #eef4ec; }
.wsk-ask-banner-amber { color: #9a6b00; border-color: #9a6b00; background: #f8f2e2; }

.wsk-ask-body { margin: 0; font-size: 16px; color: var(--wsk-text); line-height: 1.5; white-space: pre-wrap; }
.wsk-ask-citations { list-style: none; margin: 10px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.wsk-ask-citation {
  font-size: 13px; color: var(--wsk-meta); border: 1px solid var(--wsk-border);
  border-radius: 6px; padding: 6px 10px; background: var(--wsk-card);
}

/* --- Settings screen (Task 6, 2026-08-21-workspace-reskin.md) ---------------------
   workspace/settings.html. Live-measured against the mock's own Settings
   screen (2026-08-21, full JS structural dump - see task-6-report.md):
   content column is 1400px, NOT the 1280px every other workspace screen
   uses (Task 1's own .wsk-main/.wsk-content) - Settings is genuinely a
   single, wider column, not a two-column layout, so it gets its own
   wrapper class rather than forcing 1280px onto a screen that measures
   otherwise. Section wrappers themselves carry NO card border/background
   (measured: only individual elements inside some of them - the
   Connections rows - are bordered) - see settings.html's own comment.

   line-height: normal on the CONTAINER (Task 6 rider, fidelity re-measure,
   2026-08-22 - the SAME fix .wsk-email-main/.wsk-detail-main's own comment
   above already documents for this exact bug class, missed here at first
   pass): body's global line-height:1.4 was inheriting into every element
   below that doesn't set its own, most visibly the Accessibility slider
   row's 24px "A" glyph (computed line-height 33.6px here vs the mock's own
   "normal" 30px) - a 3-4px error that compounds down the page (Town Theme
   heading measured 12px lower than the mock by the time it's reached,
   Connections further still), the identical "small per-element offset
   compounds into a large false-positive fidelity band" pitfall that
   comment already explains. One container-level override fixes every
   descendant at once rather than patching each element individually. */
.wsk-settings-main {
  max-width: 1400px; margin: 0 auto; padding: 36px 40px 40px;
  display: flex; flex-direction: column; gap: 28px; box-sizing: border-box;
  line-height: normal;
}
.wsk-settings-title { margin: 0; font-size: 26px; font-weight: 700; color: var(--wsk-heading); }
.wsk-settings-h2 { margin: 0 0 4px; font-size: 18px; font-weight: 700; }
/* Connections' own H2 has no following .wsk-settings-sub paragraph to
   supply the extra spacing the way Accessibility/Town Theme's do - measured
   margin-bottom is 12px there, not 4px. */
.wsk-settings-h2-solo { margin-bottom: 12px; }
.wsk-settings-sub { margin: 0 0 12px; font-size: 14px; color: var(--wsk-text-secondary); }

/* Accessibility: A/A slider + live percentage readout. Native <input
   type=range> styled with accent-color (the SAME simple technique the
   mock itself uses - measured directly off its own inline style - rather
   than a custom ::-webkit-slider-thumb ruleset neither side needs). */
.wsk-settings-slider-row { display: flex; align-items: center; gap: 14px; width: 100%; }
.wsk-settings-a-sm { font-size: 14px; color: var(--wsk-text-secondary); flex-shrink: 0; }
.wsk-settings-a-lg { font-size: 24px; color: var(--wsk-text-secondary); flex-shrink: 0; }
.wsk-settings-slider { flex: 1 1 0%; accent-color: var(--accent); height: 6px; cursor: pointer; }
.wsk-settings-pct {
  font-size: 14px; font-weight: 700; color: var(--wsk-text);
  width: 44px; text-align: right; flex-shrink: 0;
}

/* Town Theme: 5 swatch buttons, measured 32px circles, gap 14px, wrapping.
   Each swatch's own color is a fixed hex (not var(--accent)) on purpose -
   all five must show simultaneously regardless of which ONE is currently
   active, the same reasoning hub.townflows.home.TAG_CSS_CLASS's own fixed
   per-label colors already establish for this file. Values are the exact
   five from this file's own :root comment block (the authoritative map -
   this is that map's second consumer, after the html.wsk-theme-* rules
   near the top of this file). Selected ring measured directly off the
   mock: box-shadow 0 0 0 2px #fff (the gap ring), 0 0 0 4px <that swatch's
   own color> (the outer ring) - not a single shared accent ring, since an
   UNSELECTED swatch showing another theme's own color must never imply
   that OTHER color is "selected". */
.wsk-theme-swatches { display: flex; gap: 14px; flex-wrap: wrap; }
.wsk-theme-swatch-btn {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  background: none; border: none; cursor: pointer; padding: 4px;
  font-family: inherit; transition: transform .15s ease;
}
.wsk-theme-swatch-btn:active { transform: scale(.96); }
.wsk-theme-swatch { width: 32px; height: 32px; border-radius: 50%; }
.wsk-theme-swatch-indigo { background: #5b4fd6; }
.wsk-theme-swatch-red { background: #7a2428; }
.wsk-theme-swatch-green { background: #2f4a3a; }
.wsk-theme-swatch-navy { background: #2f3f5c; }
.wsk-theme-swatch-slate { background: #4a4740; }
.wsk-theme-swatch-indigo.wsk-theme-swatch-selected { box-shadow: 0 0 0 2px #fff, 0 0 0 4px #5b4fd6; }
.wsk-theme-swatch-red.wsk-theme-swatch-selected { box-shadow: 0 0 0 2px #fff, 0 0 0 4px #7a2428; }
.wsk-theme-swatch-green.wsk-theme-swatch-selected { box-shadow: 0 0 0 2px #fff, 0 0 0 4px #2f4a3a; }
.wsk-theme-swatch-navy.wsk-theme-swatch-selected { box-shadow: 0 0 0 2px #fff, 0 0 0 4px #2f3f5c; }
.wsk-theme-swatch-slate.wsk-theme-swatch-selected { box-shadow: 0 0 0 2px #fff, 0 0 0 4px #4a4740; }
.wsk-theme-swatch-label { margin: 0; font-size: 13px; font-weight: 400; color: var(--wsk-text); }

/* Connections: read-only rows, measured byte-accurate off the mock's own
   Email row (the ONE row this hub renders for real - see settings.html's
   own comment for why the "town website" row is deliberately omitted). */
.wsk-conn-rows { display: flex; flex-direction: column; gap: 10px; }
.wsk-conn-row {
  display: flex; justify-content: space-between; align-items: center;
  background: var(--wsk-card); border: 1px solid var(--wsk-border);
  border-radius: 12px; padding: 16px 20px; box-sizing: border-box;
}
.wsk-conn-label { margin: 0; font-size: 16px; font-weight: 700; }
.wsk-conn-value { margin: 2px 0 0; font-size: 14px; color: var(--wsk-text-secondary); }
.wsk-conn-status { font-size: 15px; font-weight: 700; color: var(--accent); }

/* ============================================================================
   Mobile responsive (owner queue Q13, "Mobile-friendly: responsive layout for
   phone ratios + functionality tested on mobile viewport" - the reskin
   lane's last item, docs/superpowers/specs/2026-08-22-owner-queue.md).
   Design target 375x812, verified to also hold at 360x812 and 414x896 (the
   no-horizontal-scroll invariant) by tools/e2e/mobile-smoke.mjs. Two
   breakpoints, both narrower than every desktop layout figure in this file
   (the wider one is TOKENS' own single 700px media width; 480px is a second,
   tighter tier for the shell's own 4-tab admin nav row and a couple of other
   surfaces that stay cramped at 700px's own spacing alone) - matches this
   task's own instruction to prefer a small breakpoint set over one per
   component.

   Everything mobile-specific lives in ONE OF THESE TWO BLOCKS, with exactly
   two exceptions living beside the rules they modify instead (both proven
   zero-visual-diff at >=700px, see each one's own comment): .wsk-search-icon's
   enlarged hit area (shell section, above) and .wsk-tab's var()-wrapped
   gap/font-size/padding + `overflow: hidden` (shared shell section, above).
   Nothing else in this file changed - every desktop pixel value the
   fidelity board measures against the mock is governed by the SAME rules
   Tasks 1-7 already shipped and gated; see mobile-report.md for the
   GATE_SCREENS re-run proving that (any drift is attributed there, not
   hand-waved).

   Design choices worth naming up front (full rationale + screenshots in
   mobile-report.md):
     - Shell row1 (logo/search/icons) WRAPS to two lines below 700px: logo +
       icon buttons share line 1 (CSS `order`, NOT a markup/DOM reorder - the
       shared shell fragment is byte-identical at every width; only its
       in-viewport visual arrangement below 700px changes), the search form
       drops to its own full-width line 2 - the exact pattern this task's own
       dispatch names as acceptable ("logo + icons row, search full-width
       beneath"). Disclosed trade-off: `order` reorders visually, not in the
       DOM, so a sighted keyboard-only user's tab order (search input, then
       the account-menu avatar - the chat/bell icons are aria-disabled, never
       real tab stops) runs one step "out of visual order" relative to the
       two-line arrangement; a common, accepted trade-off for responsive top
       bars, and touch users (the primary mobile input mode) never encounter
       it since they tap directly rather than tabbing.
     - Two-column pages (Home's My Tasks + side rail; the town-admin page's
       .ws-main/.ws-content) STACK to one column; the side rail's fixed
       300px width becomes 100%.
     - The email inbox table RESTRUCTURES into stacked cards (from/date on
       top, subject+snippet full width, tag+priority chips on the bottom
       line) rather than gaining a horizontal-scroll container - the admin
       users table instead keeps its existing column-scroll wrapper AND also
       stacks its own flex columns, a simpler treatment for a simpler table.
       Column headers (which double as per-column sort links on Email) are
       hidden below 700px: a 5-column grid header has no honest visual
       counterpart once the rows themselves stop being 5 columns wide, and
       the toolbar's own always-visible "Sort: Most urgent" pill remains the
       mobile sort affordance either way - no sort capability is actually
       lost, only the finer per-column control.
   ============================================================================ */

@media (max-width: 700px) {
  /* --- shell: two-line top bar, condensed tab row -------------------------- */
  .wsk-shell-row1 {
    flex-wrap: wrap;
    justify-content: space-between;
    padding: 12px 16px;
    row-gap: 10px;
  }
  /* Visual reorder only (see this section's own header comment on the
     tab-order trade-off) - .wsk-logo/.wsk-shell-icons share line 1 (no other
     rule of theirs changes), .wsk-search-col claims the whole of line 2.
     .wsk-demo-badge shares .wsk-logo's own order value: without it the
     badge's default order 0 would jump it AHEAD of the wordmark here (equal
     order values fall back to document order, which keeps it glued right
     after the logo, same as at desktop width). */
  .wsk-logo { order: 1; }
  .wsk-demo-badge { order: 1; }
  .wsk-shell-icons { order: 2; }
  .wsk-search-col { order: 3; flex-basis: 100%; }

  :root {
    --wsk-tab-pad-x: 8px;
    --wsk-tab-gap: 6px;
  }
  .wsk-tabs { padding: 6px 8px 0; gap: 2px; }
  /* The label <span> is ALSO a flex item (of .wsk-tab's own inline-flex) and
     needs the identical "let me shrink below my own content size" fix
     .wsk-tab itself carries above - a DIFFERENT selector (not the one that
     rule's own "exactly one .wsk-tab rule" pin governs), so a plain
     min-width:0 is fine here. Ellipsis is the graceful floor: if a tab still
     can't fit its full label ("Settings", the longest, at the narrowest
     tested width - 360px, 4-tab admin view) it truncates instead of forcing
     the row - and the page - wider than the viewport. */
  .wsk-tab span {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }

  /* --- page containers: less horizontal padding, single column ------------ */
  .wsk-main, .ws-main, .wsk-email-main, .wsk-detail-main, .wsk-srch-main,
  .wsk-ask-main, .wsk-settings-main {
    padding: 20px 16px 28px;
  }
  /* .wsk-main (Home)/.ws-main (town-admin) are the only two-column layouts
     (content + a fixed-width side rail) - align-items:stretch OVERRIDES
     their own align-items:flex-start (a row-axis concern there; once
     flex-direction is column, flex-start would instead left-align each
     child at its own content width rather than stretching it to fill the
     line - the opposite of what stacking needs here). */
  .wsk-main, .ws-main {
    flex-direction: column;
    align-items: stretch;
    gap: 20px;
  }
  .wsk-side { width: 100%; }

  /* --- auth cards (login/register/forgot-password/join) ------------------- */
  .wsk-auth-page { padding: 24px 16px; }
  .wsk-auth-card { padding: 32px 24px; }

  /* --- Home: tasks-head control row, task-card action/meta rows ----------- */
  .wsk-tasks-head { flex-wrap: wrap; }
  .wsk-tasks-controls { flex-wrap: wrap; width: 100%; }
  .wsk-sort-select { min-width: 0; flex: 1 1 auto; }
  .wsk-task-actions { flex-wrap: wrap; }
  .wsk-task-meta { flex-wrap: wrap; }
  /* Real, live-measured mobile-smoke finding (not hand-computed): 90x21.6px
     at 375px, under the 24px floor. Mobile-gated for the same in-normal-flow
     reason "Show more" below is: this field sits inside .wsk-task-meta,
     followed by .wsk-task-divider/.wsk-task-actions in the same card, so a
     taller box here shifts everything after it - safe only below 700px.
     Right padding (26px, reserved for the calendar-icon background image)
     and every other property are untouched; only the vertical padding
     grows, and the icon's own `background-position: right 4px center`
     recenters automatically as the box gets taller. */
  .wsk-task-due-input { padding: 6px 26px 6px 6px; }
  .wsk-sched-head { flex-wrap: wrap; row-gap: 8px; }
  /* "Show more" (task titles >60 chars) - mobile-gated, not universal like
     .wsk-search-icon's hitbox above, since unlike that absolutely-positioned
     control this padding sits IN normal flow and would shift every element
     below it by a few px - safe to add only where the fidelity board never
     measures (below 700px; the board's own viewport is 1280px). Below the
     24px a11y floor otherwise: `display: inline` text has no box beyond its
     own glyphs (13-14px tall here). inline-block + vertical padding fixes
     it without changing the visible text at all. */
  .wsk-task-more summary { display: inline-block; padding: 5px 0; }

  /* --- Email: header row, table -> stacked cards --------------------------- */
  .wsk-email-head { flex-wrap: wrap; row-gap: 8px; }
  .wsk-detail-meta { flex-wrap: wrap; }
  .wsk-ethead { display: none; }
  .wsk-erow {
    grid-template-columns: 1fr auto;
    grid-template-areas:
      "from date"
      "subject subject"
      "tag priority";
    min-width: 0;
    column-gap: 10px;
    row-gap: 4px;
    padding: 12px 14px;
  }
  /* Same selector/specificity as the desktop divider rule (further above in
     this file) - equal specificity, later source order wins, so this
     cleanly turns the dividers off instead of needing !important. The
     desktop 5-column dividers have no honest place in a 3-row stacked
     card. */
  .wsk-ethead > *:not(:last-child), .wsk-erow > *:not(:last-child) {
    border-right: none;
  }
  .wsk-erow-from { grid-area: from; padding: 0; min-width: 0; }
  .wsk-erow-date { grid-area: date; padding: 0; text-align: right; white-space: nowrap; }
  .wsk-erow-subject-wrap { grid-area: subject; padding: 0; }
  .wsk-erow-tag { grid-area: tag; padding: 0; justify-self: start; }
  .wsk-erow-priority { grid-area: priority; padding: 0; justify-self: end; text-align: right; }
  .wsk-pagination-nav { flex-wrap: wrap; }

  /* --- Search: result-row title/meta line ---------------------------------- */
  .wsk-srch-row-top { flex-wrap: wrap; }

  /* --- Ask: back-to-home link ------------------------------------------------
     Real, live-measured mobile-smoke finding: 102x21px, under the 24px
     floor. Not a "link in a sentence" (WCAG's own exemption for those) - a
     standalone back-navigation link, the same category .wsk-task-more's
     "Show more" above already fixes the same way. .wsk-ask-back is a flex
     item of .wsk-ask-main's own column layout (gap: 18px between children),
     so - same reasoning as .wsk-task-due-input above - the padding is
     mobile-gated rather than universal: it grows this element's own box,
     which would shift every answer bubble below it by a few px on the
     1280px-viewport fidelity board. */
  .wsk-ask-back { padding: 4px 0; }

  /* --- Settings: connections row (a long connected-account value must wrap,
     not force the row wider - text-flow analogue of .wsk-tab's own
     unbroken-run floor explained above) -------------------------------------- */
  .wsk-conn-row { flex-wrap: wrap; row-gap: 6px; }
  .wsk-conn-value { overflow-wrap: anywhere; }
  /* Fix round (coordinator finding, post-review): the desktop 6px track
     height is an AUTHOR value (not a bare, unstyled native control), so
     WCAG 2.5.8's "user agent control" exemption does not actually cover it
     in the strict reading - live-measured by mobile-smoke.mjs's own tap-
     target audit at 229.2x6px, real, under the 24px floor. Bumped to 24px
     here, mobile-only (the desktop 6px value - TOKENS' own pinned figure,
     matching the mock's slim-track look - is untouched, so the fidelity
     board is unaffected by construction, same posture as every other rule
     in this section). No -webkit-appearance/custom track-and-thumb
     rebuild needed to keep the slim VISUAL look: Chrome/Edge/Firefox all
     draw the native track as its own thin line, vertically centered
     within the element's full box, independent of that box's own height -
     confirmed by screenshot at 375px (mobile-report.md) - so simply
     growing the box's HIT AREA doesn't fatten the visible line. */
  .wsk-settings-slider { height: 24px; }

  /* --- Town-admin: users table -> stacked rows, add-user form -------------- */
  .ws-admin-thead { display: none; }
  .ws-admin-row { flex-direction: column; align-items: stretch; gap: 6px; }
  .ws-admin-col-user, .ws-admin-col-scopes, .ws-admin-col-signin, .ws-admin-col-actions {
    flex: 0 0 auto;
    width: 100%;
  }
  .ws-pw-form { flex-wrap: wrap; }
  .ws-pw-form .ws-input-sm { min-width: 0; flex: 1 1 140px; }
  .ws-admin-add-form { flex-direction: column; align-items: stretch; }
  .ws-admin-add-form .ws-input-sm { width: 100%; }
  /* The <label> wrapping the "Admin" checkbox is entirely clickable (native
     <label>-for-<input> behavior) - a little vertical padding lifts its own
     tap target closer to the 24px floor. admin.html carries no fidelity
     gate of its own (see that template's header comment), so this is safe
     to add without a desktop-drift concern either way. */
  .ws-admin-checkbox { padding: 4px 0; }
}

@media (max-width: 480px) {
  /* A second, tighter tier - only the shell's 4-tab admin nav row (the
     tightest fit in the whole design: 4 evenly-split tabs, one labeled
     "Settings", inside a 360px viewport) needs it; every other surface
     above already holds at 360px under the 700px tier alone. */
  :root {
    --wsk-tab-pad-x: 6px;
    --wsk-tab-font: 13px;
  }
  .wsk-tab svg { display: none; }
}
