/* =============================================================================
   TASK & WORKFLOW MODULE — light theme, scoped to .task-theme
   ==========================================================================
   Deliberately a different visual register from the rest of this app
   (see app.css's own header comment: dark/rugged "field-tool" look).
   The task module mockups this was built from use a light, blue-accented
   SaaS look, and the person building this asked for that look specifically
   for these screens — so rather than force it into the existing dark
   palette (or leave the existing palette's tokens ambiguous by
   redefining --paper/--asphalt/etc. globally), every rule in this file is
   nested under .task-theme, with its own --task-* custom properties. Nothing
   here touches :root, so every other view is completely unaffected — same
   principle the standalone Dashboard app already uses for having its own look.
   Font families are reused from app.css (already loaded, already permitted
   by the CSP) rather than introducing a new one.
   ========================================================================== */

.task-theme {
  --task-bg: #f4f6fb;
  --task-surface: #ffffff;
  --task-surface-raised: #ffffff;
  --task-border: #aab3c8;
  --task-text: #16213a;
  --task-text-dim: #6b7690;
  --task-text-faint: #97a0b8;
  --task-accent: #2f6fed;
  --task-accent-dim: #eaf1ff;
  --task-accent-text: #ffffff;
  --task-navy: #16213a;
  --task-accent-yellow: #f2c12e;

  --task-gray: #94a3b8;
  --task-gray-bg: #f1f4f9;
  --task-blue: #3b82f6;
  --task-blue-bg: #e8f0fe;
  --task-orange: #f2994a;
  --task-orange-bg: #fef1e6;
  --task-purple: #8b5cf6;
  --task-purple-bg: #f1ebfe;
  --task-red: #e0483a;
  --task-red-bg: #fdeceb;
  --task-teal: #14b8a6;
  --task-teal-bg: #e5faf7;
  --task-green: #3fa16b;
  --task-green-bg: #e9f7ef;
  --task-slate: #64748b;
  --task-slate-bg: #eef1f5;

  --task-radius: 14px;
  --task-radius-sm: 9px;
  --task-shadow: 0 1px 2px rgba(20, 30, 60, 0.05), 0 4px 16px rgba(20, 30, 60, 0.06);

  background: var(--task-bg);
  color: var(--task-text);
  font-family: var(--font-body);
  min-height: 100%;
  padding: 14px 14px 24px;
}

/* Dark mode overrides — same mechanism as app.css's own
   :root[data-theme="dark"] block (Settings \u2192 Dark/Light, see
   theme.js's applyColorScheme()), just scoped to .task-theme the same
   way this file's own base tokens above are (see this file's header
   comment: intentionally never touches :root directly). Higher
   specificity than the plain .task-theme rule above (attribute selector
   + descendant vs. a single class) is what makes this win regardless of
   source order. Badge tint backgrounds (--task-*-bg) get genuinely
   different dark-appropriate values, not just inverted lightness — a
   pale tint like #fef1e6 behind dark text reads as "text on a light
   patch," which looks like a mistake sitting on a dark page; a
   dark-appropriate tint keeps the same "a colored wash behind the
   badge's own saturated text/icon color" relationship the light theme
   has. */
:root[data-theme="dark"] .task-theme {
  --task-bg: #10151f;
  --task-surface: #181f2e;
  --task-surface-raised: #1e2637;
  --task-border: #2b3448;
  --task-text: #e7ebf5;
  --task-text-dim: #97a1ba;
  --task-text-faint: #6d7793;
  --task-accent: #5b9bff;
  --task-accent-dim: #1c2c47;
  --task-accent-text: #ffffff;
  --task-navy: #e7ebf5;
  --task-accent-yellow: #f2c12e;

  --task-gray: #97a1ba;
  --task-gray-bg: #1e2637;
  --task-blue: #5b9bff;
  --task-blue-bg: #182640;
  --task-orange: #f2994a;
  --task-orange-bg: #3a2717;
  --task-purple: #a78bfa;
  --task-purple-bg: #291f3f;
  --task-red: #e97b6e;
  --task-red-bg: #3a1f1c;
  --task-teal: #2dd4bf;
  --task-teal-bg: #123330;
  --task-green: #5fbf8a;
  --task-green-bg: #17301f;
  --task-slate: #94a3b8;
  --task-slate-bg: #262e3d;

  --task-shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 4px 16px rgba(0, 0, 0, 0.3);
}

.task-theme * { box-sizing: border-box; }

.task-theme h1, .task-theme h2, .task-theme h3 {
  font-family: var(--font-body);
  text-transform: none;
  letter-spacing: normal;
  color: var(--task-text);
  margin: 0;
}
.task-theme h1 { font-size: 20px; font-weight: 700; }
.task-theme h2 { font-size: 15px; font-weight: 600; color: var(--task-text-dim); }
.task-theme .task-subtle { color: var(--task-text-dim); font-size: 13px; }
.task-theme .task-faint { color: var(--task-text-faint); font-size: 12px; }

/* ---------- Header row (title + primary action) ------------------------ */
.task-header-row {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 14px; gap: 10px;
}
.task-header-row h1 { display: flex; align-items: center; gap: 8px; }
.task-back-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 999px; border: 1px solid var(--task-border);
  background: var(--task-surface); color: var(--task-text); cursor: pointer; flex: none;
}

