/* =========================================================
   IEC-KAU brand theme — DaisyUI 5.
   DaisyUI 5's CDN build ships only the default light/dark themes, so the
   `iec` theme must be defined here as plain CSS custom properties scoped
   to [data-theme="iec"]. (The `@plugin "daisyui/theme"` form requires a
   Tailwind v4 build step and is a no-op in the browser JIT.)
   ========================================================= */
[data-theme="iec"] {
  color-scheme: light;
  --color-base-100: oklch(100% 0 0);
  --color-base-200: oklch(97% 0.015 115);
  --color-base-300: oklch(90% 0.06 115);
  --color-base-content: oklch(24% 0.01 155);
  --color-primary: oklch(54% 0.15 147);
  --color-primary-content: oklch(97% 0.015 115);
  --color-secondary: oklch(26% 0.04 205);
  --color-secondary-content: oklch(97% 0.015 115);
  --color-accent: oklch(74% 0.20 135);
  --color-accent-content: oklch(26% 0.04 205);
  --color-neutral: oklch(24% 0.01 155);
  --color-neutral-content: oklch(97% 0.015 115);
  --color-info: oklch(67% 0.17 155);
  --color-info-content: oklch(100% 0 0);
  --color-success: oklch(54% 0.15 147);
  --color-success-content: oklch(97% 0.015 115);
  --color-warning: oklch(80% 0.15 85);
  --color-warning-content: oklch(24% 0.01 155);
  --color-error: oklch(65% 0.24 25);
  --color-error-content: oklch(100% 0 0);

  /* Phase K palette extras — match the hex literals used by
     .badge-rose / .status-swatch--rose / … so colorFill('rose') and the
     workflow-preview SVG can resolve them via `style="fill: var(...)"`.
     Without these, var(--color-rose) is undefined and the SVG falls
     back to a black fill. */
  --color-rose:           #f43f5e;
  --color-rose-content:   #ffffff;
  --color-pink:           #ec4899;
  --color-pink-content:   #ffffff;
  --color-purple:         #a855f7;
  --color-purple-content: #ffffff;
  --color-indigo:         #6366f1;
  --color-indigo-content: #ffffff;
  --color-sky:            #0ea5e9;
  --color-sky-content:    #ffffff;
  --color-teal:           #14b8a6;
  --color-teal-content:   #ffffff;
  --color-lime:           #84cc16;
  --color-lime-content:   #1a2e05;
  --color-amber:          #f59e0b;
  --color-amber-content:  #2e1a05;

  --radius-selector: 0.5rem;
  --radius-field: 0.5rem;
  --radius-box: 0.75rem;
  --size-selector: 0.25rem;
  --size-field: 0.25rem;
  --border: 1px;
  --depth: 1;
  --noise: 0;
}

/* Raw brand hexes for direct use where theme vars don't fit. */
:root {
  --iec-green-dark:  #055934;
  --iec-green:       #208D44;
  --iec-green-light: #86C242;
  --iec-green-pale:  #D3E6BE;
  --iec-tint:        #F2F7ED;
  --iec-teal:        #0A353E;
  --iec-mint:        #A7E0C5;
  --iec-mint-light:  #EEF8F5;
  --iec-vibrant:     #2BB673;
}

/* Dashboard hero banner — light brand gradient (tint → pale green).
   Hand-written so first paint isn't at the CDN JIT's mercy, and because
   both stops are raw brand hexes outside the DaisyUI token set. */
.iec-hero { background: linear-gradient(135deg, var(--iec-tint), var(--iec-green-pale)); }

/* ---------- Typography ---------- */
/* Official brand fonts per IEC-KAU-theme.md §2.
   Self-hosted from /static/fonts/ — no Google Fonts dependency. */

/* Alexandria — body text (§2 secondary font). */
@font-face { font-family: 'Alexandria'; font-weight: 100; font-style: normal; font-display: swap;
  src: url('/static/fonts/Alexandria/Alexandria-Thin.ttf') format('truetype'); }
@font-face { font-family: 'Alexandria'; font-weight: 200; font-style: normal; font-display: swap;
  src: url('/static/fonts/Alexandria/Alexandria-ExtraLight.ttf') format('truetype'); }
@font-face { font-family: 'Alexandria'; font-weight: 300; font-style: normal; font-display: swap;
  src: url('/static/fonts/Alexandria/Alexandria-Light.ttf') format('truetype'); }
@font-face { font-family: 'Alexandria'; font-weight: 400; font-style: normal; font-display: swap;
  src: url('/static/fonts/Alexandria/Alexandria-Regular.ttf') format('truetype'); }
@font-face { font-family: 'Alexandria'; font-weight: 500; font-style: normal; font-display: swap;
  src: url('/static/fonts/Alexandria/Alexandria-Medium.ttf') format('truetype'); }
@font-face { font-family: 'Alexandria'; font-weight: 600; font-style: normal; font-display: swap;
  src: url('/static/fonts/Alexandria/Alexandria-SemiBold.ttf') format('truetype'); }
@font-face { font-family: 'Alexandria'; font-weight: 700; font-style: normal; font-display: swap;
  src: url('/static/fonts/Alexandria/Alexandria-Bold.ttf') format('truetype'); }
@font-face { font-family: 'Alexandria'; font-weight: 800; font-style: normal; font-display: swap;
  src: url('/static/fonts/Alexandria/Alexandria-ExtraBold.ttf') format('truetype'); }
@font-face { font-family: 'Alexandria'; font-weight: 900; font-style: normal; font-display: swap;
  src: url('/static/fonts/Alexandria/Alexandria-Black.ttf') format('truetype'); }

/* The Year of The Camel — main headings (§2 primary font). */
@font-face { font-family: 'Year of The Camel'; font-weight: 100; font-style: normal; font-display: swap;
  src: url('/static/fonts/TheYearofTheCamel/TheYearofTheCamel-Thin.otf') format('opentype'); }
@font-face { font-family: 'Year of The Camel'; font-weight: 200; font-style: normal; font-display: swap;
  src: url('/static/fonts/TheYearofTheCamel/TheYearofTheCamel-ExtraLight.otf') format('opentype'); }
@font-face { font-family: 'Year of The Camel'; font-weight: 300; font-style: normal; font-display: swap;
  src: url('/static/fonts/TheYearofTheCamel/TheYearofTheCamel-Light.otf') format('opentype'); }
@font-face { font-family: 'Year of The Camel'; font-weight: 400; font-style: normal; font-display: swap;
  src: url('/static/fonts/TheYearofTheCamel/TheYearofTheCamel-Regular.otf') format('opentype'); }
@font-face { font-family: 'Year of The Camel'; font-weight: 500; font-style: normal; font-display: swap;
  src: url('/static/fonts/TheYearofTheCamel/TheYearofTheCamel-Medium.otf') format('opentype'); }
@font-face { font-family: 'Year of The Camel'; font-weight: 700; font-style: normal; font-display: swap;
  src: url('/static/fonts/TheYearofTheCamel/TheYearofTheCamel-Bold.otf') format('opentype'); }
@font-face { font-family: 'Year of The Camel'; font-weight: 800; font-style: normal; font-display: swap;
  src: url('/static/fonts/TheYearofTheCamel/TheYearofTheCamel-ExtraBold.otf') format('opentype'); }

html, body { font-family: 'Alexandria', system-ui, sans-serif; }

/* Hierarchy helpers per §3 of the theme spec. */
.font-display {
  font-family: 'Year of The Camel', 'Alexandria', system-ui, sans-serif;
  font-weight: 800;
  letter-spacing: -0.01em;
  line-height: 1.1;
}
.font-title {
  font-family: 'Year of The Camel', 'Alexandria', system-ui, sans-serif;
  font-weight: 700;
  line-height: 1.2;
}
.font-subtitle {
  font-family: 'Alexandria', system-ui, sans-serif;
  font-weight: 500;
  line-height: 1.4;
}
.font-body { font-family: 'Alexandria', system-ui, sans-serif; font-weight: 400; line-height: 1.5; }
.font-label { font-family: 'Alexandria', system-ui, sans-serif; font-weight: 600; }

/* Mobile-first touch targets per spec §14. */
.btn { min-height: 2.75rem; }

/* Sidebar active-item highlight. Overrides DaisyUI 5's default menu-active
   (which is a solid neutral fill and would hide the private chip / approval
   count / any trailing badge). Uses a primary tint with alpha so anything
   beneath the highlight stays legible. */
