/* ============================================================================
   Waroenk Kito — app.css
   Single hand-written stylesheet for the chat UI (no Tailwind, no build step).

   LAYOUT/INTERACTION model = WhatsApp (chat list -> conversation -> composer,
   in/out bubbles, fixed header + composer). COLORS = Waroenk Kito brand
   (warm food-brand earth tones), NOT WhatsApp green. Palette/scale lifted
   verbatim from the legacy design tokens (_back/app/web/bm/_tokens.py) so the
   look stays on-brand and consistent with prior surfaces.

   Conventions for templates:
   - Participant chat is mobile-first, full-viewport, single-column.
   - Bubbles: .msg.out (mine, right, brand-tinted) / .msg.in (left, white).
   - SOP message kinds get .card variants inside a bubble.
   - Admin surface uses .admin-* (desktop forms; brand chrome, denser).
   - All participant-facing copy is Indonesian; NO numeric badges/scores
     (VISION rule 4) — unread is a dot (.unread-dot), never a count.
   ========================================================================== */

/* ---- Brand tokens (from _tokens.py PALETTE/SCALE) ------------------------ */
:root {
  /* palette — WHITE base canvas, brand brown on ELEMENTS only (owner 2026-06-21:
     "base canvas white, keep identity on other"). The cream is gone from the
     canvas; brown now lives on the wordmark, icons, accents — not the backdrop. */
  --bg:        #FFFFFF;  /* base canvas (white, like WhatsApp) */
  --panel:     #FFFFFF;  /* cards / rows / incoming bubbles (separated by --line, not a tint) */
  --panel2:    #F2EFEC;  /* faint neutral fill — search input, avatar fallback (NOT cream) */
  --line:      #ECE6E1;  /* hairline borders / dividers (neutral, not tan) */
  --txt:       #1F1410;  /* ink */
  --muted:     #7A6657;  /* secondary text, timestamps */
  --accent:    #6B2E1F;  /* brand primary (rendang brown) */
  --accent-tint: #F3E7E2; /* soft brown wash — active pills, avatar bg (brand as a whisper, not a slab) */
  --on-accent: #FFF8EC;  /* text on accent */
  --good:      #3F8F4A;  /* green (accepted / success) */
  --warn:      #E48421;  /* amber (pending / soft-reject) */
  --bad:       #C8321E;  /* red (missed / error) */
  --info:      #2D6E9E;  /* blue (needs-review / action-on-you) */
  --crit:      #8A1C12;  /* deep red (late / escalated) */
  --good-bg:   #E8F5EA;
  --warn-bg:   #FFF4E5;
  --bad-bg:    #FFF0EE;
  --info-bg:   #E6F0F7;
  --crit-bg:   #F7E4E1;
  /* outgoing bubble: a warm tint of the brand bg, not WA green */
  --bubble-out: #F0E4D2;
  --bubble-in:  var(--panel);

  /* scale */
  --sz-xs: 11px; --sz-sm: 12px; --sz-md: 14px; --sz-lg: 18px;
  --sz-xl: 24px; --sz-xxl: 28px;
  --pad-xs: 4px; --pad-sm: 8px; --pad-md: 14px; --pad-lg: 20px;
  --gap-sm: 8px; --gap-md: 14px; --r-md: 12px;

  --header-h: 56px;
  --composer-h: 56px;
  --shadow-sm: 0 1px 2px rgba(31, 20, 16, 0.12);
}

* { box-sizing: border-box; }

/* inline SVG icon — tints to currentColor, crisp on every device (replaces
   inconsistent Unicode glyphs). Usage: <svg class="ic"><use href="…#send"/></svg> */
.ic { width: 1.25em; height: 1.25em; display: inline-block; vertical-align: -0.18em;
  fill: none; stroke: currentColor; pointer-events: none; }
.ic.fill { fill: currentColor; stroke: none; }
html, body {
  margin: 0; padding: 0; height: 100%;
  font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: var(--sz-md);
  color: var(--txt);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  /* the PAGE never scrolls — only the inner scroll regions (.chat-list etc) do.
     Without this the body scrolled too, producing a second (fat, gutter)
     scrollbar next to the phone column. */
  overflow: hidden;
}

/* Thin, WhatsApp-style overlay scrollbars on the inner scroll regions — a chunky
   always-visible OS track inside a phone-frame breaks the illusion and steals
   horizontal space. WebKit (Edge/Chrome) + Firefox. */
* { scrollbar-width: thin; scrollbar-color: rgba(107,46,31,.30) transparent; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(107,46,31,.28); border-radius: 999px; }
::-webkit-scrollbar-thumb:hover { background: rgba(107,46,31,.45); }
a { color: var(--accent); text-decoration: none; }
img, video { max-width: 100%; display: block; }
button { font: inherit; cursor: pointer; }

/* ---- App shell (mobile-first, full viewport) ---------------------------- */
.app {
  display: flex; flex-direction: column;
  /* WK is a phone-first, WhatsApp-shaped app: clamp the shell to a MOBILE
     column and centre it. On a desktop the app sits as a phone-width panel
     (a subtle frame marks the off-column area as intentional). Desktop-native
     layout is a deliberate LATER pass — mobile is the ground truth for now. */
  height: 100dvh; width: 100%; max-width: 480px; margin: 0 auto;
  background: var(--bg);
  box-shadow: 0 0 0 1px var(--line);  /* hairline frame against the page gutter */
  position: relative;  /* anchor for in-room overlays (search, drawer) */
  overflow-x: hidden;  /* a chat never scrolls sideways — clip stray wide children */
}
/* the page gutter behind the phone column reads as deliberate, not broken. */
body { background: var(--panel2); }

/* ---- Header (WhatsApp-restraint: WHITE surface, brown as accent only) ----
   WhatsApp's header is ~white with the brand colour as a small accent (wordmark
   + active controls), not a full-bleed brand bar. We follow that: a white
   header keeps every screen light and lets the brown read as IDENTITY, not
   chrome that swallows the UI. (Owner direction 2026-06-21: white base, brown
   brand accent, clean.) */
.app-header {
  min-height: var(--header-h); flex: 0 0 auto;
  display: flex; align-items: center; gap: var(--gap-sm);
  padding: 6px var(--pad-md);
  background: var(--panel); color: var(--txt);
  border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 10;
}
/* the page title is the one brand-coloured moment in the header (like the green
   "WhatsApp" wordmark) — everything else is ink/muted on white. */