/* ---------- Buttons ------------------------------------------------------ */
.task-btn {
  font-family: var(--font-body); font-size: 14px; font-weight: 600;
  border-radius: var(--task-radius-sm); border: 1px solid var(--task-border);
  background: var(--task-surface); color: var(--task-text);
  padding: 10px 16px; cursor: pointer; min-height: 40px;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  /* Press/hover feedback (§ direct request — "modern... animations...
     when buttons are clicked") — same transform-scale-on-active
     language app.css's own .btn already established, extended to this
     theme's own button class so the whole app presses consistently
     rather than task-theme screens feeling static by comparison. Hover
     lift gated to devices that actually have a real hover (mouse/
     trackpad) — on touch, :hover can "stick" after a tap until the
     next tap elsewhere, which would leave a button looking permanently
     lifted instead of giving real feedback. */
  transition: transform 0.08s ease, box-shadow 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
}
.task-btn:active:not(:disabled) { transform: scale(0.97); }
@media (hover: hover) {
  .task-btn:not(:disabled):hover { box-shadow: 0 2px 8px rgba(20,30,60,0.1); }
}
.task-btn:disabled { opacity: 0.5; cursor: default; }
.task-btn-primary { background: var(--task-accent); border-color: var(--task-accent); color: var(--task-accent-text); }
.task-btn-danger { background: var(--task-red); border-color: var(--task-red); color: #fff; }
.task-btn-ghost { background: transparent; border-color: transparent; color: var(--task-accent); }
.task-btn-sm { padding: 6px 12px; min-height: 32px; font-size: 13px; }
.task-btn-block { width: 100%; }
.task-fab {
  position: fixed; right: 18px; bottom: 78px; z-index: 20;
  width: 56px; height: 56px; border-radius: 999px; background: var(--task-accent);
  color: #fff; border: none; box-shadow: var(--task-shadow); font-size: 26px;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
}

/* ---------- Inputs --------------------------------------------------- */
.task-theme input, .task-theme select, .task-theme textarea {
  width: 100%; max-width: 100%; min-width: 0; font-family: var(--font-body); font-size: 16px; color: var(--task-text);
  background: var(--task-surface); border: 1px solid var(--task-border);
  border-radius: var(--task-radius-sm); padding: 10px 12px; min-height: 42px;
  /* min-width: 0 — this is the Create Task modal's actual instance of
     the same flex/grid "won't shrink below content" gotcha the header
     kept running into (see app.css's long comment on that): a date
     input's own mm/dd/yyyy segments have an intrinsic minimum width
     that, inside a flex/grid .task-field layout, could force the field
     wider than its container and past the edge of the screen without
     this override, even with width:100% already set — width alone
     only sets the PREFERRED size, min-width is what actually allows
     shrinking below that when the two disagree.
     16px, not 15px: anything smaller triggers iOS Safari's automatic
     zoom-on-focus, which is what made the whole modal appear to shift/
     move left-right when a field was tapped (see taskCreateModal.js's
     openModal() and tasks.css's .task-modal for the rest of the iOS
     fixes this module already has) — the main app's own inputs already
     use 16px for exactly this reason (see app.css). */
}
.task-theme textarea { min-height: 80px; resize: vertical; }
/* Recolored to this theme's own blue accent instead of app.css's
   yellow (see that file for the base checkbox/radio styling this
   overrides just the color of) — yellow would look out of place
   against this theme's light, blue-accented surfaces. */
/* Same custom-drawn checkbox/radio as app.css, recolored for this
   theme: light surface/border at rest, blue accent + dark-on-yellow's
   equivalent (white checkmark on blue, since this theme's accent is
   already a strong color rather than needing a dark mark on top of a
   light yellow) when checked.
   Box-model properties (width/height/padding/border-radius/min-height)
   are repeated here even though app.css's input[type="checkbox"] rule
   already sets them — that rule and the generic ".task-theme input,
   select, textarea" rule two blocks up carry the exact same
   specificity (one class + one type selector each), so without an
   explicit re-assertion here, a tie goes to whichever stylesheet loads
   second (tasks.css, after app.css — see index.html), meaning the
   generic text-input sizing (100% width, 42px min-height, a much
   bigger border-radius, real padding) was winning over the checkbox's
   own 16px square — a checkbox rendered as a giant blue pill, full
   input width and height, rather than a checkbox at all. This
   selector already outranks the generic one on specificity (it adds
   an attribute selector on top), so restating the box model here fixes
   it without needing to touch load order or app.css at all. */
.task-theme input[type="checkbox"], .task-theme input[type="radio"] {
  border-color: var(--task-border);
  background: var(--task-surface);
  width: 16px; height: 16px; min-width: 16px; min-height: 16px;
  padding: 0; flex: none;
}
/* Same tie-breaking issue as the box-model properties above, just for
   border-radius specifically: app.css's own checkbox/radio-specific
   radius rules (4px / 50%) carry identical specificity to the generic
   ".task-theme input" rule's var(--task-radius-sm), so without this,
   the generic rule's much larger radius was winning here too — part of
   why the broken checkbox looked like a stadium-shaped pill rather
   than a slightly-too-big square. Split in two since checkbox and
   radio need different shapes. */
.task-theme input[type="checkbox"] { border-radius: 4px; }
.task-theme input[type="radio"] { border-radius: 50%; }
.task-theme input[type="checkbox"]:checked, .task-theme input[type="radio"]:checked {
  background: var(--task-accent);
  border-color: var(--task-accent);
}
.task-theme input[type="checkbox"]:checked::after {
  border-color: #fff;
}
.task-theme input[type="radio"]:checked::after {
  background: #fff;
}
.task-theme input[type="checkbox"]:focus-visible, .task-theme input[type="radio"]:focus-visible {
  outline-color: var(--task-accent);
}
/* The centered calendar icon itself (app.css) is a single neutral gray
   now, deliberately chosen to read fine on both this theme's light
   surfaces and the main app's dark ones — no per-theme recolor needed
   here anymore. */
.task-theme label.task-field-label {
  display: block; font-size: 12px; font-weight: 600; color: var(--task-text-dim);
  text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 6px;
}
.task-field { margin-bottom: 14px; min-width: 0; max-width: 100%; }

/* ---------- Stat pills (My Work header, Kanban filters) ---------------- */
.task-pillbar { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px; margin-bottom: 14px; -webkit-overflow-scrolling: touch; }
.task-pill {
  flex: none; display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  background: var(--task-surface); border: 1px solid var(--task-border); border-radius: var(--task-radius-sm);
  padding: 8px 14px; cursor: pointer; min-width: 84px;
}
.task-pill.active { border-color: var(--task-accent); background: var(--task-accent-dim); }
.task-pill .task-pill-count { font-size: 20px; font-weight: 700; line-height: 1; }
.task-pill .task-pill-label { font-size: 11px; color: var(--task-text-dim); white-space: nowrap; }
.task-pill.tone-red .task-pill-count { color: var(--task-red); }
.task-pill.tone-orange .task-pill-count { color: var(--task-orange); }
.task-pill.tone-blue .task-pill-count { color: var(--task-blue); }
.task-pill.tone-purple .task-pill-count { color: var(--task-purple); }
.task-pill.tone-green .task-pill-count { color: var(--task-green); }

/* ---------- Cards --------------------------------------------------------- */
.task-card {
  position: relative;
  background: var(--task-surface); border: 1px solid var(--task-border);
  border-radius: var(--task-radius); padding: 14px; margin-bottom: 10px;
  box-shadow: var(--task-shadow); cursor: pointer;
  /* :active below had no transition at all before this — the scale
     change happened instantly, easy to miss. Hover lift gated to real
     hover devices only, same reasoning as .task-btn's own (touch
     :hover can "stick" after a tap). */
  transition: transform 0.1s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.task-card:active { transform: scale(0.997); }
@media (hover: hover) {
  .task-card:hover { box-shadow: 0 4px 14px rgba(20,30,60,0.12); }
}
/* Manage Tasks' List tab select mode (taskManage.js): tints a selected
   card so it's easy to scan which rows are checked at a glance,
   alongside the actual checkbox drawListBody renders inline per card
   (matching taskFeatures.js's own selectMode pattern — see that file)
   rather than a separate decorative indicator competing with it for
   the same corner. */
.task-card.selected {
  border-color: var(--task-accent);
  background: var(--task-accent-dim);
}

/* Top-right corner badge (taskIcons.js's assigneeCornerBadgeHtml) — a
   pill that grows to fit its content: icon-only (circle) for "assigned
   to someone else," or icon+team-name / "You" text (wider pill) for
   the other two cases, so it reads at a glance without competing with
   the card's other meta text for space. Needs the card itself to be
   position: relative (set above / on .task-kanban-card below) to
   anchor to; a max-width + ellipsis keeps a long team name from
   overflowing the card. */
.task-corner-badge {
  position: absolute; top: 8px; right: 8px;
  min-width: 22px; height: 22px; max-width: calc(100% - 16px);
  border-radius: 999px; padding: 0 8px;
  display: flex; align-items: center; justify-content: center; gap: 4px;
  font-size: 11px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.task-corner-badge.individual { width: 22px; padding: 0; background: var(--task-accent-dim); color: var(--task-accent); }
.task-corner-badge.you { background: var(--task-teal); color: #fff; }
.task-corner-badge.team { background: var(--task-purple-bg); color: #6528d6; }
.task-corner-badge svg { flex: none; }
/* taskManage.js's supervisor-facing variant — always text (a name or
   "You"), colored inline per-badge via assigneeCornerBadgeDetailedHtml's
   own colorForId() hash, so the palette lives in JS (where the hashing
   happens) rather than needing N modifier classes here for N possible
   assignees. */
.task-corner-badge-detailed {
  position: absolute; top: 8px; right: 8px;
  max-width: calc(100% - 16px);
  border-radius: 999px; padding: 3px 9px;
  font-size: 11px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  display: inline-flex; align-items: center; gap: 4px;
}
.task-corner-badge-detailed svg { flex: none; }
.task-card-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.task-card-title { font-size: 15px; font-weight: 700; color: var(--task-text); margin-bottom: 2px; }
.task-card-meta { font-size: 13px; color: var(--task-text-dim); display: flex; flex-wrap: wrap; gap: 4px 10px; margin-top: 6px; align-items: center; }
/* The shared bottom row (taskIcons.js's cardBottomRowHtml): Due Date at
   the left, Submitted/Completed at the right, and an action button (if
   any) genuinely centered between them via a 3-column grid rather than
   flex + space-between, which would just push a lone button to
   whichever side has less competing content instead of the middle. */
.task-card-bottom-row {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 8px;
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--task-border);
}
.task-card-bottom-left { font-size: 11px; color: var(--task-text-dim); text-align: left; }
.task-card-bottom-center { text-align: center; }
.task-card-bottom-right { font-size: 11px; color: var(--task-text-faint); text-align: right; }
.task-card-icon {
  width: 38px; height: 38px; border-radius: 10px; display: flex; align-items: center; justify-content: center;
  flex: none; background: var(--task-blue-bg); color: var(--task-blue);
}
.task-card-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--task-border); }

/* ---------- Badges (status / priority) — always paired with text, never
   color alone, per accessibility requirement. ---------------------------- */
.task-badge {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.02em;
  border-radius: 999px; padding: 4px 10px; white-space: nowrap; border: 1px solid transparent;
}
.task-badge-icon { width: 7px; height: 7px; border-radius: 50%; flex: none; }

/* A team assignee, distinguished from an individual one at a glance
   (per the task spec) with a small people-icon + purple pill, rather
   than just plain text like an individual's name gets. */
.task-team-chip {
  display: inline-flex; align-items: center; gap: 4px;
  background: var(--task-purple-bg); color: #6528d6;
  border-radius: 999px; padding: 2px 8px 2px 6px; font-size: 12px; font-weight: 600;
}
.task-team-chip svg { flex: none; }

.task-badge.status-unassigned    { background: var(--task-gray-bg);   color: #5b6478; }
.task-badge.status-assigned      { background: var(--task-blue-bg);   color: #2255c7; }
.task-badge.status-in_progress   { background: var(--task-orange-bg); color: #b1590f; }
.task-badge.status-paused        { background: var(--task-slate-bg);  color: var(--task-slate); }
.task-badge.status-submitted     { background: var(--task-purple-bg); color: #6528d6; }
.task-badge.status-needs_revision{ background: var(--task-red-bg);    color: var(--task-red); }
.task-badge.status-approved      { background: var(--task-teal-bg);   color: #0c8c7d; }
.task-badge.status-completed     { background: var(--task-green-bg);  color: #227a4c; }
.task-badge.status-cancelled     { background: var(--task-gray-bg);   color: #5b6478; text-decoration: line-through; }
/* Dashed border deliberately, unlike every other .task-badge here —
   those are all solid-filled because they describe the task's current
   live state; this one is describing history (see
   checkedOutFromBadgeHtml in taskIcons.js), so it reads visually as
   "context," not another status to scan alongside the real ones. */
.task-badge.checked-out-from {
  background: var(--task-surface); color: var(--task-text-dim);
  border: 1px dashed var(--task-border);
}
.task-badge.checked-out-from svg { flex: none; }
.task-badge.status-overdue       { background: var(--task-red-bg);    color: var(--task-red); }
.task-badge.status-due-today     { background: #fef3c7; color: #92400e; }

.task-badge.priority-low    { background: var(--task-gray-bg);   color: #5b6478; }
.task-badge.priority-normal { background: var(--task-blue-bg);   color: #2255c7; }
.task-badge.priority-high   { background: var(--task-orange-bg); color: #b1590f; }
.task-badge.priority-urgent { background: var(--task-red-bg);    color: var(--task-red); }

/* ---------- Filters / bulk toolbars ---------------------------------- */
.task-filter-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
/* Collapsible filter panel (tableColumns.js's wireFilterToggle) — starts
   collapsed by default on both Manage Tasks' and My Work's List tabs.
   The toggle itself sits inline in the view-toggle/Select button row
   (a compact pill, same footprint as .task-view-toggle next to it) —
   .task-filter-body carries the actual bordered/padded "panel" look
   now, appearing below that row only once expanded, rather than the
   toggle living inside its own always-visible card the way an earlier
   version of this had it. */
.task-filter-toggle {
  display: flex; align-items: center; gap: 6px;
  background: var(--task-surface); border: 1px solid var(--task-border); border-radius: var(--task-radius-sm);
  padding: 7px 12px; cursor: pointer; flex: none;
  font-family: var(--font-body); font-size: 13px; font-weight: 700; color: var(--task-text);
}
.task-filter-toggle.expanded { background: var(--task-accent-dim); border-color: var(--task-accent); color: var(--task-accent); }
.task-filter-toggle-icon { display: flex; align-items: center; }
/* Shown only while collapsed AND at least one filter is actually set —
   collapsing the panel by default shouldn't also hide the fact that
   something's currently narrowing the list (see tableColumns.js's own
   countActiveFilters()). */
.task-filter-toggle-badge {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px;
  background: var(--task-accent); color: var(--task-accent-text); font-size: 11px; font-weight: 700;
}
.task-filter-panel { background: var(--task-surface); border: 1px solid var(--task-border); border-radius: var(--task-radius); padding: 14px; margin-bottom: 14px; }
.task-filter-body { background: var(--task-surface); border: 1px solid var(--task-border); border-radius: var(--task-radius); padding: 14px; margin: 8px 0 14px; }

/* ---------- List/Column view toggle (Manage Tasks > List) -------------- */
/* Small icon-only segmented control, same "pill of buttons on a tinted
   track" pattern as .task-tabs elsewhere in this file, just compact and
   icon-only since there are only ever two choices here. */
.task-view-toggle {
  display: inline-flex; background: var(--task-gray-bg); border-radius: var(--task-radius-sm); padding: 3px; gap: 2px; flex: none;
}
.task-view-toggle-btn {
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 30px; border: none; background: none; border-radius: 7px;
  color: var(--task-text-dim); cursor: pointer;
}
.task-view-toggle-btn.active { background: var(--task-surface); color: var(--task-accent); box-shadow: var(--task-shadow); }

/* ---------- Columned ("table") task/feature list -----------------------
   Reusable across every "columned view" in the app (taskManage.js's List
   tab, myWork.js's List tab, taskFeatures.js's Features browser) — a real
   <table> rather than a div grid, so column headers, native row
   semantics, and text alignment all come for free.

   overflow-x: auto (not sticky headers — this app has a documented
   history of position:sticky/fixed mobile quirks, e.g. the iOS
   bottom-sheet containment bug noted elsewhere in this file, so a plain
   horizontal-scroll fallback is the safer choice here over an untested
   sticky header) is what keeps this from breaking a narrow phone layout
   instead of just letting content overflow the screen.

   Deliberately NO width: 100% (and NO width: 1% on .task-table-col-fit
   below, despite that being the usual go-to for this) — verified with a
   real Chromium render (Playwright) that width:100% always stretches the
   table to fill its container regardless of content, with only the
   flexible column absorbing the difference: every OTHER column DOES
   shrink to content that way, but the table as a whole doesn't, which
   isn't genuinely "fit to content." Leaving both width declarations
   unset lets the browser's native auto table-layout algorithm size the
   whole table (not just individual columns) to its actual content,
   confirmed the same way — it only fills the wrap's full width when
   content genuinely needs that much room. */
.task-table-wrap {
  background: var(--task-surface); border: 1px solid var(--task-border); border-radius: var(--task-radius);
  overflow-x: auto; box-shadow: var(--task-shadow); -webkit-overflow-scrolling: touch;
}
@media (min-width: 1024px) {
  /* On desktop, a table with many rows (e.g. a task's Features tab with
     a large layer, or a long My Tasks / Task list) made .task-table-wrap
     grow to the FULL height of every row, which pushed its own native
     horizontal scrollbar all the way down to the bottom of that — the
     admin had to scroll through the entire list just to reach the
     control that scrolls it sideways. Bounding the wrap's own height and
     giving it its own vertical scroll too keeps BOTH scrollbars at a
     fixed, always-reachable position right at the table's own edges,
     without needing to touch the table's markup or column logic at all
     — every table using this shared class benefits (taskManage.js,
     taskFeatures.js, myWork.js), not just the one that was reported.
     Mobile is untouched — its own page-level scroll already keeps the
     horizontal scrollbar reachable near wherever the user's thumb
     already is, and touch scrolling doesn't have the same "hunt for the
     tiny scrollbar" problem a mouse-driven desktop UI does. */
  .task-table-wrap {
    max-height: calc(100vh - 260px);
    overflow-y: auto;
  }
  .task-table thead th {
    position: sticky;
    top: 0;
    z-index: 1;
  }
}
.task-table { border-collapse: collapse; font-size: 13px; table-layout: auto; }
.task-table thead th {
  text-align: left; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em;
  color: var(--task-text-dim); padding: 12px 14px; border-bottom: 1px solid var(--task-border);
  background: var(--task-gray-bg); white-space: nowrap;
}
/* Every header cell doubles as a sort control and a drag handle (see
   tableColumns.js — click/tap to sort, press-and-hold-then-drag, or
   click-and-drag on a mouse, to reorder) — grab cursor signals the drag
   affordance.

   touch-action: none (not pan-x) — this WAS pan-x, on the theory that
   letting a quick horizontal swipe scroll natively (since the header
   row is part of .task-table-wrap's own overflow-x: auto) was worth
   preserving. In practice that broke the drag gesture on real touch
   devices instead: pan-x explicitly tells the browser native panning
   is allowed on this axis, and per the Pointer Events spec a user
   agent that decides a touch is being used to pan can fire
   'pointercancel' on the in-progress pointer sequence rather than
   continuing to dispatch pointermove — which tableColumns.js's own
   onCancel handler treats as "gesture aborted," silently killing the
   reorder attempt, often before the long-press timer even finishes.
   `none` fully suppresses native gesture recognition on these cells so
   our own long-press timer/pointermove tracking gets to decide instead
   — same tradeoff this codebase's own .drag-handle/
   .designer-row-drag-handle already make elsewhere (touch-action: none
   on the handle itself; a person can still swipe-scroll the table by
   touching a body row instead of the header, so this only gives up
   scroll-starting-specifically-from-the-header-row, not scrolling the
   table at all).

   -webkit-touch-callout/-webkit-user-select stop iOS's long-press
   text-selection magnifier/"Copy/Look Up" callout from popping up over
   the header mid-long-press and competing with the drag gesture — same
   fix this codebase already uses for Form Designer's drag handles (see
   designer.css's .drag-handle). */
.task-table-draggable-th {
  cursor: grab; touch-action: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.task-table-draggable-th.dragging { opacity: 0.4; }
/* Where the dragged column will land — see tableColumns.js's own
   showDropIndicator()/hideDropIndicator(). A `position: fixed` overlay,
   not a normal in-flow element (a <table> row can't host an arbitrary
   <div> placeholder between <th> cells the way Report Builder's own
   flexbox row can — see that file's .rb-drag-placeholder-vertical,
   which this is the same visual idea as, just positioned differently
   out of necessity). Lives outside any specific table's DOM (appended
   once to document.body, shared across every table on the page), so
   its color can't be resolved from CSS cascade/scope the normal way —
   tableColumns.js reads the caller's own accent token via
   getComputedStyle() and sets it as a literal inline background,
   rather than this rule setting one. */
.task-table-drop-indicator {
  position: fixed; width: 3px; border-radius: 2px; z-index: 2000;
  display: none; pointer-events: none;
}
.task-table tbody tr.task-table-row {
  cursor: pointer; border-bottom: 1px solid var(--task-border); transition: background 0.1s ease;
}
.task-table tbody tr.task-table-row:last-child { border-bottom: none; }
.task-table tbody tr.task-table-row:hover { background: var(--task-gray-bg); }
/* Suppresses the :hover rule above right after a table redraw that
   wasn't caused by the mouse moving at all (sorting, reordering,
   applying a filter, paging) — see tableColumns.js's own
   suppressHoverUntilMouseMove(). Without this, a re-render can leave a
   brand new row sitting exactly under a stationary pointer (most often
   the very first row, right below wherever the header the person just
   clicked to sort happens to be), and the browser correctly, but
   confusingly, recomputes :hover onto it — reading as if the sort
   action itself had highlighted/selected that row. Higher specificity
   (extra class) than the plain :hover rule above is what makes this
   win regardless of source order; removed automatically the next time
   the mouse genuinely moves (see that function), so normal hover
   feedback resumes as soon as the person's actually pointing at
   something again. */
.task-table.suppress-hover tbody tr.task-table-row:hover { background: transparent; }
.task-table tbody tr.task-table-row.selected { background: var(--task-accent-dim); }
.task-table td { padding: 12px 14px; vertical-align: middle; color: var(--task-text); }
.task-table-th-check, .task-table-td-check { width: 36px; padding-right: 0; }
/* white-space: nowrap alone (no width — see the .task-table-wrap comment
   above for why not) is what keeps a metadata column like Status/
   Priority/Assignee/Due/Form from wrapping onto multiple lines and
   forces the browser's own auto table-layout to size it to exactly its
   content's natural width. */
.task-table-col-fit { white-space: nowrap; }
/* The primary/title column is the one deliberately flexible column —
   min-width so a very short title doesn't collapse the column to almost
   nothing, max-width so a very long one wraps onto multiple lines
   instead of stretching the whole table wider than it needs to be. */
.task-table-title-cell { min-width: 140px; max-width: 380px; }
.task-table-title { font-weight: 700; color: var(--task-text); }
.task-table-subtext { display: flex; gap: 6px; margin-top: 4px; flex-wrap: wrap; }
.task-table-subtext:empty { display: none; }
.task-table-assignee { color: var(--task-text-dim); }
.task-table-due { color: var(--task-text-dim); }
.task-table-due.overdue { color: var(--task-red); font-weight: 700; }
.task-table-form { color: var(--task-text-dim); white-space: nowrap; }
.task-table-view-btn {
  margin-left: 8px; font-size: 11px; font-weight: 700; padding: 4px 10px; border-radius: 999px;
  border: 1px solid var(--task-accent); background: var(--task-accent-dim); color: var(--task-accent); cursor: pointer;
}
.task-toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 12px; }
/* Fixed action-button rows (not variable-count things like filter chips)
   use this instead: nowrap + horizontal scroll rather than wrapping to a
   second row, so a screen full of buttons (Set Fields/Sort/Filter/
   Search/Select+) stays on one stable line instead of reflowing the
   whole screen whenever a label's width changes (e.g. "Filter (2)"),
   same pattern .task-pillbar already uses. */
.task-toolbar-scroll { display: flex; gap: 8px; flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; align-items: center; margin-bottom: 12px; }
.task-toolbar-scroll > * { flex: none; }
.task-toolbar-spacer { flex: 1; }
.task-selection-bar {
  position: sticky; top: 0; z-index: 5; display: flex; align-items: center; justify-content: space-between;
  background: var(--task-accent-dim); border: 1px solid var(--task-accent); border-radius: var(--task-radius-sm);
  padding: 10px 14px; margin-bottom: 12px; gap: 10px; flex-wrap: wrap;
}

/* ---------- Tabs (My Work / Manage switcher, task detail sub-tabs) ----- */
/* ---------- Inner sub-tabs (Manage Tasks: Overview/List/Kanban/Create,
   and any other plain text-only tab row) — light gray bar, active tab
   gets a white pill + a thin accent underline. --------------------- */
.task-tabs { display: flex; gap: 4px; background: var(--task-gray-bg); border-radius: var(--task-radius-sm); padding: 4px; margin-bottom: 14px; }
.task-tab { position: relative; flex: 1; text-align: center; padding: 10px 8px; border-radius: 7px; font-size: 13px; font-weight: 600; color: var(--task-text-dim); cursor: pointer; border: none; background: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.task-tab.active { background: var(--task-surface); color: var(--task-text); box-shadow: var(--task-shadow); }
.task-tab.active::after {
  content: ''; position: absolute; left: 18%; right: 18%; bottom: 3px; height: 2px;
  border-radius: 999px; background: var(--task-accent);
}

/* Scrollable variant — task detail's Details/Feature/Submission/
   Comments/Attachments/History row has too many tabs to fit `flex: 1`
   equal-width on a phone screen without cramming or clipping; this
   swaps to content-width tabs in a horizontally scrollable row instead
   (same .task-pillbar/.task-toolbar-scroll pattern), so every tab stays
   fully visible and reachable with a swipe rather than getting cut off
   on the right. */
.task-tabs-scroll { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.task-tabs-scroll .task-tab { flex: none; padding: 10px 16px; white-space: nowrap; }

/* ---------- Top-level mode switch (My Work / Manage Tasks / Features) —
   matches the reference mockup: a white rounded pill bar, the active
   mode as a solid dark-navy pill with a yellow underline accent, icon +
   label in both states. Deliberately its own set of classes rather than
   reusing .task-tabs/.task-tab above — visually a different, more
   prominent kind of tab (this is top-level navigation between whole
   screens, not a sub-view switch within one screen), and trying to
   parameterize one shared class for both looks would have made both
   harder to read. -------------------------------------------------- */
/* overflow: hidden isn't just visual containment here — it also
   establishes a new block formatting context, which stops the child
   .task-mode-switch's own margin-bottom from "collapsing through" this
   wrapper's bottom edge (a normal CSS behavior for a block child with
   no border/padding separating it from its parent's edge). Without
   that, the collapsed margin rendered as empty space *outside* this
   element's own painted background, showing the dark app-wide
   background underneath through a thin gap — the reported "black
   line" between the sticky switcher and the content below it. */
.task-mode-switch-wrap { position: sticky; top: 0; z-index: 5; background: var(--task-bg); overflow: hidden; }
.task-mode-switch {
  display: flex; gap: 4px; background: var(--task-surface); border-radius: 999px;
  padding: 6px; box-shadow: var(--task-shadow); margin-bottom: 14px;
  overflow-x: auto; -webkit-overflow-scrolling: touch; /* a last-resort safety net, not the primary fix — the tabs themselves are sized to actually fit their full text below, this only kicks in on a screen narrow enough that "actually fit" genuinely isn't possible */
}
.task-mode-tab {
  /* flex: 1 1 auto (not the earlier plain flex: 1) plus a min-width
     floor sized to what "Manage Tasks" actually needs at this smaller
     padding/font — a tab can still grow to share the row evenly like
     before, but won't be forced narrower than its own label needs,
     which is what caused the ellipsis truncation this is replacing.
     Reduced padding/gap/font-size/icon-size from the first version of
     this switch specifically so the full label fits without needing
     to truncate at all, per the "don't use ellipses here" request —
     .task-mode-switch's own overflow-x: auto above is only a fallback
     for a screen too narrow for that to hold true. */
  position: relative; flex: 1 1 auto; min-width: 92px; display: flex; align-items: center; justify-content: center; gap: 5px;
  padding: 10px 6px; border-radius: 999px; border: none; background: none; cursor: pointer;
  font-size: 12.5px; font-weight: 700; color: var(--task-text-dim);
  white-space: nowrap;
}
.task-mode-tab svg { width: 16px; height: 16px; flex: none; }
.task-mode-tab.active { background: var(--task-accent-dim); color: var(--task-navy); font-weight: 800; }
.task-mode-tab.active::after {
  content: ''; position: absolute; left: 22%; right: 22%; bottom: 4px; height: 3px;
  border-radius: 999px; background: var(--task-accent-yellow);
}
/* ---------- Kanban board ---------------------------------------------- */
.task-kanban { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 8px; -webkit-overflow-scrolling: touch; align-items: flex-start; }
/* width: fit-content (with sane min/max bounds) rather than a flat
   240px for every column regardless of what's actually in it — a
   column of short titles no longer carries a lot of dead empty space,
   and a column with genuinely long titles can still grow a bit before
   hitting the cap. align-items: flex-start on both this and
   .task-kanban-cards below stops cards from stretching to fill the
   column's own width by default (flex's normal cross-axis behavior),
   which is what let cards look forced-wide even when their own content
   was short. */
.task-kanban-col {
  flex: none; width: fit-content; min-width: 170px; max-width: 260px;
  background: var(--task-gray-bg); border-radius: var(--task-radius); padding: 10px;
  display: flex; flex-direction: column; align-items: flex-start;
}
.task-kanban-col.drag-over { outline: 2px dashed var(--task-accent); outline-offset: -2px; }
.task-kanban-col-header { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 4px 4px 10px; width: 100%; }
.task-kanban-col-title { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: var(--task-text-dim); white-space: nowrap; }
.task-kanban-col-count { font-size: 12px; font-weight: 700; color: var(--task-text); background: var(--task-surface); border-radius: 999px; padding: 1px 8px; flex: none; }
.task-kanban-cards { display: flex; flex-direction: column; align-items: flex-start; width: 100%; }
.task-kanban-card {
  position: relative;
  width: fit-content; min-width: 150px; max-width: 100%;
  background: var(--task-surface); border-radius: var(--task-radius-sm); padding: 10px 32px 10px 10px; margin-bottom: 8px;
  box-shadow: var(--task-shadow); cursor: grab; font-size: 13px;
}
.task-kanban-card[draggable="true"]:active { cursor: grabbing; }
.task-kanban-card .task-card-title { font-size: 13px; margin-bottom: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ---------- Metrics / workload ----------------------------------------- */
.task-metrics-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 14px; }
.task-metric-card { background: var(--task-surface); border: 1px solid var(--task-border); border-radius: var(--task-radius); padding: 12px; text-align: left; width: 100%; font-family: inherit; }
/* Explicit color on both — .task-metric-card became a <button> when the
   metrics were made clickable, and unlike a plain <div>, a <button>'s
   text color isn't reliably inherited from its parent across browsers
   (some fall back to a UA system color instead), which is what made
   these look washed-out/too light rather than the deliberately dark
   value/label of every other number in this theme. */
.task-metric-value { font-size: 22px; font-weight: 800; line-height: 1; color: var(--task-text); }
.task-metric-label { font-size: 11px; color: #4b5568; margin-top: 4px; font-weight: 600; }
.task-workload-row { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--task-border); }
.task-workload-name { flex: none; width: 110px; font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.task-workload-bar-track { flex: 1; height: 10px; border-radius: 999px; background: var(--task-gray-bg); overflow: hidden; }
.task-workload-bar-fill { height: 100%; background: var(--task-accent); border-radius: 999px; }
.task-workload-count { flex: none; width: 34px; text-align: right; font-size: 13px; font-weight: 700; }

/* ---------- Detail screen ------------------------------------------------ */
.task-detail-section { background: var(--task-surface); border: 1px solid var(--task-border); border-radius: var(--task-radius); padding: 14px; margin-bottom: 12px; }
.task-detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 16px; }
.task-detail-item-label { font-size: 11px; color: var(--task-text-faint); text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 2px; }
.task-detail-item-value { font-size: 14px; font-weight: 600; }
.task-primary-action-bar {
  position: sticky; bottom: 0; z-index: 6; display: flex; gap: 8px; padding: 20px 0 4px;
  background: linear-gradient(to top, var(--task-bg) 70%, transparent);
}

/* Sticky header (back button/title/badges/tabs) for the task detail
   screen — stays pinned as the tab content below it scrolls, instead of
   scrolling away with everything else. Needs its own solid background
   (not transparent) so scrolled-up content doesn't show through behind
   it, and a z-index above the tab content for the same reason. */
.task-detail-sticky-header {
  position: sticky; top: 0; z-index: 5; background: var(--task-bg);
  padding-top: 2px; margin-top: -2px; /* closes the 2px gap the negative offset below would otherwise leave uncovered at the very top of the scroll range */
  overflow: hidden; /* contains .task-tabs' own margin-bottom instead of letting it collapse through this element's bottom edge — same "black gap" issue .task-mode-switch-wrap has its own comment about */
}
/* Reserves room at the bottom of the tab content equal to roughly the
   sticky action bar's own rendered height, so scrolling all the way
   down shows the last bit of content above the bar rather than behind
   it — the actual bug report ("content should scroll behind the footer
   buttons," i.e. it currently doesn't, it renders under/through them). */
#tab-content { padding-bottom: 90px; }

/* ---------- Comments / history timeline -------------------------------- */
.task-comment { display: flex; gap: 10px; margin-bottom: 12px; }
.task-avatar {
  width: 32px; height: 32px; border-radius: 50%; background: var(--task-accent-dim); color: var(--task-accent);
  display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 13px; flex: none;
}
.task-comment-bubble { background: var(--task-gray-bg); border-radius: var(--task-radius-sm); padding: 8px 12px; flex: 1; }
.task-comment-author { font-size: 12px; font-weight: 700; }
.task-comment-time { font-size: 11px; color: var(--task-text-faint); }
.task-comment-text { font-size: 14px; margin-top: 2px; white-space: pre-wrap; }

.task-timeline { position: relative; padding-left: 20px; }
.task-timeline-item { position: relative; padding-bottom: 16px; }
.task-timeline-item::before {
  content: ''; position: absolute; left: -20px; top: 4px; width: 9px; height: 9px; border-radius: 50%;
  background: var(--task-accent); border: 2px solid var(--task-surface); box-shadow: 0 0 0 1px var(--task-border);
}
.task-timeline-item::after {
  content: ''; position: absolute; left: -16px; top: 14px; bottom: -4px; width: 1px; background: var(--task-border);
}
.task-timeline-item:last-child::after { display: none; }
.task-timeline-title { font-size: 13px; font-weight: 700; }
.task-timeline-meta { font-size: 12px; color: var(--task-text-dim); }
.task-timeline-notes { font-size: 13px; color: var(--task-text); margin-top: 2px; }

/* ---------- Empty / loading / error states ------------------------------ */
.task-empty-state { text-align: center; padding: 40px 16px; color: var(--task-text-dim); }
.task-empty-state .task-empty-icon { font-size: 32px; margin-bottom: 8px; opacity: 0.5; }
.task-loading { text-align: center; padding: 30px; color: var(--task-text-dim); font-size: 14px; }
.task-error-banner { background: var(--task-red-bg); color: var(--task-red); border-radius: var(--task-radius-sm); padding: 10px 14px; font-size: 13px; margin-bottom: 12px; }
.task-toast {
  position: fixed; left: 50%; bottom: 88px; transform: translateX(-50%);
  /* Same reasoning as app.css's own .toast (its z-index comment
     explains the full stacking picture) — kept at the identical value
     so a toast from either app can never end up buried under the
     other's modals/overlays. Black/white instead of this theme's own
     --task-text/--task-accent tones so every toast in the app reads
     identically regardless of which screen fired it. */
  z-index: 99999;
  background: #000; color: #fff; padding: 10px 18px; border-radius: 999px; font-size: 13px; box-shadow: var(--task-shadow);
  animation: toast-in 0.2s cubic-bezier(0.16, 1, 0.3, 1) both;
}

/* ---------- Wizard steps (bulk create) ---------------------------------- */
.task-wizard-steps { display: flex; align-items: center; gap: 6px; margin-bottom: 16px; }
.task-wizard-step { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--task-text-faint); font-weight: 600; }
.task-wizard-step .task-wizard-dot { width: 22px; height: 22px; border-radius: 50%; background: var(--task-gray-bg); color: var(--task-text-dim); display: flex; align-items: center; justify-content: center; font-size: 11px; }
.task-wizard-step.active .task-wizard-dot { background: var(--task-accent); color: #fff; }
.task-wizard-step.done .task-wizard-dot { background: var(--task-green); color: #fff; }
.task-wizard-connector { flex: 1; height: 1px; background: var(--task-border); }
.task-summary-row { display: flex; justify-content: space-between; padding: 6px 0; font-size: 14px; border-bottom: 1px solid var(--task-border); }
.task-summary-row:last-child { border-bottom: none; }
.task-summary-row .task-summary-label { color: var(--task-text-dim); }
.task-summary-row .task-summary-value { font-weight: 700; }

/* ---------- Modal / sheet (create task, comment box, etc.) ------------- */
@keyframes task-modal-backdrop-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes task-modal-sheet-in { from { transform: translateY(24px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
.task-modal-backdrop { position: fixed; inset: 0; background: rgba(15, 23, 42, 0.45); z-index: 40; animation: task-modal-backdrop-in 0.18s ease both; }
.task-modal {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 41; background: var(--task-bg);
  border-radius: 18px 18px 0 0; overflow-y: auto; overflow-x: hidden; padding: 16px 14px 24px;
  box-shadow: 0 -8px 30px rgba(20,30,60,0.2);
  /* iOS fix, take 2 — the actual cause of "can't scroll up to see the
     top": `vh` is defined against the LARGEST possible viewport (as if
     Safari's address bar were always hidden), not whatever's actually
     visible right now. A sheet capped at 88vh can end up genuinely
     taller than the real visible area once the address bar (or the
     keyboard) is showing, and since this is `position: fixed`, the part
     of it above the visible viewport isn't reachable by scrolling
     anything — not the page (`body` is permanently `overflow: hidden`
     in app.css, so there's nothing to scroll there either way) and not
     the sheet's own internal scroll (that only moves CONTENT within the box; it can't move the box itself back into
     view). `svh` ("small viewport height") is defined against the
     viewport when browser UI is fully expanded — the guaranteed-worst
     case — so a sheet capped at 88svh can never be taller than what's
     actually visible, in any browser-chrome state. The 88vh line stays
     first as the fallback for browsers that don't understand svh (an
     unsupported unit makes the whole declaration invalid, so it's
     simply skipped, leaving whichever earlier valid value was set). */
  max-height: 88vh;
  max-height: 88svh;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  animation: task-modal-sheet-in 0.22s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.task-modal-handle { width: 36px; height: 4px; border-radius: 999px; background: var(--task-border); margin: 0 auto 12px; }

/* Small, centered "fits its content" variant (§ direct request — the
   Map Creator unsaved-changes prompt "look very nice" at a size that
   matches a couple of sentences and a few buttons, not a mobile form
   sheet) — opted into via openModal(html, {centered:true}). Sits in
   the exact visual center of the viewport via `inset:0; margin:auto`
   rather than a translate-based centering trick, specifically so it
   doesn't fight the entrance/exit animation below for control of the
   `transform` property (the animation needs that fully to itself).
   `width:fit-content` plus a `max-width` cap is the actual "fit the
   contents with a little margin" behavior — it only ever grows as
   wide as its content needs (up to the cap), rather than always
   claiming a fixed width regardless of how little is in it. */
@keyframes task-modal-centered-in { from { transform: scale(0.94) translateY(10px); opacity: 0; } to { transform: scale(1) translateY(0); opacity: 1; } }
.task-modal.task-modal-centered {
  left: auto; right: auto; bottom: auto; inset: 0; margin: auto;
  width: fit-content; max-width: min(360px, calc(100vw - 32px));
  height: fit-content; max-height: 80vh; max-height: 80svh;
  border-radius: 16px; padding: 22px 22px 18px; box-shadow: 0 16px 40px rgba(20,30,60,0.3);
  animation: task-modal-centered-in 0.2s cubic-bezier(0.16, 1, 0.3, 1) both;
}

/* Slide-out panel (slidePanel.js) — an edge-anchored drawer, e.g. Map
   Creator's Add Layer (left) and Popup Designer (right). Deliberately
   its own set of classes/keyframes rather than reusing .task-modal's —
   a modal is a centered/bottom-sheet dialog you dismiss; a slide panel
   is a drawer that stays anchored to the side it slid out from, full
   viewport height, always left- or right-aligned regardless of screen
   size (narrow screens just cap its width via max-width, they don't
   turn it into a bottom sheet the way .task-modal already does on its
   own). Same backdrop treatment and easing as .task-modal-backdrop/
   .task-modal-sheet-in for a consistent feel across every overlay. */
@keyframes slide-panel-backdrop-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes slide-panel-left-in { from { transform: translateX(-100%); } to { transform: translateX(0); } }
@keyframes slide-panel-right-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
.slide-panel-backdrop {
  position: fixed; inset: 0; background: rgba(15, 23, 42, 0.45); z-index: 45;
  animation: slide-panel-backdrop-in 0.18s ease both;
}
.slide-panel {
  position: fixed; top: 0; bottom: 0; z-index: 46;
  max-width: 92vw;
  background: var(--task-bg);
  display: flex; flex-direction: column; min-height: 0;
  box-shadow: 0 0 36px rgba(20,30,60,0.28);
}
.slide-panel-left { left: 0; border-radius: 0 18px 18px 0; animation: slide-panel-left-in 0.26s cubic-bezier(0.16, 1, 0.3, 1) both; }
.slide-panel-right { right: 0; border-radius: 18px 0 0 18px; animation: slide-panel-right-in 0.26s cubic-bezier(0.16, 1, 0.3, 1) both; }
.slide-panel-header {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 16px 16px 12px; border-bottom: 1px solid var(--task-border);
}
.slide-panel-header h2 { margin: 0; font-size: 17px; }
.slide-panel-body { flex: 1 1 auto; overflow-y: auto; min-height: 0; padding: 16px; }
.slide-panel-footer {
  flex: 0 0 auto;
  display: flex; gap: 10px;
  padding: 12px 16px; border-top: 1px solid var(--task-border);
}
@media (max-width: 480px) {
  .slide-panel { width: 100vw !important; max-width: 100vw; border-radius: 0; }
}

/* Map Creator's unsaved-changes prompt — a concrete example of the
   .task-modal-centered variant above, styled to actually "fit its
   contents with a little margin and look nice" rather than just being
   correctly sized. Amber warning-triangle icon (a caution, not an
   error — nothing's actually gone wrong yet) in a soft circular badge,
   tight vertical rhythm between icon/title/text/actions instead of
   the bottom sheet's looser mobile-form spacing. */
.mb-unsaved-icon {
  width: 44px; height: 44px; border-radius: 50%; margin: 0 auto 12px;
  display: flex; align-items: center; justify-content: center;
  background: #fef3e0; color: #b8720a;
}

.mb-unsaved-title { margin: 0 0 6px; font-size: 17px; text-align: center; }
.mb-unsaved-text { margin: 0 0 18px; text-align: center; color: var(--task-text-dim); font-size: 13.5px; line-height: 1.45; }
.mb-unsaved-actions { display: flex; flex-direction: column; gap: 8px; }

/* ---------- Small helpers ------------------------------------------------ */
.task-flex-row { display: flex; align-items: center; gap: 8px; }
.task-spacer-h { height: 14px; }
.task-chip { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; background: var(--task-gray-bg); border-radius: 999px; padding: 4px 10px; }
.task-chip button { background: none; border: none; color: var(--task-text-faint); cursor: pointer; font-size: 13px; line-height: 1; padding: 0; }

@media (max-width: 380px) {
  .task-metrics-grid { grid-template-columns: repeat(2, 1fr); }
  .task-filter-grid { grid-template-columns: 1fr; }
  .task-detail-grid { grid-template-columns: 1fr; }
}

/* =============================================================================
   DESKTOP LAYOUT SYSTEM — task module specifics
   ==========================================================================
   Content-width centering for the task screens themselves lives in
   app.css (it needs #view-root's data-view attribute, set by router.js,
   to tell My Work/Manage Tasks/Task Detail apart from the Form Designer,
   which also reuses .task-theme — see that file's own writeup). What's
   here is purely the parts specific to this module's own components:
   giving fixed-column grids room to breathe now that their container is
   wider than a phone screen, without changing anything below 1024px.
   ========================================================================== */
@media (min-width: 1024px) {
  /* More breathing room between metric tiles/detail fields once they're
     not fighting for space on a narrow screen — spacing only, the grids
     themselves keep the same column counts (architecture.md: "do NOT
     simply increase padding everywhere" — this is a gap increase on
     grids that already have one, not new padding sprinkled in). */
  .task-metrics-grid { gap: 14px; }
  .task-detail-grid { gap: 14px 24px; }
  .task-filter-grid { gap: 16px; }

  /* Same reasoning as the workload name column just below: a little
     more room once the table isn't fighting a phone screen for space. */
  .task-table td, .task-table thead th { padding: 14px 18px; }

  /* The workload bars' name column was sized for a narrow phone list;
     a little more room reads better once the surrounding card itself
     isn't edge-to-edge anymore. */
  .task-workload-name { width: 150px; }

  /* Kanban board: left within the same StandardContainer as the rest of
     the task module (app.css) rather than broken out to full viewport
     width — a true edge-to-edge escape (position:relative; left:50%;
     margin-left:-50vw) risks introducing horizontal scroll/overflow
     bugs in #view-root's own scroll container for a mode this app only
     shows occasionally, and 1400px is already generous room for six
     ~300px columns. Columns just get a little wider than the mobile
     220px now that they're not fighting a phone screen for space; the
     board's own horizontal scroll (.task-kanban's overflow-x: auto,
     unchanged) still covers the case where columns genuinely don't fit.
     max-width only (not a flat width) — .task-kanban-col's own
     width: fit-content sizing (see its base rule's comment) is a
     deliberate design decision this doesn't override, just raises the
     ceiling on. */
  .task-kanban-col { max-width: 300px; }
}

/* =============================================================================
   PROJECTS (§18) — projects.js's list grid and projectDetail.js's
   overview/members screens. Everything else these two views use
   (.task-card, .task-badge, .task-header-row, .task-toolbar,
   .task-metric-card, .task-tabs, .task-field, .task-empty-state, ...) is
   already defined above and reused as-is — this section is only the
   handful of things genuinely new to Projects.
   ========================================================================== */

/* A plain responsive grid rather than the task module's single-column
   card list — a project card carries less per-card detail than a task
   card (no assignee badge, due date row, etc.), so several fit
   comfortably side by side even on a tablet-width screen; auto-fill
   with a minmax floor means this also degrades to one column on a
   phone with zero extra rules needed. */
.project-card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 10px;
}
.project-card-grid .task-card { margin-bottom: 0; cursor: pointer; }

/* Project Overview's stat row reuses .task-metrics-grid/.task-metric-card
   (already defined above for Manage Tasks) directly — no new rule
   needed. The "Upcoming"/"Recent Activity" panels below it are plain
   .task-card sections with a heading, styled here only for the small
   bits that don't already exist. */
.project-overview-section-title {
  font-size: 13px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.02em; color: var(--task-text-dim); margin: 18px 0 8px;
}
.project-activity-row {
  display: flex; justify-content: space-between; gap: 10px;
  padding: 8px 0; border-bottom: 1px solid var(--task-border);
  font-size: 13px;
}
.project-activity-row:last-child { border-bottom: none; }
.project-activity-row .meta { color: var(--task-text-dim); white-space: nowrap; }

.project-member-row {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 0; border-bottom: 1px solid var(--task-border);
}
.project-member-row:last-child { border-bottom: none; }
.project-member-row .name { font-weight: 600; flex: 1; min-width: 0; }
.project-member-row select { width: auto; flex: 0 0 160px; }

/* A team's own row plus its (collapsed-by-default) member sublist —
   direct request: "include the list of team members with their
   project roles ... below the team name. Make the list collapsed by
   default." The toggle button wraps the team name/count (not the
   whole row) so the role input/remove button beside it stay
   independently clickable without also flipping the collapse state. */
.project-team-block:not(:last-child) { border-bottom: 1px solid var(--task-border); }
.project-team-block .project-member-row { border-bottom: none; padding-bottom: 6px; }
.project-team-toggle {
  display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0;
  background: none; border: none; padding: 0; margin: 0; text-align: left;
  font: inherit; color: inherit; cursor: pointer;
}
.project-team-toggle-arrow {
  flex: none; display: inline-block; color: var(--task-text-dim);
  transition: transform 0.15s ease; font-size: 11px;
}
.project-team-toggle[aria-expanded="true"] .project-team-toggle-arrow { transform: rotate(90deg); }
.project-team-members { padding: 0 0 10px 30px; }
.project-team-member-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 6px 0; font-size: 13px;
}
.project-team-member-row .name { color: var(--task-text); }

@media (min-width: 1024px) {
  .project-card-grid { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 14px; }
}

/* Desktop control density (item 10 of a larger request) — mirrors
   app.css's own "DESKTOP CONTROL DENSITY" section exactly, just scoped
   to this file's own .task-theme/.task-btn instead of the generic
   input/select/textarea/.btn rules, and defined HERE specifically (not
   in app.css) because this file loads AFTER app.css — a same-
   specificity override placed in app.css would lose the cascade tie to
   this file's own base rule regardless of where within app.css it sat;
   putting it in the file that loads second is what actually wins.
   Mobile/tablet (below 1024px) is untouched, byte for byte. Font-size
   stays exactly 16px here too, for the identical iOS Safari zoom-on-
   focus reason the base rule's own comment already explains — only the
   box around that text shrinks. */
@media (min-width: 1024px) {
  .task-theme input, .task-theme select, .task-theme textarea {
    min-height: 34px;
    padding: 7px 10px;
  }
  .task-theme textarea { min-height: 68px; }
  .task-btn {
    min-height: 34px;
    padding: 8px 14px;
  }
  .task-btn-sm {
    min-height: 28px;
    padding: 5px 10px;
  }
}