.menu :where(li > a.menu-active),
.menu :where(li > a.menu-active:hover),
.menu :where(li > a.menu-active:focus),
.menu :where(li > button.menu-active) {
  background-color: color-mix(in oklch, var(--color-primary) 14%, transparent);
  color: var(--color-primary);
  font-weight: 600;
  box-shadow: inset 3px 0 0 0 var(--color-primary);
}
[dir="rtl"] .menu :where(li > a.menu-active),
[dir="rtl"] .menu :where(li > button.menu-active) {
  box-shadow: inset -3px 0 0 0 var(--color-primary);
}

/* ── Grouped sidebar nav (collapsible <details> sections) ──────────────
   The aggregated count badge shows only while a group is COLLAPSED (when
   expanded, each child shows its own badge). */
.shell-nav details[open] > summary .nav-group-badge { display: none; }
.shell-nav .nav-group-badge { display: inline-flex; align-items: center; margin-inline-start: 0.25rem; }
/* Width containment: the extra nesting level must never push the fixed-width
   sidebar column wider (cf. DaisyUI nested-menu width inflation). */
.shell-nav.menu { width: 100%; max-width: 100%; min-width: 0; }
.shell-nav.menu > li,
.shell-nav.menu :where(details, summary, ul, li) { min-width: 0; max-width: 100%; }
/* Collapsed groups read slightly stronger than child rows (it's a category). */
.shell-nav > li > details > summary { font-weight: 600; }

/* Hide Alpine cloaked elements before the framework hydrates. */
[x-cloak] { display: none !important; }

/* HTMX indicator — override the framework default (opacity: 0) so the
   in-button spinner releases its space when idle. Without this, save
   buttons reserve width for the hidden spinner and the label looks
   off-center. */
.htmx-indicator { display: none !important; }
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator { display: inline-block !important; }

/* Sidebar tree filter: when `is-searching` is on the scroll wrapper, hide
   every tree node that doesn't have `search-hit`. Force-expand any matched
   chain by overriding x-show's display: none on `.tree-children`. */
.is-searching .tree-node { display: none; }
.is-searching .tree-node.search-hit { display: block; }
.is-searching .tree-children { display: block !important; }

/* Suppress the browser-native clear-X on the sidebar filter input so only
   our custom Alpine-driven clear button shows. */
.shell-sidebar input[type="search"]::-webkit-search-cancel-button,
.shell-sidebar input[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
  appearance: none;
}

/* ---------- Sidebar tree — per-level styling ----------
   Each .tree-node carries `data-tree-level="N"` AND `style="--tree-level: N"`.
   Levels 1–4 map to IEC theme tokens. Levels 5+ fall through to a deterministic
   HSL rule so deep trees still get a unique-but-consistent color per depth.

   `content-visibility: auto` is set on the <li> (not on .tree-row) so the
   ENTIRE node — row + its `.tree-children` <ul> subtree — is skipped when
   off-viewport. Critical: this is what makes deep restored-expanded trees
   cheap on the chevron flip; otherwise every newly-visible descendant pays
   the per-element style recalc tax. `contain-intrinsic-size: auto 32px`
   lets the browser remember the actual rendered height after first paint
   so the scrollbar stabilizes. See plans/investigate-sidebar-tree-expand-
   slow.md Fix 1. */
.tree-node {
  --tree-accent: var(--color-primary);
  content-visibility: auto;
  contain-intrinsic-size: auto 32px;
}

/* The admin departments tree (Screen #18) reuses .tree-node but its rows
   host absolutely-positioned popovers (manager user-picker). The paint
   containment implied by `content-visibility: auto` clips those popovers
   to the ~36px row box, so the admin tree opts out. Perf is fine without
   it here — /admin/departments was measured fast BEFORE containment
   existed (plans/investigate-sidebar-tree-expand-slow.md); only the
   sidebar tree on task-list screens needed the rule. */
.admin-dept-tree .tree-node { content-visibility: visible; }

/* Task rows — same rationale as `.tree-node` above. On task-list screens
   (/lists/{id}, /my, /search) the document can hold 100+ task rows, each
   with 5-9 inline SVGs and a stack of DaisyUI badge variants. Without
   this, any document recalc (e.g. a sidebar chevron click) ends up
   styling every off-screen row too. `auto 80px` is a reasonable initial
   estimate for the row's rendered height; the `auto` keyword lets the
   browser remember the actual size after first render so the scrollbar
   stabilises. See plans/task-list-perf-fix.md Fix A. */
.task-row {
  content-visibility: auto;
  contain-intrinsic-size: auto 80px;
}
.tree-node[data-tree-level="1"] { --tree-accent: var(--color-primary); }
.tree-node[data-tree-level="2"] { --tree-accent: var(--color-secondary); }
.tree-node[data-tree-level="3"] { --tree-accent: var(--color-accent); }
.tree-node[data-tree-level="4"] { --tree-accent: var(--color-info); }
.tree-node:not([data-tree-level="1"]):not([data-tree-level="2"]):not([data-tree-level="3"]):not([data-tree-level="4"]) {
  --tree-accent: hsl(calc(140 + var(--tree-level, 5) * 47) 55% 45%);
}

/* Leading accent stripe — a 2 px bar on the inline-start edge of each row.
   Drawn via a pseudo-element so no markup change is needed. Uses logical
   properties so it sits correctly in RTL. */
.tree-row { position: relative; }
.tree-row::before {
  content: "";
  position: absolute;
  inset-block: 4px;
  inset-inline-start: -2px;
  width: 2px;
  border-radius: 1px;
  background-color: var(--tree-accent);
  opacity: 0.6;
  pointer-events: none;
}
.tree-row:hover::before { opacity: 1; }

/* Entity icon (building / checklist) picks up the level accent. Slight
   alpha so it doesn't overwhelm the name. */
.tree-icon { color: var(--tree-accent); opacity: 0.85; }

/* Chevron uses muted base-content normally and the level accent on hover. */
.tree-chevron { color: color-mix(in oklab, var(--color-base-content) 50%, transparent); }
.tree-chevron:hover { color: var(--tree-accent); }

/* Row link hover/active states use the level accent. */
.tree-link:hover { background-color: color-mix(in oklab, var(--color-base-200) 100%, transparent); }
.tree-link-active {
  background-color: color-mix(in oklab, var(--tree-accent) 12%, transparent);
  color: var(--tree-accent);
  font-weight: 500;
}

/* Expanded sub-node container — tinted background + leading divider keyed to
   the PARENT node's accent (the var cascades down from the parent .tree-node).
   Children inside redeclare their own --tree-accent for their level. */
.tree-children {
  background-color: color-mix(in oklab, var(--tree-accent) 6%, transparent);
  border-inline-start: 1px solid color-mix(in oklab, var(--tree-accent) 25%, transparent);
  border-radius: 0 6px 6px 0;
  padding-block: 2px;
}
[dir="rtl"] .tree-children { border-radius: 6px 0 0 6px; }

/* Decorative "linear wave" background — brand element (§5). */
.iec-wave {
  background:
    radial-gradient(1200px 200px at 0% 0%, var(--iec-mint-light) 0, transparent 60%),
    radial-gradient(1000px 180px at 100% 100%, var(--iec-green-pale) 0, transparent 60%),
    var(--iec-tint);
}

/* ---------- App shell critical CSS ----------
   Hand-written (not Tailwind JIT) so the shell is laid out correctly on
   first paint. Without this, the Tailwind CDN needs a few ms after load
   to compile classes like `lg:!translate-x-0` and `lg:ps-[var(...)]`,
   during which the page looks like the mobile view on desktop. */

/* Clip horizontal overflow so the off-canvas sidebar (translated past the
   viewport in mobile state) cannot extend the document's scroll width — an
   issue specifically in RTL where translateX(100%) pushes the sidebar past
   the right edge. */
html, body { overflow-x: hidden; }

.shell-sidebar {
  position: fixed;
  inset-block: 0;
  inset-inline-start: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  /* Mobile default: full-screen so deeply-nested tree nodes never get clipped
     by the 288 px desktop default. Desktop override below restores the
     resizable width. */
  width: 100dvw;
  background-color: var(--color-base-100);
  border-inline-end: 1px solid var(--color-base-300);
  transform: translateX(-100%);
  transition: transform 0.2s ease-out;
}
[dir="rtl"] .shell-sidebar { transform: translateX(100%); }
.shell-sidebar.is-open { transform: translateX(0); }

.shell-main { min-height: 100vh; display: flex; flex-direction: column; min-width: 0; }

/* Constrain the topbar logo so it can't blow out the layout before Tailwind
   JIT (on the CDN) compiles `h-12 w-auto max-w-[200px]`. */
.shell-logo { height: 3rem; width: auto; max-width: 200px; object-fit: contain; }