.app-header .title { font-size: var(--sz-lg); font-weight: 700; color: var(--accent);
  flex: 1 1 auto; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* chat-list header identity: BRANCH (hero) over name · jabatan (sub). The stack
   takes the flex so the title inside no longer grows the header itself. */
.app-header .head-id { display: flex; flex-direction: column; justify-content: center;
  flex: 1 1 auto; min-width: 0; line-height: 1.15; }
.app-header .head-id .title { flex: 0 0 auto; }
.app-header .head-sub { font-size: var(--sz-sm); font-weight: 500;
  color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.app-header .back { color: var(--txt); font-size: var(--sz-xl); line-height: 1;
  background: none; border: 0; padding: 0 var(--pad-xs); }
.app-header .hdr-btn, .app-header .ham-btn { color: var(--txt); }
.app-header .avatar { width: 36px; height: 36px; border-radius: 50%;
  background: var(--panel2); object-fit: cover; }
.app-header .spacer { flex: 1 1 auto; }
/* Logout is destructive — never an icon a user can mistake for "back". Label it. */
.app-header .logout-btn { display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--sz-md); white-space: nowrap; }
.app-header .logout-btn .logout-label { font-size: var(--sz-sm); }

/* ---- Chat list (WhatsApp home) ----------------------------------------- */
/* chat-list search — WhatsApp-style pill under the header; filters rows live */
[x-cloak] { display: none !important; }
.search-bar { display: flex; align-items: center; gap: var(--gap-sm);
  padding: var(--pad-sm) var(--pad-md); background: var(--bg);
  border-bottom: 1px solid var(--line); }
.search-bar .search-ic { width: 18px; height: 18px; flex: 0 0 auto;
  color: var(--muted); }
.search-input { flex: 1 1 auto; border: none; outline: none; background: var(--panel2);
  border-radius: 999px; padding: var(--pad-xs) var(--pad-sm); font-size: var(--sz-md);
  color: var(--text); min-width: 0; }
.search-input::placeholder { color: var(--muted); }
.search-clear { flex: 0 0 auto; border: none; background: none; cursor: pointer;
  color: var(--muted); font-size: var(--sz-xl); line-height: 1; padding: 0 var(--pad-xs); }

/* Tugas quick-nav: big segmented tap-targets that pre-filter the task list
   (owner 2026-06-21 — buttons over a dropdown for low-education staff). */
/* Tugas quick-nav — WhatsApp's filter-chip language (small OUTLINE pills, the
   active one softly brand-tinted), NOT heavy full-brown buttons. Still big
   enough to tap; just no longer 3× the weight of a list row. */
.seg-nav { display: flex; gap: var(--gap-sm); padding: var(--pad-sm) var(--pad-md);
  background: var(--panel); border-bottom: 1px solid var(--line); }
.seg-btn { position: relative; display: inline-flex; align-items: center;
  justify-content: center; gap: 6px; min-height: 32px; padding: 4px var(--pad-md);
  border-radius: 999px; background: var(--panel); color: var(--muted);
  font-size: var(--sz-sm); font-weight: 600; text-decoration: none;
  border: 1px solid var(--line); }
.seg-btn:active { transform: scale(0.98); }
/* active = SOLID brand fill (Gemini judge 2026-06-21: the soft tint was too low-
   contrast for low-education staff to see which filter is ON). Filled like
   WhatsApp's active green pill, but on a small pill — not the old heavy slab. */
.seg-active { background: var(--accent); color: var(--on-accent);
  border-color: var(--accent); }
.seg-dot { width: 7px; height: 7px; border-radius: 999px; background: var(--bad);
  flex: 0 0 auto; }
.seg-active .seg-dot { background: var(--on-accent); }

.chat-list { flex: 1 1 auto; overflow-y: auto; background: var(--bg); }
.chat-row {
  display: flex; align-items: center; gap: var(--gap-md);
  padding: var(--pad-md); border-bottom: 1px solid var(--line);
  background: var(--panel);
}
.chat-row:active { background: var(--panel2); }
/* avatar: a SOFT brown-tint disk with a brown initial — not a wall of solid
   brown slabs (which made 80 rows read as one heavy block). Light enough that
   the row's text is the focus, brand-tinted enough to feel like ours. */
.chat-row .avatar { width: 48px; height: 48px; border-radius: 50%;
  background: var(--accent-tint); flex: 0 0 auto; object-fit: cover;
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: var(--sz-lg); color: var(--accent); }

/* review entry banner — the discoverable door to the team-review worklist
   (the blind UX audit found the worklist unreachable). Shows only for users
   who are actually a reviewer of at least one room. */
.review-banner { display: flex; align-items: center; gap: var(--gap-sm);
  padding: var(--pad-md); background: var(--info-bg); color: var(--info);
  border-bottom: 1px solid var(--line); font-weight: 600;
  text-decoration: none; }
.review-banner:active { filter: brightness(0.97); }
.review-banner .ic { width: 20px; height: 20px; flex: 0 0 auto; }

/* in-app heads-up banner: a live message in a room you're not viewing. Floats
   over the top of the list (WhatsApp-style), taps through, auto-dismisses. */
.inapp-banner { position: fixed; top: 8px; left: 50%; transform: translateX(-50%);
  z-index: 60; display: flex; align-items: center; gap: var(--gap-sm);
  max-width: min(440px, 94vw); padding: 10px var(--pad-md);
  background: var(--accent); color: #fff; border-radius: 12px;
  box-shadow: 0 4px 16px rgba(0,0,0,.22); text-decoration: none;
  font-size: var(--sz-sm); font-weight: 600; }
.inapp-banner .ic { width: 18px; height: 18px; flex: 0 0 auto; }
.inapp-banner-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.review-banner span { flex: 1 1 auto; }
.review-banner .chev { width: 18px; height: 18px; opacity: .6; }
.app-header .avatar { display: inline-flex; align-items: center;
  justify-content: center; font-weight: 700; color: var(--accent); }
.chat-row .meta { flex: 1 1 auto; min-width: 0; }
/* up to 2 lines so similar task names stay distinguishable (Gemini judge: a
   one-line ellipsis hid which "cek …" task was which). Clamps at 2. */
.chat-row .name { font-weight: 600; font-size: var(--sz-md);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; line-height: 1.25; }
.chat-row .preview { color: var(--muted); font-size: var(--sz-sm);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-row .aside { display: flex; flex-direction: column; align-items: flex-end;
  gap: var(--pad-xs); flex: 0 0 auto; }
.chat-row .time { color: var(--muted); font-size: var(--sz-xs); }
/* employee identifier on a grouped task row (replaces the removed badge): a
   small brand-tinted name so 50 identical task rows under one group are told
   apart. Right-aligned in the aside, above the date. Truncates to keep the row
   to one line. */
.chat-row .row-employee { color: var(--accent); font-size: var(--sz-xs);
  font-weight: 600; max-width: 9rem; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
/* approaching-breach clock (council OPTION C 2026-06-20): reviewer worklist only,
   a quiet amber clock — foresight before the SLA blows. NOT a number/countdown,
   NOT a score (Rule 4); it sits above the status pill like a soft attention cue. */
.chat-row .rv-soon { display: inline-flex; color: var(--warn); }
.chat-row .rv-soon .ic { width: 15px; height: 15px; }
/* RM branch filter (council 2026-06-21): a plain dropdown above the worklist —
   narrow a regional's many-branch roster. NO counts on options (Rule 4). */
.rv-filter { display: flex; align-items: center; gap: var(--gap-sm);
  padding: var(--pad-sm) var(--pad-md); background: var(--bg);
  border-bottom: 1px solid var(--line); }
.rv-filter label { font-size: var(--sz-sm); font-weight: 600; color: var(--muted); }
.rv-filter select { flex: 1 1 auto; min-width: 0; font-size: var(--sz-md);
  padding: 6px 10px; border-radius: var(--r-md); border: 1px solid var(--line);
  background: var(--panel); color: var(--accent); }
/* branch grouping sub-header: a quiet section label, not a row, no count. */
.rv-group-head { padding: var(--pad-xs) var(--pad-md);
  background: var(--panel2); color: var(--muted);
  font-size: var(--sz-xs); font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; position: sticky; top: 0; z-index: 1; }
/* "Perlu aksi" badge: this room is waiting on YOU. A small accent chip (not a
   count — VISION rule 4) so a heavy user can scan 80 rows for the ones that need
   them. Blind-audit r3. */
.chat-row .row-badge { font-size: var(--sz-xs); font-weight: 600; line-height: 1;
  color: #fff; background: var(--accent); border-radius: 999px;
  padding: 3px 8px; white-space: nowrap; }
/* needs-action DOT (replaces the per-row text badge — redundant under the Perlu
   aksi filter): a small brand-brown boolean, like WhatsApp's unread dot. */
.chat-row .row-dot { width: 10px; height: 10px; border-radius: 50%;
  background: var(--accent); display: inline-block; flex: 0 0 auto; }
/* unread = a DOT, never a number (VISION rule 4) */
.unread-dot { width: 10px; height: 10px; border-radius: 50%;
  background: var(--good); display: inline-block; }

/* ---- Tugas grouped-by-task view (peninjauan folded in, owner 2026-06-22) --- */
/* A collapsed task header (Sedang Ditinjau / Butuh Perhatian / Semua). It looks
   like a chat-row so the list reads uniform, but it's a toggle, not a link. The
   body of rooms is hidden until tapped. WhatsApp-restraint: white base, brown as
   accent on the avatar + the chevron rotates open. */
.task-group { border-bottom: 1px solid var(--line); }
.task-group-head { display: flex; align-items: center; gap: var(--pad-md);
  width: 100%; padding: var(--pad-sm) var(--pad-md); background: var(--bg);
  border: 0; text-align: left; cursor: pointer; font: inherit; color: inherit; }
.task-group-head:active { background: var(--panel2); }
.task-group-head .meta { flex: 1 1 auto; min-width: 0; }
.task-group-head .name { font-weight: 600; }
.task-group-head .chev { width: 18px; height: 18px; color: var(--muted);
  flex: 0 0 auto; transition: transform .15s ease; }
.task-group-head .chev.chev-open { transform: rotate(90deg); }
/* group unread COUNT badge — the owner-sanctioned numeric exception to rule 4,
   only on the collapsed header (so the worker can see how many unread rooms hide
   inside without expanding). A brand-brown pill, WhatsApp's unread-count shape. */
.group-unread { min-width: 20px; height: 20px; padding: 0 6px; border-radius: 999px;
  background: var(--accent); color: var(--on-accent); font-size: var(--sz-xs);
  font-weight: 700; line-height: 20px; text-align: center; flex: 0 0 auto; }
/* the room rows inside an expanded group are indented a touch so the hierarchy
   reads (header → its rooms). */
.task-group-body .chat-row { padding-left: calc(var(--pad-md) + 18px); }

/* ---- Host launcher — post-login landing hub ----------------------------- */
/* Role-gated library tiles (Tugas / Obrolan / Grup, + future non-chat libs)
   on the /home page. Each tile = icon + label + optional boolean DOT (never a
   count — VISION rule 4). Tiles sit at the TOP of the column (NOT centred in
   the void — the cold-reader/owner read centred tiles as a broken/empty screen
   and gave up). Horizontally centred, wrapping, scrollable as libraries grow. */
.launcher-grid { display: flex; flex-wrap: wrap; gap: var(--gap-md);
  justify-content: center; align-items: flex-start; align-content: flex-start;
  flex: 1 1 auto; overflow-y: auto;
  padding: var(--pad-lg) var(--pad-md); background: var(--bg); }
.launcher-tile { display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: var(--gap-sm); width: 120px; height: 140px;
  background: var(--panel); border-radius: var(--r-md); box-shadow: var(--shadow-sm);
  text-decoration: none; color: var(--txt); position: relative;
  transition: box-shadow .15s, transform .15s; }
.launcher-tile:active { box-shadow: var(--shadow-sm); transform: scale(.97); }
/* icon sits in a soft brand-tint disk (room for a future library to carry its
   OWN accent so Finance/HR/Warehouse are distinguishable at a glance). */
.launcher-icon { width: 30px; height: 30px; color: var(--accent);
  background: var(--accent-tint); border-radius: 50%;
  padding: 13px; box-sizing: content-box; }
.launcher-label { font-size: var(--sz-md); font-weight: 600; text-align: center;
  color: var(--txt); }
.launcher-dot { position: absolute; top: 8px; right: 8px; width: 9px; height: 9px;
  border-radius: 50%; background: var(--warn); }
/* plain-language attention cue under the label — legibility for low-literacy
   workers who won't parse a 6px dot (first-sight society). A phrase, not a count. */
.launcher-sublabel { font-size: var(--sz-sm); font-weight: 600; text-align: center;
  color: var(--warn); line-height: 1.1; }

/* ---- Owner triage — its own page (/owner/inbox) ------------------------- */
/* Re-homed off the cramped landing (owner: "own page -> LOT of space"). One
   roomy card per escalation; the landing stays a clean chat list. No counts
   (rule 4); triage verbs are view-only. */
/* Hamburger notification dot — a boolean attention cue on the header ☰. */
.ham-btn { position: relative; }
.ham-dot { position: absolute; top: 2px; right: 2px; width: 9px; height: 9px;
  border-radius: 50%; background: var(--warn); border: 2px solid var(--accent); }
/* Owner slide-over drawer + scrim. */
.drawer-scrim { position: fixed; inset: 0; background: rgba(31,20,16,.45); z-index: 40; }
.drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(80vw, 320px);
  background: var(--panel); z-index: 41; box-shadow: var(--shadow-sm);
  transform: translateX(100%); transition: transform .18s ease;
  display: flex; flex-direction: column; }
.drawer.drawer-open { transform: translateX(0); }
.drawer-head { display: flex; align-items: center; justify-content: space-between;
  padding: var(--pad-md); background: var(--panel); color: var(--accent);
  border-bottom: 1px solid var(--line); font-weight: 700; }
.drawer-x { background: none; border: 0; color: var(--txt);
  font-size: var(--sz-xl); line-height: 1; cursor: pointer; }
.drawer-item { display: flex; align-items: center; gap: var(--gap-sm);
  padding: var(--pad-md); color: var(--ink, #2A1B14); border-bottom: 1px solid var(--line); }
.drawer-item .ic { width: 20px; height: 20px; color: var(--accent); }
.drawer-label { flex: 1 1 auto; font-weight: 600; }
.drawer-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--warn); }
/* Aktif | Arsip tabs. */
.inbox-tabs { display: flex; flex: 0 0 auto; border-bottom: 1px solid var(--line);
  background: var(--bg); }
.inbox-tab { flex: 1 1 0; text-align: center; padding: var(--pad-sm);
  font-size: var(--sz-md); font-weight: 600; color: var(--muted);
  border-bottom: 3px solid transparent; }
.inbox-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
/* The page list — roomy cards, full-height scroll (no height cap; it's a page). */
.inbox-page-scroll { flex: 1 1 auto; overflow-y: auto; background: var(--bg); }
.inbox-card { padding: var(--pad-md);
  border-bottom: 1px solid var(--line); background: var(--panel); }
.inbox-card-head { margin-bottom: var(--pad-sm); }
/* page has space → the task title WRAPS instead of truncating. */
.inbox-task { font-weight: 600; font-size: var(--sz-lg); color: var(--ink, #2A1B14);
  line-height: 1.25; }
.inbox-sub { color: var(--muted); font-size: var(--sz-sm); margin-top: 2px; }
.inbox-ack-dot { color: var(--good); font-weight: 700; margin-left: 6px;
  font-size: var(--sz-xs); }
.inbox-remind { margin: 0; display: inline-flex; }
.inbox-actions { display: flex; flex-wrap: wrap; gap: var(--gap-sm);
  margin-top: var(--pad-sm); }
.inbox-btn { font-size: var(--sz-sm); font-weight: 600; line-height: 1;
  padding: 6px 12px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--line); background: var(--panel); color: var(--accent); }
.inbox-btn.primary { background: var(--accent); color: var(--on-accent);
  border-color: var(--accent); }
.inbox-btn.quiet { color: var(--muted); }
.inbox-btn:active { filter: brightness(.95); }
/* 'why escalated' timeline — an inline expandable chain, oldest level first. */
.inbox-tl { margin-top: var(--pad-sm); }
.tl-list { list-style: none; margin: 0; padding: 0 0 0 var(--pad-sm); }
.tl-entry { display: flex; gap: var(--gap-sm); padding: 4px 0; position: relative; }
.tl-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto;
  background: var(--muted); margin-top: 5px; }
.tl-entry.tl-missed .tl-dot { background: var(--warn); }
.tl-who { font-size: var(--sz-sm); font-weight: 600; color: var(--ink, #2A1B14); }
.tl-who .tl-pos { font-weight: 400; color: var(--muted); font-size: var(--sz-xs); }
.tl-what { font-size: var(--sz-xs); color: var(--muted); }
.tl-empty { padding: var(--pad-sm); font-size: var(--sz-sm); }

/* Training (Pelatihan) row states (UX cartridge pass 2026-06-20): unwatched pulls
   the eye as a to-do (accent avatar + accent status); watched recedes (muted
   avatar + calm green done). The icon inherits the avatar's text color. */
.tr-avatar .ic { fill: currentColor; }
/* soft brand-tint disk + brown glyph (not a solid-brown slab — same restraint as
   the chat-list avatars; a column of solid-brown disks read as a heavy wall). */
.chat-row[data-watched="false"] .tr-avatar { background: var(--accent-tint); color: var(--accent); }
.chat-row[data-watched="true"]  .tr-avatar { background: var(--panel2); color: var(--muted); }
.tr-pending { color: var(--accent); font-weight: 600; }
.tr-done    { color: var(--good); }

/* ---- Conversation feed -------------------------------------------------- */
.feed {
  flex: 1 1 auto; overflow-y: auto; overflow-x: hidden;
  display: flex; flex-direction: column; gap: var(--gap-sm);
  padding: var(--pad-md);
  background: var(--bg);
  min-width: 0;  /* flex child must be allowed to shrink below content width */
}
.day-sep { align-self: center; background: var(--panel2); color: var(--muted);
  font-size: var(--sz-xs); padding: 2px var(--pad-sm); border-radius: 8px; }
/* escalation-slice separator (owner 2026-06-22, Claude-Code model-switch
   metaphor): a centered rule-with-text that FRAMES the instance a viewer above
   the worker was pulled into — open banner where they entered, close banner
   (brand-tinted) when it resolved. Full-width, lines flank the text. */
.esc-sep { display: flex; align-items: center; gap: var(--pad-sm);
  width: 100%; margin: var(--gap-sm) 0; padding: 0 var(--pad-sm); }
.esc-sep-line { flex: 1 1 auto; height: 1px; background: var(--line); }
.esc-sep-text { flex: 0 1 auto; text-align: center; font-size: var(--sz-xs);
  font-weight: 600; color: var(--muted); line-height: 1.4; }
.esc-sep-open .esc-sep-text { color: var(--muted); }
.esc-sep-close .esc-sep-text { color: var(--accent); }
/* a soft brand-tint rule for the close banner. Uses a fixed light-brown rather
   than color-mix() — that function needs Chrome 111+, and the low-education
   audience runs older Android Chrome/WebView (the stale-CSS bug 2026-06-22 came
   from that same old-mobile reality); baseline CSS only on participant surfaces. */
.esc-sep-close .esc-sep-line { background: #D8C3BA; }
/* operating-calendar marker under a day separator (T1 step 2) */
.day-mark { align-self: center; font-size: var(--sz-xs); font-weight: 600;
  padding: 2px 10px; border-radius: 8px; }
.day-mark.closed { background: var(--bad-bg); color: var(--bad); }
.day-mark.tagged { background: var(--warn-bg); color: var(--warn); }

/* WhatsApp-style row: avatar gutter (incoming) + the message column. */
.msg-row { display: flex; align-items: flex-end; gap: 6px; max-width: 88%; }
.msg-row.in  { align-self: flex-start; }
.msg-row.out { align-self: flex-end; }
.msg-col { display: flex; flex-direction: column; min-width: 0; }
.msg-avatar { flex: 0 0 28px; width: 28px; height: 28px; border-radius: 50%;
  background: var(--panel2, #efe6da); color: var(--accent); overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--sz-xs); font-weight: 600; align-self: flex-end; }
.msg-avatar img { width: 100%; height: 100%; object-fit: cover; }
.msg-avatar img.failed { display: none; }

.msg {
  padding: var(--pad-sm) var(--pad-md);
  border-radius: 14px;        /* lighter, WhatsApp-ish; was the heavier r-md */
  position: relative; word-wrap: break-word; font-size: var(--sz-md);
}
.msg .body { white-space: pre-wrap; }
.msg .ts { font-size: var(--sz-xs); color: var(--muted);
  float: right; margin: 6px 0 -2px var(--pad-sm); }
.msg-row.in  .msg { background: var(--bubble-in); border-bottom-left-radius: 4px;
  box-shadow: 0 1px 1px rgba(31,20,16,0.06); }
.msg-row.out .msg { background: var(--bubble-out); border-bottom-right-radius: 4px; }
/* sender name+position above the bubble, grouped with the avatar */
.msg-col .sender { font-size: var(--sz-xs); font-weight: 600;
  color: var(--accent); margin: 0 0 2px 4px; display: inline-block; }
.msg-col .sender .sender-pos { font-weight: 400; color: var(--muted); }
.msg img, .msg video { border-radius: 8px; margin-bottom: var(--pad-xs); }

/* read-receipt ticks (out messages) */
.ticks { display: inline-block; margin-left: 4px; color: var(--muted); }
.ticks.read { color: var(--good); }

/* reply quote */
.reply-quote { border-left: 3px solid var(--accent);
  background: rgba(107, 46, 31, 0.06); padding: var(--pad-xs) var(--pad-sm);
  border-radius: 6px; margin-bottom: var(--pad-xs); font-size: var(--sz-sm);
  color: var(--muted); }

/* ---- SOP message-kind cards (inside the feed) --------------------------- */
/* sop_task_instance: the task card the employee receives */
.card { border-radius: 14px; overflow: hidden; }
.card-task { border: 1px solid var(--line); background: var(--panel); }
/* lighter than the old solid-accent bar: a tinted strip with accent text +
   a left accent rule, so the card reads as a chat card, not a heavy panel. */
.card-task .head { background: var(--bg); color: var(--accent);
  padding: var(--pad-xs) var(--pad-md); font-weight: 600; font-size: var(--sz-sm);
  border-bottom: 1px solid var(--line); border-left: 3px solid var(--accent); }
.card-task .instructions { padding: var(--pad-md); white-space: pre-wrap; }
.card-task .deadline { padding: 0 var(--pad-md) var(--pad-md);
  color: var(--muted); font-size: var(--sz-sm); }

/* status chips for sop_accept / sop_soft_reject etc. */
.chip { display: inline-flex; align-items: center; gap: var(--pad-xs);
  padding: 2px var(--pad-sm); border-radius: 999px; font-size: var(--sz-sm);
  font-weight: 600; }
.chip.good { background: var(--good-bg); color: var(--good); }
.chip.warn { background: var(--warn-bg); color: var(--warn); }
.chip.bad  { background: var(--bad-bg);  color: var(--bad); }

/* ── THE STATUS PILL — one canonical vocabulary, used everywhere ──────────
   Drive by data-state; never hand-pick colors per surface. 5 states only
   (small fixed palette reads as a code; >6 colors becomes noise). Each pill
   is glyph + color + text so it survives colorblindness AND low literacy.
   The ::before glyph is injected by CSS so templates only set the state.

     belum     ○  red       — employee hasn't reported
     tinjau    ●  blue      — needs the reviewer's action (on YOU)
     ulang     🔁 amber      — reviewer asked to redo
     diterima  ✓  green      — accepted, done
     terlambat ⏲  deep red   — SLA missed / escalated

   Variants: .pill (filled, default), .pill.dot (glyph+color only, dense rows),
   .pill.lg (bigger, for room headers / Gojek-style prominence). */
.pill { display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 10px; border-radius: 999px; font-size: var(--sz-sm);
  font-weight: 600; line-height: 1.4; white-space: nowrap; }
.pill .ic { width: 1em; height: 1em; }  /* glyph from icons.svg, tints to color */
.pill[data-state="belum"]     { background: var(--bad-bg);  color: var(--bad); }
.pill[data-state="tinjau"]    { background: var(--info-bg); color: var(--info); }
.pill[data-state="ulang"]     { background: var(--warn-bg); color: var(--warn); }
.pill[data-state="diterima"]  { background: var(--good-bg); color: var(--good); }
.pill[data-state="terlambat"] { background: var(--crit-bg); color: var(--crit); }
/* dot variant: glyph + color, text hidden (dense lists; legend needed) */
.pill.dot { padding: 3px; width: 22px; height: 22px; justify-content: center; gap: 0; }
.pill.dot .pill-text { display: none; }
/* large variant: room headers, prominent status (Gojek-driver scale) */
.pill.lg { font-size: var(--sz-md); padding: 5px 14px; }
.pill.lg .ic { width: 1.2em; height: 1.2em; }

/* legend strip — teaches the pill vocabulary once (top of a worklist) */
.legend { flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: 6px;
  padding: var(--pad-sm) var(--pad-md); background: var(--panel);
  border-bottom: 1px solid var(--line); }

/* reviewer drill-down — Task List (tier 1). A task row rolls up its rooms'
   status as count chips (urgent first); the avatar tints to the most-urgent
   state present so the eye lands on the task that needs attention. */
.task-avatar[data-top] { color: var(--muted); }
.task-avatar[data-top="terlambat"] { background: var(--crit-bg); color: var(--crit); }
.task-avatar[data-top="tinjau"]    { background: var(--info-bg); color: var(--info); }
.task-avatar[data-top="ulang"]     { background: var(--warn-bg); color: var(--warn); }
.task-avatar[data-top="belum"]     { background: var(--bad-bg);  color: var(--bad); }
.task-avatar[data-top="diterima"]  { background: var(--good-bg); color: var(--good); }
.task-avatar .ic { width: 22px; height: 22px; }
.rollup { display: inline-flex; align-items: center; gap: 5px; }
.count-chip { min-width: 20px; height: 22px; padding: 0 7px; border-radius: 999px;
  display: inline-flex; align-items: center; gap: 3px; justify-content: center;
  font-size: var(--sz-sm); font-weight: 700; }
.count-chip .ic { width: 12px; height: 12px; flex: 0 0 auto; }
.count-chip[data-state="terlambat"] { background: var(--crit-bg); color: var(--crit); }
.count-chip[data-state="tinjau"]    { background: var(--info-bg); color: var(--info); }
.count-chip[data-state="ulang"]     { background: var(--warn-bg); color: var(--warn); }
.rollup .chev { width: 18px; height: 18px; color: var(--muted); flex: 0 0 auto; }
.all-clear { color: var(--good); font-weight: 600; }

/* system banner (e.g. "Definisi tugas diperbarui.") */
.system-banner { align-self: center; background: var(--warn-bg);
  color: var(--muted); font-size: var(--sz-sm); padding: var(--pad-xs) var(--pad-md);
  border-radius: 8px; text-align: center; max-width: 90%; }

/* ---- Composer (fixed bottom) ------------------------------------------- */
.composer {
  flex: 0 0 auto; min-height: var(--composer-h);
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--gap-sm);
  padding: var(--pad-sm) var(--pad-md);
  background: var(--panel2); border-top: 1px solid var(--line);
}
/* the staged-photo preview is a full-width row ABOVE the input — needs the
   composer to wrap, else its flex:0 0 100% shoves the input row off-screen
   (bug: upload pushed the input field down). order:-1 floats it on top. */
.composer .evidence-preview { order: -1; }
.composer .text {
  flex: 1 1 auto; resize: none; max-height: 120px;
  border: 1px solid var(--line); border-radius: 20px;
  padding: var(--pad-sm) var(--pad-md); background: var(--panel);
  font: inherit; color: var(--txt);
}
.composer .icon-btn {
  width: 44px; height: 44px; border-radius: 50%; border: 0;
  background: var(--accent); color: var(--on-accent);
  font-size: var(--sz-lg); flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
}
.composer .attach { background: transparent; color: var(--accent); }
/* @-mention chip (council OPTION B 2026-06-21): one-tap "Tanya <atasan>" that
   prefixes the chat field and routes the send to /mention (pulls the target in). */
.composer .mention-chip { flex: 0 0 auto; display: inline-flex; align-items: center;
  gap: 4px; height: 36px; padding: 0 12px; border-radius: 18px;
  border: 1px solid var(--accent); background: rgba(123,46,30,.06);
  color: var(--accent); font-size: var(--sz-sm); font-weight: 600;
  white-space: nowrap; cursor: pointer; align-self: flex-end; }
.composer .mention-chip .ic { width: 16px; height: 16px; }
/* WhatsApp-style staged-photo thumbnail — breaks to its own row above the input
   controls (flex 0 0 100%), with a ✕ to drop the photo before sending. */
.evidence-preview { flex: 0 0 100%; position: relative; width: 84px; }
.evidence-preview img { width: 84px; height: 84px; object-fit: cover;
  border-radius: 10px; border: 1px solid var(--line); display: block; }
.evidence-preview-remove { position: absolute; top: -7px; right: -7px;
  width: 22px; height: 22px; border-radius: 50%; border: 0; cursor: pointer;
  background: var(--accent); color: var(--on-accent); font-size: 15px;
  line-height: 1; display: inline-flex; align-items: center; justify-content: center; }
/* reviewer review-actions: accept (green) + redo (amber) */
.composer .accept-btn { background: var(--good); }
.composer .redo-btn   { background: var(--warn); }
/* labelled review buttons (icon + word) so a new reviewer never hesitates which
   is approve vs send-back — blind UX audit r2. */
.composer .labelled-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 0 var(--pad-md); height: 44px; border: 0; border-radius: 22px;
  color: var(--on-accent); font-weight: 700; font-size: var(--sz-md);
  flex: 1 1 0; min-width: 0; cursor: pointer; }
.composer .labelled-btn .ic { width: 20px; height: 20px; flex: 0 0 auto; }
.composer .labelled-btn span { white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; }
.composer.review { gap: var(--gap-sm); }

/* tap-to-accept (👍 reaction style, VISION §4.6) */
.react-accept { background: transparent; border: 0; font-size: var(--sz-xl);
  padding: var(--pad-xs); }

/* ---- Auth / login (OTP) ------------------------------------------------- */
.auth { display: flex; flex-direction: column; align-items: center;
  justify-content: center; height: 100dvh; padding: var(--pad-lg); gap: var(--gap-md);
  background: var(--bg); width: 100%; max-width: 480px; margin: 0 auto; }
.auth .logo { width: 96px; height: 96px; margin-bottom: var(--pad-md); }
.auth .panel { background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: var(--pad-lg); width: 100%; max-width: 360px;
  box-shadow: var(--shadow-sm); }
.auth h1 { font-size: var(--sz-xl); margin: 0 0 var(--pad-md); color: var(--accent); }
.auth input { width: 100%; padding: var(--pad-sm) var(--pad-md);
  border: 1px solid var(--line); border-radius: 8px; font: inherit;
  margin-bottom: var(--pad-md); }
.btn-primary { width: 100%; padding: var(--pad-sm) var(--pad-md); border: 0;
  border-radius: 8px; background: var(--accent); color: var(--on-accent);
  font-weight: 600; font-size: var(--sz-md); }
.btn-primary:active { filter: brightness(0.92); }
.form-error { color: var(--bad); font-size: var(--sz-sm); }

/* friendly browser error page (403/404) — phone-framed, centered */
.error-page { display: flex; align-items: center; justify-content: center;
  height: 100dvh; padding: var(--pad-lg); background: var(--bg); }
.error-card { text-align: center; max-width: 320px; width: 100%;
  display: flex; flex-direction: column; gap: var(--pad-md); align-items: center; }
.error-card .error-emoji { font-size: 48px; line-height: 1; }
.error-card h1 { font-size: var(--sz-xl); margin: 0; color: var(--accent); }
.error-card .btn-primary { width: auto; padding: var(--pad-sm) var(--pad-lg); }

/* ---- Owner admin surface (desktop, forms — the ONLY forms in the app) --- */
.admin { max-width: 1000px; margin: 0 auto; padding: var(--pad-lg); }
.admin h1, .admin h2 { color: var(--accent); }
.admin-card { background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: var(--pad-lg); margin-bottom: var(--pad-lg);
  box-shadow: var(--shadow-sm); }
.admin label { display: block; font-size: var(--sz-sm); color: var(--muted);
  margin: var(--pad-sm) 0 var(--pad-xs); }
.admin input, .admin select, .admin textarea {
  width: 100%; padding: var(--pad-sm); border: 1px solid var(--line);
  border-radius: 6px; font: inherit; }
.admin table { width: 100%; border-collapse: collapse; font-size: var(--sz-sm); }
.admin th, .admin td { text-align: left; padding: var(--pad-sm);
  border-bottom: 1px solid var(--line); }
.admin th { color: var(--muted); font-weight: 600; }

/* ---- Console form surface (the /console shell — .admin-body container) --- */
.admin-card-title { margin: 0 0 var(--pad-sm); color: var(--accent);
  font-size: var(--sz-md); font-weight: 700; }
.admin-flash { padding: var(--pad-sm) var(--pad-md); border-radius: 8px;
  margin-bottom: var(--pad-md); font-size: var(--sz-sm); border: 1px solid; }
.admin-flash.ok  { background: #ECF6EC; border-color: #B6D9B6; color: #2D6A2D; }
.admin-flash.err { background: #FBECEC; border-color: #E2B6B6; color: #9B2C2C; }
.admin-form { display: flex; flex-wrap: wrap; gap: var(--pad-sm) var(--pad-md);
  align-items: flex-end; }
.admin-form label { display: flex; flex-direction: column; gap: 3px;
  font-size: var(--sz-sm); color: var(--muted); flex: 1 1 180px; margin: 0; }
.admin-form label.wide { flex: 1 1 100%; }
.admin-form label.chk { flex: 0 0 auto; flex-direction: row; align-items: center;
  gap: 6px; color: var(--txt); }
.admin-form label.chk input { width: auto; }
.admin-form input, .admin-form select, .admin-form textarea {
  width: 100%; padding: var(--pad-sm); border: 1px solid var(--line);
  border-radius: 6px; font: inherit; background: var(--panel); }
.admin-form .btn-primary { width: auto; flex: 0 0 auto; }
.admin-grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--pad-lg); margin-bottom: var(--pad-lg); }
.admin-grid-2 .admin-card { margin-bottom: 0; }
.admin-body table { width: 100%; border-collapse: collapse; font-size: var(--sz-sm); }
.admin-body th, .admin-body td { text-align: left; padding: var(--pad-sm);
  border-bottom: 1px solid var(--line); }
.admin-body th { color: var(--muted); font-weight: 600; }
.row-actions { display: flex; gap: var(--pad-sm); flex-wrap: wrap; }
.btn-ghost { padding: 4px 10px; border: 1px solid var(--line); border-radius: 6px;
  background: var(--panel); color: var(--accent); font-size: var(--sz-xs);
  font-weight: 600; cursor: pointer; }
.btn-ghost:hover { background: var(--panel2); }
.btn-ghost.danger { color: #9B2C2C; border-color: #E2B6B6; }

/* ledger report (printable HTML; opened in a new tab) */
@media print {
  .app-header, .composer, .admin .actions { display: none; }
  body { background: #fff; }
}

/* ---- Utilities ---------------------------------------------------------- */
.muted { color: var(--muted); }
.center { text-align: center; }
.hidden { display: none !important; }
.spinner { width: 20px; height: 20px; border: 3px solid var(--line);
  border-top-color: var(--accent); border-radius: 50%;
  animation: spin 0.7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* lazy "load older" sentinel — a centered spinner at the top of the feed that
   fetches the previous page of history when scrolled into view (WhatsApp). */
.load-older { display: flex; justify-content: center; align-items: center;
  padding: var(--pad-sm) 0; min-height: 32px; }

/* header icon button (search trigger etc.) */
.hdr-btn { background: none; border: none; color: var(--on-accent); cursor: pointer;
  padding: 4px; display: inline-flex; align-items: center; }
.hdr-btn .ic { width: 22px; height: 22px; }

/* in-room search overlay — slides over the feed; a search bar + results list.
   Tapping a hit navigates to /c/{id}?at={mid} (the feed loads the window around
   the hit and flashes it). */
.search-overlay { position: absolute; inset: 0; z-index: 30; background: var(--bg);
  display: none; flex-direction: column; }
.search-overlay.open { display: flex; }
.search-bar.in-room { background: var(--accent); }
.search-bar.in-room .search-input { background: var(--panel); }
.search-results { flex: 1 1 auto; overflow-y: auto; }
.search-count { padding: var(--pad-sm) var(--pad-md); font-size: var(--sz-sm); }
.search-hint { padding: var(--pad-lg) var(--pad-md); }
.search-hit { display: flex; flex-direction: column; gap: 2px; padding: var(--pad-md);
  border-bottom: 1px solid var(--line); background: var(--panel); text-decoration: none;
  color: var(--text); }
.search-hit:active { background: var(--panel2); }
.search-hit-body { font-size: var(--sz-md); overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
.search-hit-time { font-size: var(--sz-sm); color: var(--muted); }

/* forward destination picker — reuses the chat-list row look; the rows are
   buttons (post-on-tap) not links. */
.fwd-title { color: var(--on-accent); font-weight: 600; padding-left: var(--pad-xs); }
.fwd-row { width: 100%; text-align: left; border: none; cursor: pointer; font: inherit; }

/* edit bar — slides up over the composer with the message text prefilled */
.edit-bar { position: absolute; left: 0; right: 0; bottom: 0; z-index: 25;
  display: none; align-items: center; gap: var(--gap-sm);
  padding: var(--pad-sm) var(--pad-md); background: var(--panel);
  border-top: 2px solid var(--accent); }
.edit-bar.open { display: flex; }
.edit-bar-label { font-size: var(--sz-sm); font-weight: 600; color: var(--accent);
  flex: 0 0 auto; }
.edit-bar .search-input { flex: 1 1 auto; }
.edit-bar .hdr-btn { color: var(--accent); }

/* jumped-to message flash (search result landing) */
.msg-row.flash .msg { animation: msg-flash 1.6s ease-out; }
@keyframes msg-flash {
  0%, 30% { background: var(--info-bg); box-shadow: 0 0 0 3px var(--info-bg); }
  100% { background: inherit; box-shadow: none; }
}

/* ---- PROTOTYPE: reviewer room-to-room navigation ----------------------- */
/* next/prev through the worklist order — each target is a separate 1:1 room */
.navset { display: inline-flex; align-items: center; gap: var(--pad-xs);
  flex: 0 0 auto; }
.navbtn { color: var(--on-accent); font-size: var(--sz-xl); line-height: 1;
  width: 32px; height: 32px; display: inline-flex; align-items: center;
  justify-content: center; border-radius: 50%; }
.navbtn:active { background: rgba(255,255,255,.15); }
.navbtn.disabled { opacity: .3; pointer-events: none; }
.navpos { color: var(--on-accent); font-size: var(--sz-xs); opacity: .85;
  min-width: 30px; text-align: center; }
/* evidence photo placeholder (real media render comes later) */
.photo-stub { background: var(--panel2); border: 1px dashed var(--line);
  border-radius: 8px; padding: 18px var(--pad-md); text-align: center;
  color: var(--muted); margin-bottom: var(--pad-xs); }

/* ---- Responsive: phone-width on desktop too (mobile is the ground truth) -
   WK is phone-first; on a wide screen the app stays a centred MOBILE column in
   a calm field (a deliberate desktop-native layout is a LATER pass, when the
   product is stable). So this no longer WIDENS on desktop — it keeps the same
   480px clamp as the base rule and just adds the framed-pane chrome. */
@media (min-width: 721px) {
  body { background: var(--panel2); }
  .app {
    max-width: 480px; height: 100dvh;
    border-left: 1px solid var(--line); border-right: 1px solid var(--line);
    box-shadow: 0 0 24px rgba(31, 20, 16, 0.08);
  }
  /* tighter tap targets aren't needed with a mouse; give the composer room */
  .composer .text { max-height: 160px; }
}

/* ── owner ledger grid (T1 step 5, owner-only, printable) ───────────── */
.ledger { max-width: 860px; margin: 0 auto; padding: var(--pad-lg); }
.ledger-head h1 { font-size: var(--sz-xl); color: var(--accent); margin: 0 0 4px; }
.ledger-head .sub { color: var(--muted); font-size: var(--sz-sm); margin: 0 0 var(--pad-sm); }
.ledger-head .summary { font-size: var(--sz-md); margin: 0 0 var(--pad-md); }
.ledger .muted { color: var(--muted); }
.ledger .center { text-align: center; }
.ledger table.grid { width: 100%; border-collapse: collapse; font-size: var(--sz-sm); }
.ledger .grid th, .ledger .grid td {
  text-align: right; padding: 6px 10px; border-bottom: 1px solid var(--line); }
.ledger .grid th:first-child, .ledger .grid td:first-child { text-align: left; }
.ledger .grid thead th { background: var(--panel2); color: var(--accent); font-weight: 600; }
.ledger .grid tbody tr.has-miss { background: var(--bad-bg); }
.ledger .grid tbody tr.has-miss td:nth-child(6) { color: var(--bad); font-weight: 600; }
.ledger .grid tfoot td { border-top: 2px solid var(--line); font-weight: 600; }
/* The 7-col grid overflows a phone; let it scroll sideways so Missed/Miss-rate
   stay reachable instead of clipping off-screen (blind-audit r3 P1c). */
.ledger .grid-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.ledger .grid-scroll .grid { min-width: 520px; }
@media print {
  body { background: #fff; }
  .ledger { max-width: none; }
  .ledger .grid-scroll { overflow-x: visible; }
  .ledger .grid-scroll .grid { min-width: 0; }
}

/* ════════════════════════════════════════════════════════════════════════
   UI BUILD 2026-06-04 — SOP cards, contextual composer, admin shell, polish
   ════════════════════════════════════════════════════════════════════════ */

/* ---- extra SOP cards (evidence / training) + reply lines + riders ------- */
.card-task .rider { padding: 0 var(--pad-md) 2px; color: var(--muted);
  font-size: var(--sz-sm); }
.card-task .meta { padding: var(--pad-xs) var(--pad-md) var(--pad-sm);
  display: flex; gap: var(--pad-xs); flex-wrap: wrap; }
.card-evidence { border: 1px solid var(--line); background: var(--panel); }
.card-evidence .head { background: var(--info-bg); color: var(--info);
  padding: var(--pad-sm) var(--pad-md); font-weight: 600; font-size: var(--sz-sm); }
.card-evidence img { display: block; width: 100%; max-height: 280px;
  object-fit: cover; }
.card-evidence .notes { padding: var(--pad-sm) var(--pad-md);
  white-space: pre-wrap; }
.card-training { border: 1px solid var(--warn); background: var(--warn-bg); }
.card-training .head { color: var(--warn); padding: var(--pad-sm) var(--pad-md);
  font-weight: 600; }
.card-training .body { padding: 0 var(--pad-md) var(--pad-sm); color: var(--txt); }
.card-training img { display: block; width: 100%; max-height: 240px;
  object-fit: cover; }

/* tap-to-open photo frame + LOUD load-failure state (blind UX audit r2): a photo
   that doesn't load must be obvious, never a silent broken box a reviewer approves
   past. .media-failed (set by the img onerror) hides the broken img and shows a
   visible retry chip; the whole frame is a link to the full-size media. */
.media-frame { display: block; position: relative; cursor: zoom-in;
  text-decoration: none; }
.media-frame img { display: block; width: 100%; max-height: 280px;
  object-fit: cover; }
.media-retry { display: none; }
.media-frame.media-failed { cursor: pointer; }
.media-frame.media-failed .media-retry { display: flex; align-items: center;
  justify-content: center; gap: 6px; min-height: 96px; padding: var(--pad-md);
  background: var(--bad-bg); color: var(--bad); font-size: var(--sz-sm);
  font-weight: 600; text-align: center; }
/* compact reply lines for accept / soft-reject / training-confirm */
.reply-line { display: inline-flex; align-items: center; gap: 6px;
  font-weight: 600; font-size: var(--sz-sm); }
.reply-line .ic { width: 1.1em; height: 1.1em; }
.reply-line.accept { color: var(--good); }
.reply-line.redo   { color: var(--warn); }

/* ════ WhatsApp bubble affordances (dispatcher: _bubble.html) ════════════ */
/* forwarded tag */
.fwd-tag { display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--sz-xs); color: var(--muted); font-style: italic;
  margin-bottom: 2px; }
.fwd-tag .ic { width: 1em; height: 1em; }
/* reply / quote — a left-bar snippet of the replied-to message */
.reply-quote { display: block; border-left: 3px solid var(--accent);
  background: rgba(0,0,0,.04); border-radius: 6px; padding: 4px 8px;
  margin-bottom: 4px; text-decoration: none; color: inherit; }
.reply-quote .reply-who { display: block; font-size: var(--sz-xs);
  font-weight: 700; color: var(--accent); }
.reply-quote .reply-text { display: block; font-size: var(--sz-sm);
  color: var(--muted); white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; max-width: 220px; }
/* caption under image/video/audio */
.msg .caption { margin-top: 4px; font-size: var(--sz-sm); }
/* video */
.media-video { position: relative; }
.media-video video { display: block; width: 100%; max-height: 320px;
  border-radius: 8px; background: #000; }
.media-video .media-dur, .media-audio .media-dur { font-size: var(--sz-xs);
  color: var(--muted); }
/* audio / voice note */
.media-audio { display: flex; align-items: center; gap: 8px; min-width: 200px; }
.media-audio .ic { width: 22px; height: 22px; color: var(--accent);
  flex: 0 0 auto; }
.media-audio audio { flex: 1 1 auto; height: 36px; }
/* document / file */
.media-doc { display: flex; align-items: center; gap: 10px; min-width: 200px;
  padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px;
  background: var(--panel); text-decoration: none; color: inherit; }
.media-doc .ic { width: 28px; height: 28px; color: var(--accent); flex: 0 0 auto; }
.media-doc .doc-meta { flex: 1 1 auto; min-width: 0; }
.media-doc .doc-name { display: block; font-weight: 600; font-size: var(--sz-sm);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.media-doc .doc-size { font-size: var(--sz-xs); color: var(--muted); }
.media-doc .doc-open { font-size: var(--sz-xs); font-weight: 700;
  color: var(--accent); flex: 0 0 auto; }
/* multi-image album grid */
.album { display: grid; gap: 3px; border-radius: 8px; overflow: hidden;
  max-width: 280px; }
.album.grid-1 { grid-template-columns: 1fr; }
.album.grid-2 { grid-template-columns: 1fr 1fr; }
.album.grid-3 { grid-template-columns: 1fr 1fr; }
.album.grid-3 .album-tile:first-child { grid-column: span 2; }
.album.grid-4 { grid-template-columns: 1fr 1fr; }
.album-tile { position: relative; display: block; aspect-ratio: 1; }
.album-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
.album-more { position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; background: rgba(0,0,0,.5); color: #fff;
  font-size: var(--sz-lg); font-weight: 700; }
/* deleted tombstone */
.msg .body.deleted { display: inline-flex; align-items: center; gap: 6px;
  font-style: italic; color: var(--muted); }
.msg .body.deleted .ic { width: 1.1em; height: 1.1em; }
/* edited tag + footer row */
.msg-foot { display: inline-flex; align-items: baseline; gap: 6px;
  float: right; margin-left: 8px; }
.msg-foot .edited { font-size: var(--sz-xs); color: var(--muted);
  font-style: italic; }
/* reaction pills under the bubble */
.reactions { display: flex; gap: 4px; margin-top: -6px; margin-bottom: 4px;
  padding-left: 6px; }
.msg-row.out .reactions { justify-content: flex-end; padding-right: 6px; }
.reaction { display: inline-flex; align-items: center; gap: 2px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 999px;
  padding: 1px 6px; font-size: var(--sz-sm); box-shadow: 0 1px 2px rgba(0,0,0,.08); }
.reaction .rc { font-size: var(--sz-xs); color: var(--muted); }

/* ════ collapsed task card + Detail button ══════════════════════════════ */
.card-task .instructions.clamp { display: -webkit-box; -webkit-line-clamp: 2;
  -webkit-box-orient: vertical; overflow: hidden; }
/* the worker-owes-a-photo cue under a task card — points at the camera already in
   the composer below (sim fix 2026-06-16; replaced the per-card "Lihat detail"
   pill that read as THE action and crowded out the photo flow). */
.task-photo-cue { padding: var(--pad-xs) var(--pad-md) var(--pad-sm);
  font-size: var(--sz-sm); font-weight: 700; color: var(--accent); }

/* "needs-me": the ONE card the worker must act on now stands out from the
   done/waiting cards so a low-literacy cook isn't lost (sim finding 2026-06-16:
   couldn't tell which card needs action now). A soft accent ring + tint, like a
   WhatsApp unread highlight — clear, not loud. */
.card-cycle.needs-me, .card-task.needs-me {
  border: 2px solid var(--accent); border-left-width: 3px;
  box-shadow: 0 0 0 3px rgba(123,46,30,.10); background: #fffaf6; }
/* evidence card rendered as a tappable button (opens the review drawer) */
.card-evidence.as-button { display: block; width: 100%; text-align: left;
  border: 1px solid var(--line); background: var(--panel); cursor: pointer; }
.card-open-hint { display: block; margin-top: 4px; font-size: var(--sz-xs);
  color: var(--accent); font-weight: 600; }

/* ════ CENTERED task card — THREE panels (owner mockup 2026-06-21) ════════
   The SOP task is a SYSTEM artifact (sender None/sop), so it's CENTERED, not
   aligned out/in. Owner mockup: LEFT metadata block · CENTER narrow summary ·
   RIGHT action panel. The center is narrow (not a data dump — the meta lives
   LEFT), which is why three panels fit on a phone. */
.cyc-center { display: flex; justify-content: center; width: 100%; align-self: center; }
/* SINGLE-COLUMN STACK (re-architected 2026-06-21 after the Gemini judge flagged
   the old 3-column grid: a 32% title column squished the task name to one word
   per line). The card is now a vertical stack — header, metadata lines, content,
   then full-width action buttons — like a WhatsApp message card. The CSS finally
   matches the template's `.stack-layout` intent. */
.task-card { display: flex; flex-direction: column; gap: var(--gap-sm);
  width: 100%; max-width: 100%; padding: var(--pad-md);
  border: 1px solid var(--line); border-radius: 12px;
  background: var(--panel); }
.task-card.actionable { border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(123,46,30,.10); }
/* rows just stack; no grid columns */
.task-card .tc-row { min-width: 0; }
/* header: pin emoji + full-width title on ONE line (ellipsis if truly long) */
.tc-header { display: flex; align-items: center; gap: 6px; }
.tc-title-wrap { display: flex; align-items: center; gap: 6px; min-width: 0; flex: 1 1 auto; }
.tc-title { font-weight: 700; color: var(--accent); font-size: var(--sz-md);
  min-width: 0; line-height: 1.25;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; }
.tc-id-badge { font-size: var(--sz-xs); color: var(--muted); flex: 0 0 auto; }
/* metadata: label + value on one line each, wrapping naturally */
.tc-details { display: flex; flex-direction: column; gap: 2px; }
.tc-meta-line { display: flex; gap: 6px; font-size: var(--sz-sm); line-height: 1.4;
  flex-wrap: wrap; }
.tc-meta-line .tc-label { color: var(--muted); flex: 0 0 auto; }
.tc-meta-line .tc-val { color: var(--txt); display: inline-flex; align-items: center; gap: 4px; }
.tc-meta-line .tc-val .ic { width: 14px; height: 14px; opacity: .7; }
.tc-meta-line.late .tc-val { color: var(--bad); font-weight: 700; }
.tc-content { display: flex; flex-direction: column; gap: 6px; }
/* full-width action buttons (WhatsApp-style), stacked or paired */
.tc-btn-group { display: flex; gap: var(--gap-sm); }
.tc-btn-full { flex: 1 1 0; display: inline-flex; align-items: center;
  justify-content: center; gap: 6px; min-height: 42px; padding: var(--pad-sm) var(--pad-md);
  border-radius: var(--r-md); border: 1px solid var(--line); background: var(--panel);
  color: var(--txt); font-size: var(--sz-md); font-weight: 600; cursor: pointer; }
.tc-btn-full .ic { width: 18px; height: 18px; }
.tc-btn-full.primary { background: var(--accent); color: var(--on-accent);
  border-color: var(--accent); }
.tc-btn-full.secondary { background: var(--panel); color: var(--accent);
  border-color: var(--accent); }
.tc-btn-full:active { transform: scale(.98); }
/* a calm full-width state pill when the card isn't actionable */
.tc-state-pill { display: inline-flex; align-items: center; justify-content: center;
  gap: 6px; min-height: 38px; padding: var(--pad-sm); border-radius: var(--r-md);
  background: var(--panel2); color: var(--muted); font-size: var(--sz-sm); font-weight: 600; }
.tc-state-pill .ic { width: 16px; height: 16px; }
.tc-state-pill.ok { background: var(--good-bg); color: var(--good); }
.tc-state-pill.queued { background: var(--warn-bg); color: var(--warn); }
/* the "Lihat catatan tugas" detail affordance — a tap ROW (owner 2026-06-22):
   a coloured vertical rule on the left, the label, and a ">" chevron pinned
   right, so it reads "tap to open", not a web link. */
.cyc-detail-row { display: flex; align-items: center; gap: var(--pad-sm);
  width: 100%; padding: 6px 0; background: none; border: 0; text-align: left;
  cursor: pointer; font: inherit; color: var(--accent); }
.cyc-detail-row:active { opacity: .7; }
.cyc-detail-rule { width: 3px; align-self: stretch; min-height: 18px;
  border-radius: 999px; background: var(--accent); flex: 0 0 auto; }
.cyc-detail-text { flex: 1 1 auto; font-weight: 600; font-size: var(--sz-sm); }
.cyc-detail-chev { width: 16px; height: 16px; color: var(--accent); opacity: .7;
  flex: 0 0 auto; }
/* "Selesai" badge — small, pinned BOTTOM-RIGHT (owner 2026-06-22), not a
   full-width pill. A compact green chip the eye finds in the corner. */
/* "Selesai" badge, pinned bottom-right + small. Right-aligned WITHOUT :has()
   (Chrome 105+) — :has() silently no-ops on the older Android Chrome the staff
   run, which would re-center the badge. `.tc-actions` is flex-column (the button
   branches are full-width, unaffected); the badge aligns itself to the end. */
.tc-actions { display: flex; flex-direction: column; }
.tc-done-badge { display: inline-flex; align-items: center; gap: 4px;
  align-self: flex-end; padding: 2px 10px; border-radius: 999px;
  background: var(--good-bg); color: var(--good); font-size: var(--sz-xs);
  font-weight: 700; line-height: 1.6; }
.tc-done-badge .ic { width: 13px; height: 13px; }
/* compact live-escalation line: who holds the task now. Brand-tinted so it
   reads as the live "where is this" signal, not just another meta line. */
.tc-meta-line.tc-esc .tc-val { color: var(--accent); font-weight: 600; }
/* the instance ref title (#id · day) — quieter than the old task-name title */
.tc-title.tc-ref { color: var(--muted); font-weight: 700; }
/* STATUS COLOUR LANGUAGE (2026-06-21): a thin left status bar makes the state
   scannable down a column without reading any text — the existing semantic
   palette (good/warn/bad/info/crit), NOT a score (Rule 4: a colour cue is an
   action signal, like an unread dot, not a performance metric).
     berjalan/pending → neutral (no bar)   submitted → blue (review pending)
     redo             → amber (re-do owed)  accepted  → green (done)
     overdue          → red   (SLA blown). The overdue tint also warms the card. */
.task-card { border-left: 3px solid transparent; }
.task-card.status-submitted { border-left-color: var(--info); }
.task-card.status-redo       { border-left-color: var(--warn); }
.task-card.status-accepted   { border-left-color: var(--good); }
.task-card.status-overdue    { border-left-color: var(--bad); background: var(--bad-bg); }
.task-card.status-overdue .tc-left { background: var(--bad-bg); }
/* LEFT — metadata block, tinted, monospace-ish small print */
.tc-left { background: var(--bg); padding: var(--pad-sm) var(--pad-md);
  font-size: var(--sz-xs); color: var(--muted); line-height: 1.5;
  border-right: 1px solid var(--line); min-width: 0; }
.tc-meta-id { font-weight: 700; color: var(--accent); margin-bottom: 2px; }
.tc-meta { word-break: break-word; }
.tc-meta-task { margin-top: 3px; color: var(--ink); font-weight: 600; }
/* CENTER — narrow live summary */
.tc-mid { padding: var(--pad-sm) var(--pad-md); min-width: 0; }
.tc-head { display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 6px; }
.tc-title { font-weight: 700; color: var(--accent); font-size: var(--sz-sm); }
.tc-due { display: inline-flex; align-items: center; gap: 3px;
  font-size: var(--sz-xs); color: var(--muted); white-space: nowrap; }
.tc-due .ic { width: 13px; height: 13px; opacity: .8; }
.tc-due.late { color: var(--bad, #c0392b); font-weight: 700; }
.tc-statusrow { display: flex; align-items: center; flex-wrap: wrap;
  gap: 6px 8px; margin-bottom: 6px; }
.tc-statusrow .cyc-detail-link { white-space: nowrap; padding: 0; }
/* RIGHT — action panel: real buttons that drive the composer (owner mockup) */
.tc-right { display: flex; flex-direction: column; align-items: stretch;
  justify-content: center; gap: 6px; min-width: 0;
  border-left: 1px solid var(--line); background: var(--bg); padding: var(--pad-sm) 8px; }
.tc-btn { display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 8px 4px; border: 1px solid var(--line); border-radius: 10px;
  background: var(--panel); color: var(--ink); font-size: 11px; font-weight: 700;
  line-height: 1.1; text-align: center; cursor: pointer; }
/* PRIMARY action is filled + bold so it's unmistakably THE button to press; the
   secondary ("Lapor") is quiet and smaller, so the cook isn't torn "yang mana
   yang bener?" (cold-reader v4 fix 2026-06-21). One obvious tap target. */
.tc-btn.primary { border-color: var(--accent); color: #fff;
  background: var(--accent); }
.tc-btn.secondary { border: none; background: none; color: var(--muted);
  font-size: 10px; font-weight: 600; padding: 4px; }
.tc-btn.secondary .ic { width: 15px; height: 15px; opacity: .8; }
.tc-btn .ic { width: 20px; height: 20px; }
.tc-btn:active { transform: scale(.96); }
/* quiet right-zone state when the card isn't actionable (submitted/done) — keeps
   the fixed 3rd column from reading empty, without inviting a re-submit */
/* the ONE status word per card (cold-reader fix 2026-06-21): the single state
   truth lives here in the right zone — never blank, never a second status word
   elsewhere. A small column: icon over word, calm and legible. */
.tc-rstate { display: inline-flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 3px; line-height: 1.15;
  font-size: var(--sz-xs); font-weight: 600; color: var(--muted); text-align: center; }
.tc-rstate.ok { color: #2e7d32; }
.tc-rstate.ok .ic { width: 20px; height: 20px; }
/* queued: owed-but-not-this-turn (backlog cleared newest-first) — a quiet amber
   "next in line", so an older overdue card reads as still-yours, not dead. */
.tc-rstate.queued { flex-direction: column; gap: 3px; color: var(--warn);
  font-weight: 600; line-height: 1.15; }
.tc-rstate.queued .ic { width: 18px; height: 18px; }

/* ════ legacy bound task-cycle card (assignment + evidence = ONE bubble) ══ */
.cyc-wrap { display: flex; flex-direction: column; align-items: flex-start; }
.cyc-wrap.out { align-items: flex-end; }
.card-cycle { display: block; width: 100%; max-width: 80%; text-align: left;
  border: 1px solid var(--line); border-radius: 10px; overflow: hidden;
  background: var(--panel); cursor: pointer; padding: 0; }
.card-cycle.no-drawer { cursor: default; }
/* status accent runs down the left edge so the whole unit reads as one task */
/* ONE consistent card chrome — status is shown by the pill (words + a small
   colour), NOT by recolouring the whole card edge. The per-status left-border
   read as random green/blue/white and broke visual flow for a low-literacy
   user (sim finding 2026-06-16; WhatsApp keeps uniform bubble chrome). */
.card-cycle { border-left: 3px solid var(--line); }
.card-cycle.needs-me { border-left-color: var(--accent); }

/* ── A. identity line: a "Tugas" label + a real per-bubble "Klik untuk detail".
   Single self-contained bubble (owner sketch 2026-06-16, reasoned): the truncated
   instruction DUMP is gone — full SOP text lives in the detail drawer reached by
   this link, so a half-instruction never reads as the task. */
.cyc-idline { display: flex; align-items: center; justify-content: space-between;
  gap: 8px; padding: var(--pad-sm) var(--pad-md); background: var(--bg); }
.cyc-ref { font-weight: 700; color: var(--accent); font-size: var(--sz-sm); }
/* NOT a web link (cold-reader fix 2026-06-21): a blue underlined "Klik untuk
   detail" read as a scary website link the cook was afraid to tap. Calm dark
   tap-row chrome instead — a faint pill, ink text, no blue, no underline. */
.cyc-detail-link { display: inline-flex; align-items: center; gap: 4px;
  background: var(--bg); border: 1px solid var(--line); cursor: pointer;
  padding: 3px 10px; border-radius: 999px;
  color: var(--muted); font-size: var(--sz-xs); font-weight: 600;
  text-decoration: none; }
.cyc-detail-link:active { transform: scale(.97); }
.cyc-detail-link .ic { width: 1em; height: 1em; }

/* ── B. one-line status: a label + a single clear word. Replaces the instruction
   wall — the cook reads state at a glance, not a cut-off comma list. */
.cyc-statusline { display: flex; align-items: center; gap: 8px;
  padding: var(--pad-sm) var(--pad-md) 0; }
.cyc-state-label { font-size: var(--sz-sm); color: var(--muted); }
.cyc-pill { display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--sz-xs); font-weight: 700; padding: 2px 8px; border-radius: 999px;
  white-space: nowrap; }
.cyc-pill .ic { width: 1em; height: 1em; }
.cyc-pill.wait   { background: var(--bg); color: var(--muted); border: 1px solid var(--line); }
.cyc-pill.review { background: var(--info-bg); color: var(--info); }
.cyc-pill.ok     { background: #e7f3e8; color: #2e7d32; }
.cyc-pill.redo   { background: #fbeede; color: #b25e00; }
.cyc-pill.late   { background: #fdecea; color: #c0392b; }

/* ── C. evidence: photo thumb(s) + a one-line report peek, tappable for review */
.cyc-evidence { display: block; width: 100%; text-align: left;
  margin: var(--pad-sm) 0 0; padding: var(--pad-sm) 0 0;
  border: none; background: none; cursor: pointer; border-top: 1px solid var(--line); }
.cyc-evidence.no-drawer { cursor: default; }
.cyc-thumbs { display: flex; gap: 4px; }
/* cyc-thumb broken-photo marker (UX cartridge pass 2026-06-20): on media failure
   the empty box now reads as an unavailable photo (a muted camera on a faint bad
   tint), not a mystery blank. */
.cyc-thumb-broken { display: none; }
.cyc-thumb.media-failed img { display: none; }
.cyc-thumb.media-failed .cyc-thumb-broken { display: flex; align-items: center;
  justify-content: center; width: 56px; height: 56px; border-radius: 6px;
  background: var(--bad-bg); color: var(--bad); }
.cyc-thumb-broken .ic { width: 22px; height: 22px; opacity: .8; }
.cyc-thumb { display: block; width: 56px; height: 56px; border-radius: 6px;
  overflow: hidden; flex: 0 0 auto; background: var(--bg); }
.cyc-thumb img { width: 100%; height: 100%; object-fit: cover; }
.cyc-note { margin-top: 4px; font-size: var(--sz-sm); color: var(--ink); }
.clamp1 { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical;
  overflow: hidden; }
.cyc-open { display: block; margin-top: 4px;
  font-size: var(--sz-xs); color: var(--accent); font-weight: 600; }
.cyc-evidence.no-drawer .cyc-open { color: var(--muted); }
/* OPTION A read-by-capability (council 2026-06-20): a worker's OWN evidence is a
   read-only confirmation, not a tap-to-review button — so no pointer, no accent
   link colour. It reads as a delivered status (WhatsApp "sent"), not an action. */
div.cyc-evidence { cursor: default; }
.cyc-open.mine { color: var(--muted); font-weight: 500; }

/* a non-evidence CTA line (own pending photo cue, or "waiting for proof") */
.cyc-cta { padding: var(--pad-sm) var(--pad-md) var(--pad-sm);
  font-size: var(--sz-sm); }
.cyc-cta.muted { color: var(--muted); font-size: var(--sz-xs); }
.cyc-cta.cue-photo { font-weight: 700; color: var(--accent); }

.cyc-outcome { margin: var(--pad-xs) var(--pad-md) var(--pad-sm); font-size: var(--sz-sm); }
.cyc-outcome.redo { color: #b25e00; }
/* bottom padding for the whole card since sections no longer carry it */
.card-cycle { padding-bottom: var(--pad-xs); }

/* header title + persistent room-level "Lihat detail" (the task def is constant
   for a room → surfaced once here, not per card; sim fix 2026-06-16). */
.title-stack { display: flex; flex-direction: column; justify-content: center;
  flex: 1 1 auto; min-width: 0; line-height: 1.15; }
/* inside the stack the title no longer grows the header itself */
.title-stack .title { flex: 0 0 auto; }
.room-detail-link { display: inline-flex; align-items: center; gap: 4px;
  align-self: flex-start; margin-top: 1px; padding: 0; border: 0;
  background: transparent; color: rgba(255,255,255,.82); cursor: pointer;
  font-size: var(--sz-xs); font-weight: 500; }
.room-detail-link .ic { width: .9em; height: .9em; }
.room-detail-link:active { color: #fff; }

/* ════ right→left sliding drawer (detail + review) ══════════════════════ */
.drawer-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.35);
  opacity: 0; pointer-events: none; transition: opacity .2s ease; z-index: 40; }
.drawer-backdrop.show { opacity: 1; pointer-events: auto; }
.drawer { position: fixed; top: 0; right: 0; height: 100%; width: min(420px, 92vw);
  background: var(--bg); box-shadow: -4px 0 24px rgba(0,0,0,.18);
  transform: translateX(100%); transition: transform .24s ease; z-index: 50;
  display: flex; flex-direction: column; }
.drawer.open { transform: translateX(0); }
/* white drawer header to match the app's white-base restraint (this later rule
   used to override the earlier .drawer-head with a full-brown bar). */
.drawer-head { display: flex; align-items: center; gap: 10px;
  padding: var(--pad-md); background: var(--panel); color: var(--accent);
  border-bottom: 1px solid var(--line); }
.drawer-close { background: transparent; border: 0; color: var(--txt); cursor: pointer;
  display: inline-flex; padding: 4px; }
.drawer-close .ic { width: 24px; height: 24px; }
.drawer-title { font-weight: 700; font-size: var(--sz-md); }
.drawer-body { flex: 1 1 auto; overflow-y: auto; padding: var(--pad-md); }
.drawer-section { margin-bottom: var(--pad-md); }
.drawer-label { font-size: var(--sz-sm); font-weight: 700; color: var(--accent);
  margin-bottom: 6px; }
.instructions-full { white-space: pre-wrap; line-height: 1.5; }
.review-entry { border: 1px solid var(--line); border-radius: 8px;
  padding: var(--pad-sm); margin-bottom: var(--pad-sm); background: var(--panel); }
.review-entry img { display: block; width: 100%; border-radius: 6px; }
.review-entry-meta { display: flex; justify-content: space-between;
  font-size: var(--sz-xs); color: var(--muted); margin-top: 4px; }
.review-outcome { display: inline-flex; align-items: center; gap: 6px;
  font-weight: 700; margin-top: 6px; }
.review-outcome.accept { color: var(--good); }
.review-outcome.redo { color: var(--warn); }
.drawer-actions { display: flex; flex-direction: column; gap: var(--pad-sm);
  padding: var(--pad-md); border-top: 1px solid var(--line); }
.drawer-actions form { display: flex; gap: 6px; align-items: center; }
.drawer-actions input[type=text] { flex: 1 1 auto; padding: var(--pad-sm);
  border: 1px solid var(--line); border-radius: 6px; font: inherit; }
.btn-accept, .btn-redo { display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 14px; border: 0; border-radius: 8px; font-weight: 700;
  cursor: pointer; white-space: nowrap; }
.btn-accept { background: var(--good); color: #fff; width: 100%;
  justify-content: center; }
.btn-redo { background: var(--warn); color: #fff; }
.btn-accept .ic, .btn-redo .ic { width: 1.1em; height: 1.1em; }

/* ════ message action sheet (react / reply / forward / delete) ══════════ */
.msg-menu-btn { position: absolute; top: 2px; right: 4px; border: 0;
  background: transparent; color: var(--muted); cursor: pointer; opacity: 0;
  font-size: 16px; line-height: 1; padding: 2px 4px; transition: opacity .12s; }
.msg:hover .msg-menu-btn, .msg-menu-btn:focus { opacity: .7; }
.msg { position: relative; }
.sheet-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.3);
  opacity: 0; pointer-events: none; transition: opacity .18s; z-index: 55; }
.sheet-backdrop.show { opacity: 1; pointer-events: auto; }
.action-sheet { position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  background: var(--bg); border-top-left-radius: 16px; border-top-right-radius: 16px;
  box-shadow: 0 -4px 24px rgba(0,0,0,.2); transform: translateY(100%);
  transition: transform .22s ease; padding: var(--pad-sm) 0 var(--pad-md); }
.action-sheet.open { transform: translateY(0); }
.react-row { display: flex; justify-content: space-around; padding: var(--pad-sm) var(--pad-md);
  border-bottom: 1px solid var(--line); }
.react-pick { font-size: 26px; background: transparent; border: 0; cursor: pointer;
  padding: 4px 8px; border-radius: 50%; }
.react-pick:active { background: var(--panel2); }
.sheet-item { display: block; width: 100%; text-align: left; background: transparent;
  border: 0; padding: 14px var(--pad-lg); font: inherit; font-size: var(--sz-md);
  cursor: pointer; }
.sheet-item:active { background: var(--panel2); }
.sheet-item.danger { color: var(--bad); }
.sheet-item.cancel { color: var(--muted); border-top: 1px solid var(--line); }

/* ---- contextual composer variants -------------------------------------- */
/* the bottom bar morphs by obligation; these style each mode's affordances */
.composer .attach { width: 44px; height: 44px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; flex: 0 0 auto; border: 0; background: transparent;
  color: var(--accent); }
.composer .attach:disabled { opacity: .5; }
.composer .attach .ic { width: 22px; height: 22px; }

/* training nudge strip — sits above the evidence composer when a guide is owed
   AND a photo is owed, so it never blocks the camera (blind UX audit fix). */
.training-strip { display: flex; align-items: center; gap: var(--gap-sm);
  padding: var(--pad-sm) var(--pad-md); background: var(--warn-bg);
  border-top: 1px solid var(--line); font-size: var(--sz-sm); color: var(--warn); }
.training-strip span { flex: 1 1 auto; }
.training-strip .btn-ghost { color: var(--warn); border-color: var(--warn); }

/* ---- in-app camera overlay (Guard 1) ----------------------------------- */
.cam-overlay { position: fixed; inset: 0; z-index: 100; background: #000;
  display: flex; flex-direction: column; }
.cam-video { flex: 1 1 auto; min-height: 0; width: 100%; object-fit: cover;
  background: #000; }
.cam-bar { flex: 0 0 auto; display: flex; align-items: center;
  justify-content: space-between; padding: var(--pad-lg);
  padding-bottom: calc(var(--pad-lg) + env(safe-area-inset-bottom, 0px));
  background: #000; }
.cam-shutter { width: 68px; height: 68px; border-radius: 50%;
  border: 4px solid #fff; background: #fff; cursor: pointer; flex: 0 0 auto; }
.cam-shutter:active { transform: scale(.94); }
.cam-shutter:disabled { opacity: .4; }
.cam-cancel { background: none; border: 0; color: #fff; font: inherit;
  font-size: var(--sz-md); cursor: pointer; flex: 0 0 72px; text-align: left; }
.cam-spacer { flex: 0 0 72px; }
.composer.evidence .text, .composer.review .text {
  flex: 1 1 auto; border: 1px solid var(--line); border-radius: 20px;
  padding: var(--pad-sm) var(--pad-md); background: var(--panel); font: inherit; }
.composer .icon-btn:disabled { opacity: .4; }
.composer .icon-btn .ic { width: 22px; height: 22px; }
.composer.training { justify-content: center; }
.btn-primary.wide { display: inline-flex; align-items: center; gap: 8px;
  justify-content: center; width: auto; padding: var(--pad-sm) var(--pad-lg); }
.btn-primary.wide .ic { width: 20px; height: 20px; }
.composer-hint { flex: 0 0 100%; text-align: center; color: var(--warn);
  font-size: var(--sz-xs); padding-top: 4px; }
/* one-tap reminder — sits above the chat composer when an escalated reviewer is
   waiting, so a low-literacy user nudges with a single tap (VISION Rule 3). */
.composer-quick-action { flex: 0 0 100%; display: flex; justify-content: center;
  padding: var(--pad-sm) var(--pad-md) 0; }
[x-cloak] { display: none !important; }

/* ---- reviewer header meta (was inline styles) -------------------------- */
.rv-meta { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column;
  justify-content: center; gap: 1px; }
.rv-title { font-size: var(--sz-md); display: flex; align-items: center; gap: 8px;
  min-width: 0; line-height: 1.2; }
.rv-title-ellip { font-size: var(--sz-md); white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; min-width: 0; flex: 1 1 auto; }
.rv-title .pill.dot { flex: 0 0 auto; }
.rv-sub { color: var(--on-accent); opacity: .85; line-height: 1.2;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ---- header connection dot (live SSE state) ---------------------------- */
.conn-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto;
  margin-right: 4px; transition: background .2s; }
.conn-dot.online  { background: var(--good); }
.conn-dot.offline { background: var(--warn); }
.app-header .ic { width: 22px; height: 22px; }
.back .ic { width: 24px; height: 24px; }
.inline-form { margin: 0; display: inline-flex; }

/* ---- empty states ------------------------------------------------------- */
.empty-feed, .empty-list { padding: 40px 20px; }
.empty-list .small, .small { font-size: var(--sz-sm); }
.empty-list .small { margin-top: 6px; opacity: .8; }

/* ---- login polish (states) --------------------------------------------- */
.auth .divider { display: flex; align-items: center; gap: 8px; color: var(--muted);
  font-size: var(--sz-sm); margin: var(--pad-md) 0; }
.auth .divider::before, .auth .divider::after { content: ""; flex: 1;
  height: 1px; background: var(--line); }
.btn-google { width: 100%; padding: var(--pad-sm) var(--pad-md); border: 1px solid var(--line);
  border-radius: 8px; background: var(--panel); color: var(--txt); font-weight: 600;
  font-size: var(--sz-md); display: inline-flex; align-items: center; justify-content: center;
  gap: 8px; text-decoration: none; }
.btn-primary[aria-busy="true"] { opacity: .7; pointer-events: none; }

/* ---- staging-only dev credentials panel (never in prod) ---------------- */
.dev-creds { max-width: 460px; width: 100%; margin-top: var(--pad-md);
  background: var(--panel); border: 1px dashed var(--warn);
  border-radius: var(--r-md); padding: var(--pad-sm) var(--pad-md); }
.dev-creds summary { cursor: pointer; font-weight: 600; color: var(--warn);
  font-size: var(--sz-sm); }
.dev-creds-note { font-size: var(--sz-xs); color: var(--muted); margin: 6px 0; }
.dev-creds table { width: 100%; border-collapse: collapse; font-size: var(--sz-xs); }
.dev-creds th, .dev-creds td { text-align: left; padding: 4px 6px;
  border-bottom: 1px solid var(--line); }
.dev-creds th { color: var(--muted); font-weight: 600; }
.dev-creds tbody tr { cursor: pointer; }
.dev-creds tbody tr:hover { background: var(--panel2); }
.dev-creds code { font-size: .95em; }

/* ════════════════════════════════════════════════════════════════════════
   OWNER ADMIN SHELL — the ONLY sidebar in the product (desktop, English ok)
   ════════════════════════════════════════════════════════════════════════ */
.admin-shell { display: flex; min-height: 100dvh; background: var(--bg); }
.admin-side { flex: 0 0 232px; background: var(--accent); color: var(--on-accent);
  display: flex; flex-direction: column; }
.admin-brand { display: flex; align-items: center; gap: 10px;
  padding: var(--pad-lg) var(--pad-md); font-weight: 700; font-size: var(--sz-lg);
  border-bottom: 1px solid rgba(255,255,255,.12); }
.admin-logo { width: 28px; height: 28px; border-radius: 6px; }
.admin-nav { display: flex; flex-direction: column; padding: var(--pad-sm) 0; flex: 1 1 auto; }
.admin-nav-item { display: block; padding: var(--pad-sm) var(--pad-lg);
  color: var(--on-accent); text-decoration: none; font-size: var(--sz-md);
  border: 0; background: transparent; text-align: left; width: 100%; cursor: pointer;
  font: inherit; opacity: .85; }
.admin-nav-item:hover { opacity: 1; background: rgba(255,255,255,.08); }
.admin-nav-item.active { opacity: 1; background: rgba(255,255,255,.16);
  font-weight: 600; box-shadow: inset 3px 0 0 var(--on-accent); }
.admin-nav-item.linkish { opacity: .85; }
.admin-side-foot { padding: var(--pad-sm) 0 var(--pad-md);
  border-top: 1px solid rgba(255,255,255,.12); }
.admin-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.admin-top { padding: var(--pad-md) var(--pad-lg); border-bottom: 1px solid var(--line);
  background: var(--panel); }
.admin-top h1 { margin: 0; color: var(--accent); font-size: var(--sz-xl); }
.admin-body { padding: var(--pad-lg); overflow: auto; }
.admin-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: var(--pad-md); margin: var(--pad-md) 0; }
.admin-card.tile { display: flex; align-items: center; justify-content: center;
  min-height: 72px; text-decoration: none; color: var(--accent); transition: box-shadow .15s; }
.admin-card.tile:hover { box-shadow: 0 2px 10px rgba(31,20,16,.12); }
.admin-body code { background: var(--panel2); padding: 1px 5px; border-radius: 4px;
  font-size: .92em; }
.api-list { margin: var(--pad-sm) 0; padding-left: var(--pad-lg); line-height: 1.9; }
.ledger-pick { display: flex; gap: var(--pad-md); align-items: flex-end; flex-wrap: wrap; }
/* min-width:0 lets the flex label shrink below its select's longest option so a
   long "Jenis tugas" value ellipsises instead of overflowing the card @360
   (cosmetic society L4). */
.ledger-pick label { display: flex; flex-direction: column; font-size: var(--sz-sm);
  color: var(--muted); gap: 4px; min-width: 0; max-width: 100%; }
.ledger-pick input, .ledger-pick select { padding: var(--pad-sm); border: 1px solid var(--line);
  border-radius: 6px; font: inherit; width: 100%; min-width: 0; max-width: 100%;
  text-overflow: ellipsis; }
.ledger-pick .btn-primary { width: auto; }
/* mobile chrome hidden on desktop — sidebar is always shown there */
.admin-mobilebar, .admin-burger, .admin-backdrop { display: none; }

/* ── admin mobile: content-first + hamburger drawer (Lark-style) ──────── */
@media (max-width: 720px) {
  .admin-shell { display: block; min-height: 100dvh; }

  /* slim sticky top bar: ☰  current-section  logo */
  .admin-mobilebar { display: flex; align-items: center; gap: var(--pad-sm);
    position: sticky; top: 0; z-index: 30; height: 52px; padding: 0 var(--pad-md);
    background: var(--accent); color: var(--on-accent); box-shadow: var(--shadow-sm); }
  .admin-mobilebar-title { flex: 1 1 auto; min-width: 0; font-weight: 600;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .admin-mobilebar-logo { width: 30px; height: 30px; border-radius: 50%; }

  /* hamburger (three bars; pure CSS) */
  .admin-burger { display: inline-flex; flex-direction: column; justify-content: center;
    gap: 4px; width: 32px; height: 32px; cursor: pointer; }
  .admin-burger span { display: block; height: 2px; background: var(--on-accent);
    border-radius: 2px; }

  /* content shows immediately; the old top heading bar is redundant now */
  .admin-top { display: none; }
  .admin-body { padding: var(--pad-md); }

  /* the sidebar becomes an off-canvas drawer (slides in from the left) */
  .admin-side { position: fixed; top: 0; left: 0; bottom: 0; z-index: 40;
    width: 78%; max-width: 300px; flex: none;
    transform: translateX(-100%); transition: transform .22s ease;
    overflow-y: auto; }
  .admin-backdrop { display: block; position: fixed; inset: 0; z-index: 35;
    background: rgba(31,20,16,.45); opacity: 0; pointer-events: none;
    transition: opacity .22s ease; }

  /* :checked drives the drawer open + backdrop visible */
  .admin-nav-toggle:checked ~ .admin-shell .admin-side { transform: translateX(0); }
  .admin-nav-toggle:checked ~ .admin-shell .admin-backdrop { opacity: 1; pointer-events: auto; }

  /* bigger tap targets in the drawer (accessibility) */
  .admin-nav-item { padding: var(--pad-md) var(--pad-lg); }

  /* wide data tables scroll horizontally inside their card instead of blowing
     out the page width — every column stays reachable, page stays 360-wide */
  .admin-card { overflow-x: auto; }
  .admin-body table { min-width: 520px; }
  /* forms/filters stack on narrow screens */
  .ledger-pick { flex-direction: column; align-items: stretch; }
  .ledger-pick .btn-primary { width: 100%; }
}

/* ---- Pelatihan (training library) detail ------------------------------- */
.pelatihan-detail { flex: 1 1 auto; overflow-y: auto; background: var(--bg);
  padding: var(--pad-lg) var(--pad-md); display: flex; flex-direction: column;
  gap: var(--pad-md); }
.pd-title { margin: 0; color: var(--accent); font-size: var(--sz-lg); }
.pd-desc { margin: 0; color: var(--muted); }
.pd-media { width: 100%; }
.pd-video, .pd-img { width: 100%; border-radius: var(--r-md); background: #000; }
.pd-done { text-align: center; color: var(--ok, #2e7d32); font-weight: 600;
  padding: var(--pad-md); }

/* ---- member profile / contact card ------------------------------------ */
.profile { flex: 1 1 auto; overflow-y: auto; background: var(--bg);
  display: flex; flex-direction: column; align-items: center;
  padding: var(--pad-lg) var(--pad-md); gap: var(--pad-sm); }
/* UX cartridge pass 2026-06-20: the change-photo control used to be a dark strip
   pinned INSIDE the circular avatar (inset:auto 0 0 0) and got clipped by the
   circle's border-radius+overflow:hidden → "Ganti foto pro…". Moved it BELOW the
   circle as a plain accent text button. The image clip stays ON the .avatar-clip
   wrapper; .avatar-edit lives outside it so it isn't cut. */
.profile-avatar { position: relative; width: 120px; height: 120px;
  background: transparent;
  display: inline-flex; align-items: center; justify-content: center;
  margin-bottom: 34px; }
.profile-avatar img,
.profile-avatar .avatar-initial { border-radius: 50%; }
.profile-avatar img { width: 120px; height: 120px; object-fit: cover;
  box-shadow: var(--shadow-sm); }
.profile-avatar img.failed { display: none; }
.profile-avatar .avatar-initial { width: 120px; height: 120px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--panel2, #efe6da); box-shadow: var(--shadow-sm);
  font-size: 48px; font-weight: 600; color: var(--accent); }
.avatar-edit { position: absolute; top: 100%; left: 50%;
  transform: translateX(-50%); margin-top: var(--pad-xs); white-space: nowrap; }
.avatar-edit-btn { display: flex; align-items: center; justify-content: center;
  gap: var(--pad-xs); padding: var(--pad-xs) var(--pad-sm); font-size: var(--sz-sm);
  color: var(--accent); cursor: pointer; background: transparent; font-weight: 500; }
.avatar-edit-btn .ic { width: var(--sz-sm); height: var(--sz-sm); fill: var(--accent); }
.profile-name { font-size: var(--sz-lg); font-weight: 600; color: var(--accent);
  margin-top: var(--pad-xs); }
.profile-fields { width: 100%; max-width: 420px; margin: var(--pad-sm) 0 0;
  display: flex; flex-direction: column; gap: 1px; }
.pf-row { display: flex; flex-direction: column; gap: 2px;
  background: var(--panel); padding: 10px var(--pad-md);
  border-radius: var(--r-md); }
.pf-row dt { font-size: var(--sz-xs); color: var(--muted); }
.pf-row dd { margin: 0; font-size: var(--sz-md); word-break: break-word; }
.pf-link { color: var(--accent); }
.profile-actions { width: 100%; max-width: 420px; margin-top: var(--pad-md);
  display: flex; flex-direction: column; gap: var(--pad-sm); }
.btn-ghost.wide { display: inline-flex; align-items: center; justify-content: center;
  gap: 6px; width: 100%; padding: 12px; border-radius: var(--r-md);
  border: 1px solid var(--accent); color: var(--accent); background: transparent; }