.shell-desktop-only { display: none; }

@media (min-width: 1024px) {
  .shell-sidebar {
    transform: translateX(0) !important;
    width: var(--sidebar-w, 288px);
  }
  .shell-main { padding-inline-start: var(--sidebar-w, 288px); }
  .shell-mobile-only { display: none !important; }
  .shell-desktop-only { display: block; }
}

/* Hidden-scrollbar scrolling for in-card lists (e.g. template picker in the
   quick-create modal). Matches native-app feel: rows sit edge-to-edge of the
   container, scrolling is still available via wheel / touch / arrow keys. */
.scroll-clean {
  scrollbar-width: none;        /* Firefox */
  -ms-overflow-style: none;     /* old Edge / IE */
}
.scroll-clean::-webkit-scrollbar { display: none; }

/* Chat layout is always LTR, regardless of the page's UI direction. DaisyUI
   uses logical CSS props (border-end-start-radius, inset-inline-start, etc.)
   on the bubble + a `[dir=rtl]` override for the tail's rotateY. When the
   page's <html dir="rtl">, those logical props flip bubbles into RTL shape
   even if the chat row carries dir="ltr". Pin everything to PHYSICAL props so
   the bubble geometry can't drift no matter what the cascade does. Unlayered
   rules below beat DaisyUI's `@layer daisyui` rules. */

/* Force LTR direction on the chat row so grid column layout (1fr auto vs auto
   1fr) places avatar on the correct side. Inner `<div dir="auto">` on the
   bubble body keeps its auto text direction via HTML's `dir="auto"` semantics. */
.chat { direction: ltr; }

/* DaisyUI's default chat grid is two equal 1fr columns, capping each bubble at
   ~half the canvas. Make the avatar column intrinsic (auto) and give the bubble
   column the rest so long messages/media can use the full width (bubbles still
   stay fit-content for short text, up to .chat-bubble max-width). */
.chat.chat-start { grid-template-columns: auto minmax(0, 1fr); }
.chat.chat-end   { grid-template-columns: minmax(0, 1fr) auto; }

/* Bubble shape — physical corners so the flat corner can't flip on RTL roots. */
.chat.chat-start .chat-bubble {
  border-top-left-radius: var(--radius-field, 0.5rem);
  border-top-right-radius: var(--radius-field, 0.5rem);
  border-bottom-right-radius: var(--radius-field, 0.5rem);
  border-bottom-left-radius: 0;  /* flat on left — next to left avatar */
}
.chat.chat-end .chat-bubble {
  border-top-left-radius: var(--radius-field, 0.5rem);
  border-top-right-radius: var(--radius-field, 0.5rem);
  border-bottom-left-radius: var(--radius-field, 0.5rem);
  border-bottom-right-radius: 0;  /* flat on right — next to right avatar */
}

/* Tail pseudo — physical positions + rotation, no logical props. */
.chat.chat-start .chat-bubble::before {
  inset-inline: auto;
  left: -.75rem;
  right: auto;
  transform: rotateY(0);
}
.chat.chat-end .chat-bubble::before {
  inset-inline: auto;
  left: 100%;
  right: auto;
  transform: rotateY(180deg);
}

/* WhatsApp-style bubble colors — own = light green, others = white, dark text.
   Single light theme (data-theme="iec"), so literal WhatsApp colors are safe.
   The tail (::before) inherits the bubble's background so it always matches. */
.chat .chat-bubble {
  background-color: #ffffff;
  color: #111b21;
  box-shadow: 0 1px 0.5px rgba(11, 20, 26, 0.13);
}
.chat .chat-bubble-own { background-color: #d9fdd3; }
.chat .chat-bubble::before { background-color: inherit; }

/* Bubble content must wrap inside the grid column on narrow viewports.
   Long unbreakable strings (URLs, IDs, tokens) would otherwise widen the
   bubble past the chat canvas and produce a page-level horizontal scrollbar
   on mobile. min-width:0 lets the grid column shrink below intrinsic content
   width; overflow-wrap:anywhere breaks anywhere when needed. */
.chat-bubble {
  position: relative;        /* anchor for the overlapping reaction chips */
  min-width: 0;
  min-height: 0;             /* drop DaisyUI's 2.75rem floor — fit content */
  max-width: 90%;            /* of the now-full-width bubble column */
  padding-block: 0.4rem;     /* tighter than DaisyUI default — less vertical waste */
  padding-inline: 0.75rem;
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* "No messages yet" placeholder — always rendered inside the chat canvas so
   the canvas keeps its single flex-column layout in every state. Shown by
   default; hidden the moment any `.chat` bubble precedes it (server render OR
   HTMX beforeend OR cache injection OR the sync poller). The general-sibling
   rule (not :only-child) so the always-present scroll-up sentinel doesn't
   suppress the placeholder when the conversation is genuinely empty. */
.chat-empty-placeholder { display: flex; }
.chat ~ .chat-empty-placeholder { display: none; }

/* Scroll-up sentinel — a zero-height top anchor the cache watches
   (IntersectionObserver) to lazy-load older pages. Never visually present. */
.chat-top-sentinel { height: 1px; flex: none; pointer-events: none; }

/* WhatsApp-style chat composer — rounded pill wrapping a plain textarea
   with a styled highlight overlay painted behind it. Textarea has transparent
   text + visible caret; the overlay div shows the same text with inline
   bold/italic/delim/mention spans. Every character maps 1:1, so caret position
   is correct without any JS-side offset tracking. */
.chat-composer-pill {
  display: flex;
  align-items: flex-end;
  gap: 0.125rem;
  background-color: var(--color-base-100);
  border: 1px solid var(--color-base-300);
  border-radius: 1.75rem;
  padding: 0.25rem 0.5rem;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
  transition: border-color 0.15s ease;
}
.chat-composer-pill:focus-within {
  border-color: color-mix(in oklab, var(--color-primary) 40%, var(--color-base-300));
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-primary) 15%, transparent);
}
.chat-composer-pill .btn-circle {
  width: 2rem;
  height: 2rem;
  min-height: 2rem;
  aspect-ratio: 1 / 1;
  padding: 0;
  flex: 0 0 auto;
}
.chat-editor-stack {
  display: block;
}
.chat-textarea,
.chat-highlight {
  width: 100%;
  min-height: 2.25rem;
  max-height: 12rem;
  box-sizing: border-box;
  padding: 0.5rem 1.25rem;
  line-height: 1.45;
  font-size: 0.875rem;
  font-family: inherit;
  white-space: pre-wrap;
  overflow-wrap: break-word;
  word-break: break-word;
}
.chat-textarea {
  position: relative;
  z-index: 1;
  display: block;
  flex: 1 1 auto;
  min-width: 0;
  resize: none;
  border: 0;
  outline: none;
  background: transparent;
  color: transparent;
  caret-color: var(--color-base-content);
  overflow-y: auto;
}
.chat-textarea::selection { background-color: color-mix(in oklab, var(--color-primary) 25%, transparent); color: transparent; }
.chat-highlight {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  color: var(--color-base-content);
  overflow: hidden;
}
.chat-highlight-inner { will-change: transform; }
.chat-highlight .md-delim { opacity: 0.4; }
.chat-highlight .md-ol    { color: color-mix(in oklab, var(--color-primary) 70%, var(--color-base-content)); font-weight: 500; }
.chat-highlight .md-mention {
  color: var(--color-primary);
  background-color: color-mix(in oklab, var(--color-primary) 10%, transparent);
  border-radius: 0.25rem;
  padding: 0 0.15rem;
}
.chat-send-btn,
.chat-mic-btn,
.chat-side-btn {
  width: 2.5rem;
  height: 2.5rem;
  min-height: 2.5rem;
  aspect-ratio: 1 / 1;
  padding: 0;
  flex: 0 0 auto;
  align-self: flex-end;
}

/* ── Composer action bar — ALWAYS left-to-right, independent of UI language.
   Only the <textarea> flips direction (chatComposer.applyDir); the bar layout
   (+ on the far start, mic/send on the far end) stays fixed like WhatsApp. */
.chat-composer-row {
  direction: ltr;
  display: flex;
  align-items: flex-end;
  gap: 0.5rem;
}
.chat-text-form {
  display: flex;
  align-items: flex-end;
  gap: 0.5rem;
  flex: 1 1 auto;
  min-width: 0;
}

/* Emoji panel (emoji-picker-element web component) — themed via its documented
   CSS custom properties so it tracks the active DaisyUI theme + RTL search. */
.chat-emoji-panel { width: max-content; }
.chat-emoji-picker {
  --background: var(--color-base-100);
  --border-color: var(--color-base-300);
  --input-border-color: var(--color-base-300);
  --indicator-color: var(--color-primary);
  --input-font-color: var(--color-base-content);
  --button-active-background: color-mix(in oklab, var(--color-primary) 18%, transparent);
  --button-hover-background: color-mix(in oklab, var(--color-base-content) 8%, transparent);
  border: 1px solid var(--color-base-300);
  border-radius: 1rem;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
  height: 22rem;
  width: 20rem;
  max-width: min(20rem, 80vw);
}

/* Recording state — red pulse + live timer in a pill-shaped status strip. */
.chat-record-status,
.chat-preview-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background-color: var(--color-base-100);
  border: 1px solid var(--color-base-300);
  border-radius: 1.75rem;
  padding: 0.5rem 1rem;
  min-height: 2.75rem;
}
.chat-rec-dot {
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 9999px;
  background-color: var(--color-error);
  flex: 0 0 auto;
  animation: chat-rec-pulse 1.2s ease-in-out infinite;
}
@keyframes chat-rec-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.25; }
}

/* ── Decorative waveform — shared by the composer rows AND the sent bubble.
   Thin rounded bars centred on a baseline; --wf carries the played fraction
   (a left→right CSS var) for the progress dot. */
.chat-wf {
  display: flex;
  align-items: center;
  /* Thin bars are capped at 3px, so on a wide pill they'd cluster at the
     start; space-between spreads them evenly across the whole track (when the
     track is narrow and bars overflow, this collapses back to the 2px gap). */
  justify-content: space-between;
  gap: 2px;
  height: 1.75rem;
  position: relative;
  cursor: pointer;
  /* Let pointer-drag scrub the wave without the touch starting a page scroll. */
  touch-action: none;
}
.chat-wf-bar {
  flex: 1 1 0;
  min-width: 2px;
  max-width: 3px;
  border-radius: 9999px;
  background-color: color-mix(in oklab, currentColor 28%, transparent);
  transition: background-color 0.1s linear;
}
.chat-wf-bar.is-played {
  background-color: var(--color-primary);
}
.chat-wf-dot {
  position: absolute;
  top: 50%;
  inset-inline-start: var(--wf, 0%);
  width: 0.7rem;
  height: 0.7rem;
  margin-inline-start: -0.35rem;
  border-radius: 9999px;
  background-color: var(--color-primary);
  box-shadow: 0 0 0 2px var(--color-base-100);
  transform: translateY(-50%);
  pointer-events: none;
}
/* Recording: a live voice-like wave scrolls in (driven by JS in chatComposer);
   fade the entering/leaving edges so bars don't pop. No progress dot/seek while
   live. Under reduced-motion the JS renders a static seeded shape instead. */
.chat-wf--rec {
  cursor: default;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 10px, #000 calc(100% - 6px), transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 10px, #000 calc(100% - 6px), transparent 100%);
}
.chat-wf--rec .chat-wf-bar { background-color: var(--color-error); }

/* Respect the OS reduce-motion setting — freeze the blinking record dot. */
@media (prefers-reduced-motion: reduce) {
  .chat-rec-dot { animation: none; }
}

/* Media bubbles — trim the text padding so images/players sit edge-to-edge. */
.chat-bubble-media { padding: 0.35rem; }
/* Sent-voice-bubble custom player: play · speed · waveform · time. */
.chat-voice {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: min(17rem, 62vw);
  max-width: 100%;
  padding: 0.15rem 0.25rem;
}
.chat-voice-play {
  flex: 0 0 auto;
  width: 2rem;
  height: 2rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 9999px;
  color: var(--color-primary);
  background-color: color-mix(in oklab, var(--color-primary) 14%, transparent);
}
.chat-voice-play:hover {
  background-color: color-mix(in oklab, var(--color-primary) 24%, transparent);
}
.chat-voice-speed {
  flex: 0 0 auto;
  min-width: 2.2rem;
  height: 1.5rem;
  padding: 0 0.4rem;
  font-size: 0.7rem;
  font-weight: 600;
  line-height: 1;
  border-radius: 9999px;
  color: var(--color-primary);
  background-color: color-mix(in oklab, var(--color-primary) 16%, transparent);
}
.chat-voice-speed:hover {
  background-color: color-mix(in oklab, var(--color-primary) 26%, transparent);
}
.chat-voice-time {
  flex: 0 0 auto;
  font-size: 0.72rem;
  opacity: 0.7;
}
/* WhatsApp-style image bubble — the photo fills the bubble with only a hairline
   of bubble background (green/white) showing as a frame. The link wraps + clips
   the image and a bottom gradient scrim that carries the overlaid timestamp. */
.chat-bubble:has(.chat-image-link) { padding: 0.18rem; }
.chat-image-link {
  position: relative;
  display: block;
  line-height: 0;
  border-radius: 0.45rem;
  overflow: hidden;
}
.chat-image-attachment {
  display: block;
  max-width: min(20rem, 75vw);
  max-height: 24rem;
  width: auto;
  height: auto;
  object-fit: cover;
}
/* Bottom scrim so the white timestamp stays readable over any photo. */
.chat-image-link::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: 2.4rem;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.5), transparent);
  pointer-events: none;
}
.chat-img-time {
  position: absolute;
  bottom: 0.25rem;
  inset-inline-end: 0.45rem;
  z-index: 1;
  color: #fff;
  font-size: 0.65rem;
  line-height: 1;
  letter-spacing: 0.02em;
}
.chat-file-card {
  max-width: min(26rem, 80%);
  padding: 0.35rem 0.5rem;
  border-radius: 0.6rem;
  color: inherit;
  text-decoration: none;
}
.chat-file-card:hover {
  background-color: color-mix(in oklab, var(--color-base-content) 8%, transparent);
}

/* Date divider — WhatsApp-style: a small, centered, rounded, subtle pill that
   floats above the canvas (Today / Yesterday / YYYY-MM-DD). */
.chat-day-divider {
  display: inline-block;
  font-size: 0.7rem;
  font-weight: 500;
  line-height: 1;
  color: color-mix(in oklab, var(--color-base-content) 70%, transparent);
  background-color: var(--color-base-100);
  padding: 0.3rem 0.7rem;
  border-radius: 9999px;
  box-shadow: 0 1px 1.5px rgba(11, 20, 26, 0.1);
}

/* ── Mobile "expanded chat" mode ──────────────────────────────────────────
   The expand icon (beside the Chat tab) and the collapse bar are hidden by
   default and only revealed below the app's desktop breakpoint (1024px). When
   `.chat-expanded` is set on the chat panel wrapper, it fills the viewport in
   place — no new route/modal. z-index 45 keeps it over the topbar/closed sidebar
   but UNDER the chat's own body-level popovers (modals 50, bubble-menu 60). */
.chat-expand-icon { display: none; }
.chat-collapse-bar { display: none; }
body.chat-locked { overflow: hidden; }
@media (max-width: 1023.98px) {
  .chat-expand-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.7rem;
    height: 1.7rem;
    margin-inline-end: 0.15rem;
    align-self: center;
    border-radius: 9999px;
    color: var(--color-base-content);
    opacity: 0.7;
    transition: opacity 0.12s ease, background-color 0.12s ease;
  }
  .chat-expand-icon:hover,
  .chat-expand-icon:focus-visible {
    opacity: 1;
    background-color: color-mix(in oklab, var(--color-primary) 12%, transparent);
  }
  .chat-expanded {
    position: fixed !important;
    inset: 0 !important;
    height: 100dvh !important;
    width: 100vw !important;
    max-width: none !important;
    margin: 0 !important;
    padding: 0 !important;            /* edge-to-edge — no left/right frame */
    z-index: 45;
    background-color: var(--color-base-100);
  }
  /* Message list fills the full width (no rounded inset / side margin). */
  .chat-expanded [id$="-chat-canvas"] {
    border-radius: 0;
  }
  /* Keep only the collapse bar and composer off the very screen edge. */
  .chat-expanded .chat-collapse-bar {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.1rem 0.5rem 0.3rem;
    border-bottom: 1px solid var(--color-base-200);
  }
  .chat-expanded .chat-composer-root { padding-inline: 0.4rem; }
}

/* Sender name inside the bubble (foreign messages, all kinds) — small, colored,
   sits above the content. Timestamp (.chat-bubble-time) trails the content. */
.chat-sender {
  display: block;
  font-size: 0.72rem;
  font-weight: 600;
  line-height: 1.15;
  color: var(--color-primary);
  margin-bottom: 0.1rem;
}
/* Image bubbles have hairline padding — give the name a little room over the photo. */
.chat-bubble-media .chat-sender { padding: 0.05rem 0.25rem 0.15rem; margin-bottom: 0; }

/* Inline timestamp inside bubbles (shown for all messages now). Small, muted,
   trailing. */
.chat-bubble-time {
  display: block;
  margin-block-start: 0.1rem;
  font-size: 0.65rem;
  line-height: 1;
  opacity: 0.6;
  text-align: end;
}
/* Others' messages: the reaction chips overlap the bubble's END (right) corner,
   so put the timestamp at the START (left) to avoid colliding with them. (Own
   messages keep the time at the end — their chips are on the start side.) */
.chat.chat-start .chat-bubble-time { text-align: start; }

/* Reactions overlapping the bubble's bottom corner (WhatsApp/Telegram style),
   anchored to the corner OPPOSITE the tail. The chip cluster is the HTMX swap
   target (#chat-msg-{id}-reactions); empty when there are no reactions. */
.chat-reactions {
  position: absolute;
  bottom: -0.7rem;
  display: flex;
  gap: 0.2rem;
  flex-wrap: wrap;
  z-index: 1;
}
.chat.chat-end  .chat-reactions { inset-inline-start: 0.5rem; }
.chat.chat-start .chat-reactions { inset-inline-end: 0.5rem; }
/* Reserve a little space below a bubble ONLY when it actually carries reactions,
   so the overlapping chips don't collide with the next message. :has re-runs
   automatically after the HTMX reaction swap. */
.chat:has(.chat-reactions:not(:empty)) { margin-bottom: 0.7rem; }
/* WhatsApp-style reaction chips — white, and CIRCULAR: a single reaction is a
   perfect circle (square box + full radius); it only stretches into a pill when
   a count (>1) is shown. A primary border marks the user's own reactions. */
.chat-reactions .badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.1rem;
  width: 1.6rem;            /* fixed square → perfect circle for a lone emoji */
  height: 1.6rem;
  min-width: 1.6rem;
  padding: 0;
  border-radius: 9999px;
  background-color: #ffffff;
  color: #111b21;
  border: none;
  box-shadow: 0 1px 2px rgba(11, 20, 26, 0.18);
  font-size: 0.9rem;
  line-height: 1;
}
/* Only stretch into a pill when a count (>1) is actually shown. */
.chat-reactions .badge:has(bdi) { width: auto; padding: 0 0.4rem; }

/* Hover icons — absolute to the position:relative bubble so they track its
   variable width. Hidden until the bubble is hovered, then FULLY opaque; kept
   reachable on touch. .chat is forced LTR, so physical left/right == visual.
   Layout:
     • React (😊): always BESIDE the bubble's inner edge, vertically centered.
     • Menu (⋯): MOBILE → stacked ABOVE the react icon (limited beside-width);
                 DESKTOP (≥640px) → overlay the bubble's TOP-RIGHT corner. */
.chat-tool-react,
.chat-tool-menu {
  position: absolute;
  opacity: 0;
  transition: opacity 0.12s ease;
  z-index: 2;
}
.chat:hover .chat-tool-react,
.chat:hover .chat-tool-menu,
.chat-tool-react:focus-visible,
.chat-tool-menu:focus-visible { opacity: 1; }
@media (hover: none) { .chat-tool-react, .chat-tool-menu { opacity: 0.55; } }

/* React — beside the bubble's inner edge, vertically centered (all sizes). */
.chat-tool-react { top: 50%; transform: translateY(-50%); }
.chat.chat-end  .chat-tool-react { right: 100%; margin-right: 0.2rem; }
.chat.chat-start .chat-tool-react { left: 100%; margin-left: 0.2rem; }

/* Menu — MOBILE default: beside the bubble, stacked ABOVE the react icon. The
   two icons are CENTERED as a group on the bubble (menu in the top half, react
   in the bottom half) so a short bubble overflows only ~0.3rem each side — which
   the mobile .chat margin-top below absorbs (no overlap with the previous msg). */
.chat-tool-menu { top: 50%; transform: translateY(calc(-100% - 0.15rem)); }
.chat.chat-end  .chat-tool-menu { right: 100%; margin-right: 0.2rem; }
.chat.chat-start .chat-tool-menu { left: 100%; margin-left: 0.2rem; }
@media (max-width: 639.98px) {
  .chat-tool-react { transform: translateY(0.15rem); }   /* bottom half of the stack */
  .chat { margin-top: 0.65rem; }                          /* spacing above each bubble */
}

/* Menu — DESKTOP: overlay the bubble's top-right corner. */
@media (min-width: 640px) {
  .chat-tool-menu {
    top: 0.2rem;
    right: 0.2rem;
    left: auto;
    margin: 0;
    transform: none;
  }
  .chat.chat-start .chat-tool-menu { left: auto; right: 0.2rem; }
  .chat.chat-end   .chat-tool-menu { right: 0.2rem; }
}
.chat-tool-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  border-radius: 9999px;
  background-color: var(--color-base-100);
  border: 1px solid var(--color-base-300);
  color: var(--color-base-content);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
}
.chat-tool-btn:hover {
  background-color: color-mix(in oklab, var(--color-primary) 12%, var(--color-base-100));
  border-color: color-mix(in oklab, var(--color-primary) 40%, var(--color-base-300));
}

/* Body-level bubble menu + reaction picker (fixed → escapes canvas clipping). */
.chat-menu-backdrop { position: fixed; inset: 0; z-index: 60; }
.chat-menu-pop { min-width: 11rem; }
.chat-react-pop { max-width: min(22rem, 92vw); }
.chat-react-emoji {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.1rem;
  height: 2.1rem;
  border-radius: 9999px;
  font-size: 1.25rem;
  line-height: 1;
  transition: background-color 0.1s ease, transform 0.1s ease;
}
.chat-react-emoji:hover {
  background-color: color-mix(in oklab, var(--color-base-content) 10%, transparent);
  transform: scale(1.15);
}
.chat-react-more {
  font-size: 1rem;
  color: var(--color-base-content);
  opacity: 0.7;
}
.chat-react-panel { padding-top: 0.25rem; }

/* Quoted-reply block inside a bubble (clickable → jumps to the original). */
.chat-reply-quote {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.05rem;
  width: 100%;
  text-align: start;
  margin-bottom: 0.3rem;
  padding: 0.2rem 0.5rem;
  border-inline-start: 3px solid color-mix(in oklab, var(--color-primary) 60%, transparent);
  border-radius: 0.35rem;
  background-color: color-mix(in oklab, var(--color-base-content) 6%, transparent);
  font-size: 0.78rem;
  cursor: pointer;
}
.chat-reply-quote:hover {
  background-color: color-mix(in oklab, var(--color-base-content) 12%, transparent);
}
.chat-reply-author { color: var(--color-primary); }
.chat-reply-snippet {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  opacity: 0.85;
}
/* Own bubble is now light (dark text), so the quote uses the default dark
   styling — no special-case override needed. */

/* Reply preview bar above the composer pill. */
.chat-reply-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
  padding: 0.35rem 0.6rem;
  border-inline-start: 3px solid var(--color-primary);
  border-radius: 0.4rem;
  background-color: color-mix(in oklab, var(--color-base-content) 6%, transparent);
}
.chat-reply-bar-body { display: flex; flex-direction: column; }
.chat-reply-bar-label { font-size: 0.72rem; opacity: 0.7; }
.chat-reply-bar-snippet { font-size: 0.82rem; }
/* Editing banner reuses .chat-reply-bar; tint its rail to distinguish edit mode. */
.chat-editing-bar { border-inline-start-color: var(--color-info); }
/* Pending-attachment preview (file/photo chosen, awaiting caption + send). */
.chat-pending-thumb { width: 2rem; height: 2rem; object-fit: cover; border-radius: 0.3rem; }

/* "Edited" label + pin flag shown inline with the bubble timestamp. */
.chat-edited { font-style: italic; opacity: 0.85; margin-inline-end: 0.3rem; }
.chat-bubble-meta { display: block; margin-block-start: 0.1rem; font-size: 0.65rem; opacity: 0.6; text-align: end; }
.chat-pinned-flag { margin-inline-end: 0.2rem; }
.chat-bubble-time .chat-pinned-flag,
.chat-bubble-time .chat-edited { font-size: 0.6rem; }

/* File-mention chip in a rendered message — an inline file-attachment card
   (type icon + truncated name) that jumps to the Files tab on click. */
.chat-file-mention {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  max-width: 14rem;
  vertical-align: bottom;
  padding: 0.1rem 0.4rem;
  border-radius: 0.5rem;
  text-decoration: none;
  color: inherit;
  background-color: color-mix(in oklab, var(--color-base-content) 8%, transparent);
  border: 1px solid color-mix(in oklab, var(--color-base-content) 12%, transparent);
}
.chat-file-mention:hover { background-color: color-mix(in oklab, var(--color-base-content) 14%, transparent); }
.chat-file-mention--missing { opacity: 0.7; cursor: default; }
.chat-file-mention-icon { display: inline-flex; flex: 0 0 auto; }
.chat-file-mention-name {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
/* File-type tints (hand-written so the dynamically-injected color survives the
   Tailwind CDN JIT — see memory tailwind_cdn_fouc). */
.chat-file-tone-error     { color: var(--color-error); }
.chat-file-tone-info      { color: var(--color-info); }
.chat-file-tone-success   { color: var(--color-success); }
.chat-file-tone-warning   { color: var(--color-warning); }
.chat-file-tone-secondary { color: var(--color-secondary); }
.chat-file-tone-accent    { color: var(--color-accent); }
.chat-file-tone-neutral   { color: var(--color-base-content); }
/* Highlight a Files-tab row when reached from a chat file-mention (ring only, so
   it doesn't fight the row's own bg-base-100 / new-file bg-error/5). */
.chat-file-pulse { animation: chat-file-pulse 1.6s ease; }
@keyframes chat-file-pulse {
  0%, 100% { box-shadow: 0 0 0 0 transparent; }
  25%      { box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-warning) 55%, transparent); }
}
/* Composer overlay coloring for the [[file:…]] token (mirrors .md-mention). */
.chat-highlight .md-file {
  color: var(--color-info);
  background-color: color-mix(in oklab, var(--color-info) 10%, transparent);
  border-radius: 0.25rem;
  padding: 0 0.15rem;
}
.chat-suggest-fileicon { font-size: 1rem; width: 1.5rem; text-align: center; }

/* Pinned-message bar above the chat canvas. */
.chat-pinned-bar {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  max-height: 5.5rem;
  overflow-y: auto;
  padding: 0.25rem 0.4rem;
  border-inline-start: 3px solid var(--color-warning);
  border-radius: 0.4rem;
  background-color: color-mix(in oklab, var(--color-warning) 8%, transparent);
}
.chat-pinned-item { display: flex; align-items: center; gap: 0.4rem; }
.chat-pinned-jump {
  display: flex; align-items: center; gap: 0.4rem; flex: 1 1 auto;
  text-align: start; font-size: 0.8rem; padding: 0.1rem 0.15rem; border-radius: 0.3rem;
}
.chat-pinned-jump:hover { background-color: color-mix(in oklab, var(--color-base-content) 6%, transparent); }
.chat-pinned-author { font-size: 0.75rem; opacity: 0.85; }
.chat-pinned-snippet { font-size: 0.8rem; opacity: 0.7; }

/* Swipe-to-reply (touch). pan-y keeps vertical scroll native; the drag transform
   is applied inline by JS. The reply arrow fades in via the --swipe progress var. */
.chat[data-can-reply="1"] { touch-action: pan-y; }
.chat-swiping { position: relative; }
.chat-swiping::before {
  content: "↩";
  position: absolute;
  inset-inline-start: -1.75rem;
  top: 50%;
  transform: translateY(-50%);
  font-size: 1.1rem;
  color: var(--color-primary);
  opacity: var(--swipe, 0);
  pointer-events: none;
}

/* Soft-delete tombstone + jump-to pulse. */
.chat-bubble-deleted {
  background-color: transparent !important;
  color: var(--color-base-content) !important;
  border: 1px dashed var(--color-base-300);
  box-shadow: none;
}
.chat-bubble-deleted::before { display: none !important; }  /* drop the tail */
.chat-pulse { animation: chat-pulse 1.3s ease; }
@keyframes chat-pulse {
  0%, 100% { background-color: transparent; }
  25% { background-color: color-mix(in oklab, var(--color-warning) 30%, transparent); }
}

/* Long-message clamp — collapse tall bodies with a fade + Show more toggle. */
.chat-clamped {
  max-height: 16rem;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, #000 70%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 70%, transparent 100%);
}
.chat-showmore { display: inline-block; margin-top: 0.15rem; }

/* Flash toast — self-contained styling (does NOT use DaisyUI .alert, which
   paints `--alert-color` as the background and fights our theme-neutral look).
   Slide-in on insert, hold, fade-out; JS removes the element on animationend. */
.flash-toast {
  display: grid;
  grid-auto-flow: column;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 0.5rem 0.75rem 1rem;
  background-color: var(--color-base-100);
  color: var(--color-base-content);
  border: 1px solid var(--color-base-300);
  border-inline-start: 4px solid var(--color-info);
  border-radius: var(--radius-box, 0.5rem);
  width: 100%;
  pointer-events: auto;
  opacity: 0;
  animation:
    flash-toast-in 220ms ease-out forwards,
    flash-toast-out 360ms ease-in 3600ms forwards;
}
.flash-toast-success { border-inline-start-color: var(--color-success); }
.flash-toast-success > svg:first-child { color: var(--color-success); }
.flash-toast-error   { border-inline-start-color: var(--color-error);   }
.flash-toast-error   > svg:first-child { color: var(--color-error);   }
.flash-toast-warning { border-inline-start-color: var(--color-warning); }
.flash-toast-warning > svg:first-child { color: var(--color-warning); }
.flash-toast-info    { border-inline-start-color: var(--color-info);    }
.flash-toast-info    > svg:first-child { color: var(--color-info);    }
@keyframes flash-toast-in {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes flash-toast-out {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(-8px); }
}

/* ---------- Admin departments tree (Screen #18) ----------
   Reuses .tree-node / .tree-row / .tree-icon / .tree-chevron / .tree-children
   from the sidebar (above). This variant just resets the <ul> defaults so
   nesting drives indentation, adds a grid row layout, and gives editable
   fields a hover affordance. */
.admin-dept-tree, .admin-dept-tree ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
.admin-dept-tree ul { padding-inline-start: 1.25rem; }

.admin-dept-tree .tree-row-inner {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 0.5rem;
  /* Match sidebar's .tree-link spacing (py-1.5 px-2). */
  padding: 0.375rem 0.5rem;
  border-radius: 6px;
  transition: background-color 120ms;
  min-width: 0;
}
.admin-dept-tree .tree-row-inner:hover {
  background-color: color-mix(in oklab, var(--tree-accent) 8%, transparent);
}
/* Name cell grows; everything else stays at its natural width. */
.admin-dept-tree .dept-name-cell { flex: 1 1 auto; min-width: 0; }
.admin-dept-tree .dept-manager-cell,
.admin-dept-tree .dept-counts-cell { flex: 0 0 auto; }
@media (max-width: 768px) {
  .admin-dept-tree .dept-manager-cell { display: none !important; }
}
@media (max-width: 540px) {
  .admin-dept-tree .dept-counts-cell { display: none !important; }
}

/* Inline-editable affordance: dotted underline on hover; cursor: text/pointer. */
.inline-edit-trigger {
  cursor: pointer;
  border-radius: 4px;
  padding: 2px 4px;
  margin: -2px -4px;
  transition: background-color 120ms, box-shadow 120ms;
}
.inline-edit-trigger:hover {
  background-color: color-mix(in oklab, var(--tree-accent) 14%, transparent);
  box-shadow: inset 0 -1px 0 0 color-mix(in oklab, var(--tree-accent) 60%, transparent);
}

/* Name text: match the sidebar's plain `.tree-link` text-sm — no per-level
   typography. The level distinction comes from the existing accent stripe
   (--tree-accent) and icon color cascade. */
.admin-dept-tree .dept-name-text { font-size: 0.875rem; }

/* Compact count badge styles. */
.dept-count-badge {
  display: inline-flex; align-items: center; gap: 0.25rem;
  font-size: 0.7rem; padding: 1px 6px; border-radius: 999px;
  background-color: color-mix(in oklab, var(--tree-accent) 12%, transparent);
  color: color-mix(in oklab, var(--tree-accent) 80%, var(--color-base-content));
}

/* Kill the native browser search-clear (we render our own × button). */
#admin-dept-tree-root input[type="search"]::-webkit-search-cancel-button,
#admin-dept-tree-root input[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
  appearance: none;
}

/* List rows inside the admin tree: slightly muted icon to differentiate
   visually from dept rows even before reading the icon shape. */
.admin-dept-tree .tree-node.is-list .tree-icon { opacity: 0.7; }

/* On mobile, inline edit triggers (click name / chip → swap input) are
   disabled — the cramped layout makes them unusable on small screens. Use
   the dedicated edit drawer instead (the pencil-icon button stays clickable). */
@media (max-width: 767px) {
  .admin-dept-tree .inline-edit-trigger {
    pointer-events: none;
    cursor: default;
  }
  .admin-dept-tree .inline-edit-trigger:hover {
    background-color: transparent;
    box-shadow: none;
  }
}

/* Admin drawer forms — flex+overflow handshake.
   The form is `flex-1` inside a `flex flex-col` aside. For `overflow-y-auto`
   to actually scroll, the form needs `min-height: 0` (the default
   `min-height: auto` keeps the form expanded to content height, which
   pushes the bottom rows past the aside's bounds when there are many
   chips/members). Tailwind's `min-h-0` utility is unreliable here because
   the drawer is HTMX-injected — the play CDN may not have compiled it. */
#admin-dept-form,
#admin-list-form {
  min-height: 0;
}

/* Screen #19 Phase J — Full-page Status Template editor layout.
   Sticky topbar + sidebar (280px ≥1024px, accordion below) + main + bottom
   workflow preview. All horizontal spacing uses logical properties for RTL.
   J.2/J.3 fill the placeholders the J.1 template renders. */
.template-editor {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  min-height: calc(100vh - 12rem);
}
.template-editor__topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
  padding-block: 0.5rem;
  padding-inline: 0.75rem;
  background: var(--color-base-100);
  border: 1px solid var(--color-base-300);
  border-radius: var(--radius-box, 1rem);
  box-shadow: 0 1px 2px rgba(0,0,0,0.04);
}
.template-editor__body {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.75rem;
  min-width: 0;
}
@media (min-width: 1024px) {
  .template-editor__body {
    grid-template-columns: 280px 1fr;
  }
}
.template-editor__sidebar {
  background: var(--color-base-100);
  border: 1px solid var(--color-base-300);
  border-radius: var(--radius-box, 1rem);
  min-width: 0;
  padding: 0.5rem;
}
.template-editor__sidebar-inner { min-width: 0; }
/* Mobile: nav hidden by default, expand via toggle button.
   Desktop ≥1024px: nav always visible, toggle hidden. */
.template-editor__sidebar-nav {
  display: none;
  margin-block-start: 0.5rem;
}
.template-editor__sidebar-nav.is-open { display: flex; }
@media (min-width: 1024px) {
  .template-editor__sidebar-nav { display: flex; margin-block-start: 0; }
  .template-editor__sidebar-toggle { display: none; }
}
/* Phase J.6 — status sub-tree scrolls when >10 statuses so the
   sidebar doesn't push the editor body off the viewport. */
.template-editor__sidebar-nav details > ul {
  max-height: 22rem;
  overflow-y: auto;
}
/* Phase J.6 — focus-visible outlines for interactive elements that
   don't already get one from DaisyUI. */
.template-editor__sidebar button:focus-visible,
.template-editor__sidebar summary:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
  border-radius: var(--radius-box, 0.5rem);
}
.workflow-preview__node:focus-visible rect {
  stroke: var(--color-primary);
  stroke-width: 3;
}

/* Phase K.2 — Status color swatches inside the master-detail panel.
   8 DaisyUI semantic colors as larger clickable circles, with a ring
   on the selected one. Keyboard-focusable. */
.status-swatch {
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 9999px;
  border: 2px solid transparent;
  cursor: pointer;
  transition: transform 0.15s ease, border-color 0.15s ease;
  padding: 0;
}
.status-swatch:hover { transform: scale(1.08); }
.status-swatch:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}
.status-swatch.is-selected {
  border-color: var(--color-base-content);
}
.status-swatch--neutral   { background: var(--color-neutral); }
.status-swatch--primary   { background: var(--color-primary); }
.status-swatch--secondary { background: var(--color-secondary); }
.status-swatch--accent    { background: var(--color-accent); }
.status-swatch--info      { background: var(--color-info); }
.status-swatch--success   { background: var(--color-success); }
.status-swatch--warning   { background: var(--color-warning); }
.status-swatch--error     { background: var(--color-error); }
/* Phase K polish — extended hex palette (Tailwind 500-shade) so
   admins have 16 distinguishable picks instead of 8 semantic ones. */
.status-swatch--rose      { background: #f43f5e; }
.status-swatch--pink      { background: #ec4899; }
.status-swatch--purple    { background: #a855f7; }
.status-swatch--indigo    { background: #6366f1; }
.status-swatch--sky       { background: #0ea5e9; }
.status-swatch--teal      { background: #14b8a6; }
.status-swatch--lime      { background: #84cc16; }
.status-swatch--amber     { background: #f59e0b; }
/* Custom-color picker swatch — checkerboard background when no hex
   is set yet so it reads as "pick any color". Selected state inherits
   the chosen hex via inline-style on the wrapper. */
.status-swatch--custom {
  background: repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 0.7rem 0.7rem;
  position: relative;
}
/* Vertical separator between the preset palette and the custom-color
   picker so the latter reads as its own affordance. */
.status-swatch-sep {
  display: inline-block;
  width: 1px;
  align-self: stretch;
  background: var(--color-base-300);
  margin-inline: 0.25rem;
}

/* Phase K-polish — fix profile-picture clipping inside picker
   trigger buttons. DaisyUI 5 globally adds `.avatar-group .avatar
   { border: 4px solid var(--color-base-100); }` so every avatar
   inside a group carries an 8px-wide cream-colored ring. On a
   btn-sm (32px tall) primary button that ring (a) bursts past the
   button's content area at top/bottom, clipping the avatars, and
   (b) reads as a misplaced cream halo on the green background.
   Scope-override inside .btn: thin the ring to 2px and bleed it
   into the button background via currentColor (transparent halo). */
.btn .avatar-group .avatar {
  border-width: 2px;
  border-color: currentColor;
}
/* Pickers also need a touch more vertical room than btn-sm's
   default 32px — bumping to 36px (min-h-9) gives the now-24px
   avatars (20px content + 4px own border) clean breathing space. */
.template-editor__main .btn-sm:has(.avatar-group),
.template-editor__main .btn-xs:has(.avatar-group) {
  min-height: 2.25rem;
}

/* Inline status-nav row (forward / back arrows under the status pill
   in the task header). Tight square buttons that read as a single
   navigation unit, separated by a faint divider so they don't look
   like two random ghost buttons. */
.task-status-nav {
  padding: 0.15rem 0.35rem;
  border: 1px solid var(--color-base-300);
  border-radius: 9999px;
  background: var(--color-base-100);
}
.task-status-nav__btn {
  width: 1.5rem;
  height: 1.5rem;
  min-height: 1.5rem;
  padding: 0;
}
.task-status-nav__btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

/* Sleek "+ Add status" trigger in the sidebar sub-tree — outlined
   pill that doesn't compete with the primary Save button in the
   topbar, but still reads as clickable (primary color text + border,
   hover fills). */
.template-editor__add-status {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  width: 100%;
  justify-content: center;
  padding: 0.3rem 0.75rem;
  border-radius: 9999px;
  border: 1px dashed var(--color-primary);
  background: transparent;
  color: var(--color-primary);
  font-size: 0.75rem;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 0.12s ease, border-style 0.12s ease;
}
.template-editor__add-status:hover {
  background: color-mix(in srgb, var(--color-primary) 8%, transparent);
  border-style: solid;
}
.template-editor__add-status:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

/* Phase K polish — badge variants for the 8 new status colors so
   they render correctly anywhere `'badge-' + s.color` is composed.
   `badge-soft` modifier maps to a lighter tinted background. */
.badge-rose    { background-color: #f43f5e; color: #fff; border-color: #f43f5e; }
.badge-pink    { background-color: #ec4899; color: #fff; border-color: #ec4899; }
.badge-purple  { background-color: #a855f7; color: #fff; border-color: #a855f7; }
.badge-indigo  { background-color: #6366f1; color: #fff; border-color: #6366f1; }
.badge-sky     { background-color: #0ea5e9; color: #fff; border-color: #0ea5e9; }
.badge-teal    { background-color: #14b8a6; color: #fff; border-color: #14b8a6; }
.badge-lime    { background-color: #84cc16; color: #1a2e05; border-color: #84cc16; }
.badge-amber   { background-color: #f59e0b; color: #2e1a05; border-color: #f59e0b; }
.badge-soft.badge-rose,   .badge-rose.badge-soft   { background-color: #fff1f3; color: #be123c; border-color: #fecdd3; }
.badge-soft.badge-pink,   .badge-pink.badge-soft   { background-color: #fdf2f8; color: #be185d; border-color: #fbcfe8; }
.badge-soft.badge-purple, .badge-purple.badge-soft { background-color: #faf5ff; color: #7e22ce; border-color: #e9d5ff; }
.badge-soft.badge-indigo, .badge-indigo.badge-soft { background-color: #eef2ff; color: #4338ca; border-color: #c7d2fe; }
.badge-soft.badge-sky,    .badge-sky.badge-soft    { background-color: #f0f9ff; color: #0369a1; border-color: #bae6fd; }
.badge-soft.badge-teal,   .badge-teal.badge-soft   { background-color: #f0fdfa; color: #0f766e; border-color: #99f6e4; }
.badge-soft.badge-lime,   .badge-lime.badge-soft   { background-color: #f7fee7; color: #4d7c0f; border-color: #d9f99d; }
.badge-soft.badge-amber,  .badge-amber.badge-soft  { background-color: #fffbeb; color: #b45309; border-color: #fde68a; }

/* Phase K.2 — drag affordance for the status sidebar sub-tree.
   Hint a drop cursor on hover; the row itself remains clickable. */
.template-editor__statuses-list li[draggable="true"]:hover .cursor-grab {
  opacity: 0.7;
}
/* Phase E/G overflow fix — defensive containment at every layer of
   the sidebar tree, so a long row title can never escape the 280px
   sidebar column. Hand-written (unlayered) CSS beats DaisyUI 5's
   layered rules and is immune to Tailwind v4 JIT timing. */

/* Layer 1 — sidebar column. Last-resort backstop: even if every inner
   rule fails, content is clipped at the column boundary. */
.template-editor__sidebar { overflow-x: hidden; }

/* Layer 2 — sidebar menu + every top-level item, details, and summary
   in the chain leading to the sub-tree rows. Without min-width:0 on
   each link of this chain, an inner row's intrinsic width propagates
   upward through the un-constrained <li>/<details>/<summary> ancestors
   and the tree grows past the sidebar's 280px column even though the
   menu itself is pinned to 100%. `overflow: hidden` at each layer is
   the unconditional kill switch — any descendant that would visually
   overflow gets clipped at the layer's edge. */
.template-editor__sidebar-nav.menu {
  width: 100%;
  max-width: 100%;
  min-width: 0;
  overflow: hidden;
}
.template-editor__sidebar-nav.menu > li,
.template-editor__sidebar-nav.menu > li > details,
.template-editor__sidebar-nav.menu > li > details > summary {
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
}

/* Layer 3 — sub-tree list + row li. Same containment so the row's
   content cannot push its ancestors wider. */
.template-editor__statuses-list,
.template-editor__statuses-list > li {
  max-width: 100%;
  min-width: 0;
  overflow: hidden;
}

/* Layer 4 — row button. DaisyUI 5's `.menu li > *` applies
   `display: grid` with a `max-content` middle track that prevents
   the title span from ever truncating. Force flex so the title's
   `flex-1 min-w-0` works and the `<bdi class="truncate">` ellipsis-
   cuts the text. */
.template-editor__statuses-list > li > button {
  display: flex !important;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  width: 100%;
  padding-block: 0.375rem;
  padding-inline: 0.75rem;
  box-sizing: border-box;
}
/* Layer 5 — text containment. The title text container is the <span>
   child of the button that isn't the drag handle or a chip; force it
   to flex-grow with min-width:0 + overflow:hidden so it can never
   exceed the row's free space. The text itself lives in a <bdi> —
   block + truncate so long titles ellipsis at the row edge. */
.template-editor__statuses-list > li > button
  > span:not([aria-hidden]):not(.badge):not(.shrink-0) {
  flex: 1 1 0%;
  min-width: 0;
  overflow: hidden;
}
.template-editor__statuses-list bdi {
  display: block;
  max-width: 100%;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding-inline-end: 0.25rem;
  box-sizing: border-box;
}
.template-editor__main {
  background: var(--color-base-100);
  border: 1px solid var(--color-base-300);
  border-radius: var(--radius-box, 1rem);
  padding: 1rem;
  min-width: 0;
}
.template-editor__preview {
  background: var(--color-base-100);
  border: 1px solid var(--color-base-300);
  border-radius: var(--radius-box, 1rem);
  min-width: 0;
}
@media (max-width: 639px) {
  .template-editor__preview { display: none; }
}

/* Phase J.3 — Workflow preview SVG (status nodes + dashed lock edges). */
.workflow-preview { min-width: 0; }
.workflow-preview__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding-block: 0.5rem;
  padding-inline: 0.75rem;
  border-block-end: 1px solid var(--color-base-300);
}
.workflow-preview__body { min-width: 0; padding: 0.5rem; }
.workflow-preview__scroll {
  overflow-x: auto;
  overflow-y: hidden;
  min-width: 0;
}
.workflow-preview__svg {
  display: block;
  width: 100%;
  min-width: 640px;
  height: 130px;
}
.workflow-preview__node:hover rect { filter: brightness(1.08); }
.workflow-preview__node:focus { outline: none; }
.workflow-preview__node:focus rect { filter: brightness(1.15); }

/* Picker popovers on mobile (≤ 640px) — switch from the desktop's
   trigger-anchored model to a fixed bottom-sheet so they always fit
   inside the viewport regardless of where the trigger is on the page.
   Avoids overflow when the trigger sits near the screen edge (e.g.
   a subtask-row "Assignees" chip or an admin "Add members" button). */
@media (max-width: 640px) {
  .picker-popover {
    position: fixed !important;
    inset-inline-start: 0.5rem !important;
    inset-inline-end: 0.5rem !important;
    inset-block-end: 0.5rem !important;
    top: auto !important;
    width: auto !important;
    max-width: none !important;
    max-height: 70vh !important;
  }
}

/* ---------- Sidebar tree context menu ----------
   Kebab opener: invisible until row hover (focus-visible for keyboards);
   faintly visible on touch devices, where hover doesn't exist. Scoped to
   .shell-sidebar so the admin tree (which shares .tree-node/.tree-row)
   is unaffected. */
.shell-sidebar .tree-kebab { opacity: 0; }
.shell-sidebar .tree-row:hover .tree-kebab,
.shell-sidebar .tree-kebab:focus-visible { opacity: 1; }
@media (hover: none) {
  .shell-sidebar .tree-kebab { opacity: 0.55; }
}

/* The menu itself — fixed at the stored cursor/kebab coordinates, kept
   hidden until treeMenuPlace() clamps it into the viewport. Hand-written
   (not Tailwind) so layout-critical positioning never waits on the CDN
   JIT (see "App shell critical CSS" above). */
.tree-context-menu {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 60;
  visibility: hidden;
  min-width: 13rem;
  max-width: min(20rem, calc(100vw - 16px));
  max-height: min(70vh, 30rem);
  overflow-y: auto;
}

/* Deep-link "selected" row on the admin dept tree: an attention pulse that
   settles into a faint persistent tint (cleared by adminTreeFocus on the
   next click elsewhere). Uses the row's level accent for brand consistency. */
.tree-row-focused {
  border-radius: 8px;
  outline: none;
  animation: tree-row-focus-pulse 1.6s ease-out forwards;
}
@keyframes tree-row-focus-pulse {
  0% {
    background-color: color-mix(in oklab, var(--tree-accent) 28%, transparent);
    box-shadow: 0 0 0 2px var(--tree-accent);
  }
  100% {
    background-color: color-mix(in oklab, var(--tree-accent) 10%, transparent);
    box-shadow: 0 0 0 1px color-mix(in oklab, var(--tree-accent) 40%, transparent);
  }
}

/* ── Print — Programs & Events timetable / detail showcase ─────────────
   Strips the app chrome (sidebar/topbar/pollers/overlays) so window.print()
   yields a clean A4 sheet. Tailwind `print:hidden` / `print:block` utilities
   (browser-JIT) handle per-element control; these rules cover what utilities
   can't reach. `.print-only` mirrors a hidden→print-visible helper. */
.print-only { display: none; }
@media print {
  @page { size: A4; margin: 14mm; }
  html, body { background: #fff !important; }
  * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  .shell-sidebar, .shell-main > header, #modal-host, #flash, #drawer-host,
  #tree-menu-host, #changes-poller, #notif-sound-poller { display: none !important; }
  .shell-main { padding-inline-start: 0 !important; }
  main { padding: 0 !important; }
  .card { box-shadow: none !important; }
  .print-only { display: block !important; }
  tr, .card, .timeline li, section { break-inside: avoid; }
  a { text-decoration: none !important; color: inherit !important; }
}
