/* ============================================================
   Mintlify-inspired token system (T1). See
   docs/superpowers/specs/2026-06-15-mintlify-restyle-design.md
   --accent / --accent-ink / --bg are injected per-tenant in
   layout.ts <head> from brandingSync(). Do not hardcode them here.
   ============================================================ */
:root {
  /* ink scale */
  --ink: #0d0d0d;
  --ink-2: #333333;
  --ink-3: #666666;
  --ink-4: #727272;  /* 4.81 on --surface, 4.61 on --surface-2. #888888 was 3.54; #767676 was
                        4.54 on white but only 4.35 on --surface-2, which is where the muted
                        chips actually sit — the old lock only ever checked it against white. */
  /* lines + surfaces */
  --line: rgba(0, 0, 0, 0.05);
  --line-2: rgba(0, 0, 0, 0.08);
  --surface: #ffffff;
  --surface-2: #fafafa;
  /* radii */
  --r-sm: 4px;
  --r-md: 8px;
  --r-card: 16px;
  --r-lg: 24px;
  --r-pill: 9999px;
  /* type scale — the modal rem sizes across the app (rule 28). New code picks a rung;
     off-scale sizes (0.55/0.7/0.72/0.78/0.8/0.875/0.9/0.95rem) are pre-existing drift and stay
     literal until their file is touched — not a mass migration. */
  --fs-2xs:  0.65rem;  /* pill / count-badge micro-text */
  --fs-xs:   0.75rem;  /* small labels, meta lines — most common small size */
  --fs-sm:   0.85rem;  /* secondary body */
  --fs-base: 1rem;
  --fs-lg:   1.2rem;   /* section heads */
  --fs-xl:   1.5rem;   /* page titles */
  /* spacing scale — 4px base. Same rule as the type scale: adopt for generic component
     rhythm; measured layout constants (list-card reflow, safe-area insets) stay literal. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  /* shadows */
  --shadow-card: rgba(0, 0, 0, 0.03) 0 2px 4px;
  --shadow-btn: rgba(0, 0, 0, 0.06) 0 1px 2px;
  /* status colors */
  /* Status hues, LIGHT-mode values. These are deliberately darker than the "obvious"
     Tailwind-500 shades they started as, because their dominant use across ~120 call
     sites is as TEXT on --surface, where #ef4444 measures 3.76 and #22c55e only 2.28.
     Darkening them here fixes every one of those sites without touching a single view.
     The dark block below restores the vivid values — on #141414 the bright shades are
     the readable ones (#22c55e is 8.08 there, #15803d only 3.67), so this is one hue
     with two surface-dependent values, exactly like --accent-text.
     For a SOLID fill carrying white ink, use the --c-*-solid family instead: those must
     stay dark in BOTH modes, which is the one case this split cannot serve. */
  --c-hot: #c81e1e;
  --c-warm: #b45309;
  --c-cold: #1d4ed8;
  --c-closing: #15803d;
  --c-dead: #6b7280;
  --c-invoiced: #6d28d9;
  --c-danger: #c81e1e;
  --c-success: #15803d;
  --c-wa: #25D366; /* WhatsApp brand green — fixed, not tenant-themed */
  /* Solid-fill variants: darkened so WHITE ink clears AA on them. Use these ONLY for a
     filled chip/button that must stay solid (nav count badge, role pills, primary-action
     buttons). Never for text — the vivid --c-* above are the text/tint colours, and they
     stay vivid precisely because they must also read on the dark surface, where these
     darkened values would fail (e.g. --c-cold as text on #141414: 5.01 vivid, 2.75 dark). */
  --c-danger-solid:  #b91c1c;  /* white 6.47 */
  --c-success-solid: #15803d;  /* white 5.02 */
  --c-warm-solid:    #b45309;  /* white 5.02 */
  --c-cold-solid:    #1d4ed8;  /* white 6.70 */
  --c-wa-solid:      #0e7a6d;  /* white 5.22 — darkened WhatsApp green */
  /* Role pill fills (were hardcoded hex inline in .role-pill, violating rule 28). */
  /* Boundary ring for solid fills. Transparent in light — there the fill itself clears
     1.4.11 against the surface (5.02-6.70). On the dark surface the same fills drop to
     2.75-3.0, so they need an explicit edge or the chip dissolves into the background.
     Text contrast is unaffected either way; this is the non-text half of WCAG. */
  --solid-ring: transparent;
  --c-neutral-solid: #5a6270;  /* white 6.09 — role-pill/tag-pill default + muted .btn-pill */
  --c-role-owner:   #6d28d9;  /* white 7.10 */
  --c-role-cs:      #026398;  /* white 5.68 — #0284c7 was 4.10 */
  --c-role-admin:   #08677f;  /* white 5.72 — #0891b2 was 3.68 */
  --c-role-kitchen: #15803d;  /* white 5.02 */
  --c-role-driver:  #b45309;  /* white 5.02 */
  /* Chart series (/performa) — Okabe–Ito, colour-blind safe. One colour per CS, stable across charts. */
  --c-series-1: #0072b2;
  --c-series-2: #c75000;
  --c-series-3: #00875f;
  --c-series-4: #b3569a;
  --c-series-5: #9a6f00;
  --c-series-6: #2f86b8;
  --c-series-7: #6b6b6b;
  --c-series-8: #111111;
  --surface-fixed-light: #ffffff; /* fixed white chip — for artwork with baked-in dark text/light bg (e.g. login wordmark) that must not follow dark mode */
  /* readable accent for text/headings on light surfaces (bright accent is low-contrast
     on white); themed darker in light, full accent in dark (see dark blocks below).
     72% was not dark enough — it measured 3.24:1 on white for the tenant accent (#18E299) and failed
     WCAG AA. 55% gives 5.21:1 with headroom for any tenant accent. Dark mode was measured
     as correct and is unchanged (it remaps to the full accent below). */
  --accent-text: color-mix(in srgb, var(--accent) 55%, #000);

  /* radius/spacing live at Pico's plain :root, so they bridge safely here */
  --pico-border-radius: var(--r-md);
}

/* --- Pico color bridge ---
   Pico defines its color vars under :root:not([data-theme=dark]) etc. = specificity
   (0,2,0), which beats a plain :root. We MUST match that specificity or the tenant
   accent never reaches Pico (buttons stay Pico azure). Accent is identical in light
   and dark; --ink/--surface/--line are themed on :root and resolve per-mode.
   --bg is deliberately NOT bridged to the page background (it is unthemed; letting
   Pico theme the page bg keeps dark mode correct). */
:root:not([data-theme="dark"]),
[data-theme="dark"],
[data-theme="light"] {
  --pico-primary: var(--accent);
  --pico-primary-background: var(--accent);
  --pico-primary-border: var(--accent);
  --pico-primary-underline: var(--accent);
  --pico-primary-focus: var(--accent);
  --pico-primary-inverse: var(--accent-ink);
  --pico-primary-hover: color-mix(in srgb, var(--accent) 85%, #000);
  --pico-primary-hover-background: color-mix(in srgb, var(--accent) 85%, #000);
  --pico-primary-hover-border: color-mix(in srgb, var(--accent) 85%, #000);
  --pico-color: var(--ink);
  --pico-muted-color: var(--ink-3);
  --pico-card-background-color: var(--surface);
  --pico-border-color: var(--line-2);
  --pico-muted-border-color: var(--line);
  --pico-box-shadow: var(--shadow-card);
  /* Pico hardcodes its own form-element (input/select/textarea) colors per
     theme rather than deriving them from --pico-color/--pico-border-color —
     e.g. dark mode's --pico-form-element-background-color is rgb(28,33,43.5),
     a blue-tinted gray, vs our neutral --surface-2 (#1a1a1a). Left unbridged,
     every native input/select reads as a visibly different hue than the rest
     of the (tokenized) UI in dark mode. */
  --pico-form-element-background-color: var(--surface-2);
  --pico-form-element-border-color: var(--line-2);
  --pico-form-element-color: var(--ink);
  --pico-form-element-placeholder-color: var(--ink-4);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ink: #ededed;
    --ink-2: #cccccc;
    --ink-3: #a0a0a0;
    --ink-4: #8a8a8a;  /* 5.29 on --surface dark; #777777 was 4.11 */
    --line: rgba(255, 255, 255, 0.08);
    --line-2: rgba(255, 255, 255, 0.12);
    --surface: #141414;
    --surface-2: #1a1a1a;
    --shadow-card: rgba(0, 0, 0, 0.4) 0 2px 4px;
    --shadow-btn: rgba(0, 0, 0, 0.4) 0 1px 2px;
    --solid-ring: #8a8a8a;  /* 5.29 on --surface dark; see :root */
    /* Vivid again on the dark surface — see the note in :root. */
    --c-hot: #ef4444;
    --c-warm: #f97316;
    --c-cold: #3b82f6;
    --c-closing: #22c55e;
    --c-danger: #ef4444;
    --c-success: #22c55e;
    --c-dead: #9ca3af;
    --c-invoiced: #a78bfa;
    --accent-text: var(--accent);
    --c-series-1: #56b4e9;
    --c-series-2: #f08a3c;
    --c-series-3: #2fbf8f;
    --c-series-4: #d98cc4;
    --c-series-5: #e6b83c;
    --c-series-6: #8fd0f5;
    --c-series-7: #a8a8a8;
    --c-series-8: #ededed;
  }
}

:root[data-theme="dark"] {
  --ink: #ededed;
  --ink-2: #cccccc;
  --ink-3: #a0a0a0;
  --ink-4: #8a8a8a;  /* 5.29 on --surface dark; #777777 was 4.11 */
  --line: rgba(255, 255, 255, 0.08);
  --line-2: rgba(255, 255, 255, 0.12);
  --surface: #141414;
  --surface-2: #1a1a1a;
  --shadow-card: rgba(0, 0, 0, 0.4) 0 2px 4px;
  --shadow-btn: rgba(0, 0, 0, 0.4) 0 1px 2px;
  --solid-ring: #8a8a8a;  /* 5.29 on --surface dark; see :root */
  /* Vivid again on the dark surface — see the note in :root. */
  --c-hot: #ef4444;
  --c-warm: #f97316;
  --c-cold: #3b82f6;
  --c-closing: #22c55e;
  --c-danger: #ef4444;
  --c-success: #22c55e;
  --c-dead: #9ca3af;
  --c-invoiced: #a78bfa;
  --accent-text: var(--accent);
  --c-series-1: #56b4e9;
  --c-series-2: #f08a3c;
  --c-series-3: #2fbf8f;
  --c-series-4: #d98cc4;
  --c-series-5: #e6b83c;
  --c-series-6: #8fd0f5;
  --c-series-7: #a8a8a8;
  --c-series-8: #ededed;
}

/* Mobile table scroll */
.table-wrapper { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* Compact icon button for dense table-row action cells (leads Aksi, etc).
   These were inline-styled with no class, so they inherited Pico's full button
   padding (12px 16px) and measured 52x48px each — three of them (a Closing lead
   with the ⭐ reviewed action) came to a 160px cluster. 34px keeps three inside a
   ~108px lane.
   Keeps a full 44px target by default; the density tradeoff below is scoped to narrow
   viewports, where the cluster genuinely cannot fit beside the meta line. Above the
   breakpoint there is space, so there is nothing to trade. */
.row-action {
  background: none; border: none; cursor: pointer;
  padding: 0; margin: 0; min-height: 44px; min-width: 44px;
  font-size: 0.95rem; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
}
.row-action--danger { color: var(--c-danger); }

/* Desktop row controls stay out of the touch DOM layout. The separate cell does not use
   .c-actions, so its hidden markup cannot reserve a mobile card's action lane through :has(). */
.list-card .desktop-row-action-cell, .desktop-row-actions, .desktop-row-delete,
.desktop-action-hint, .desktop-action-label { display: none; }

/* One pill geometry for every control on a test-food row, at EVERY width. The cabang
   <select> and the two state toggles are different elements that had drifted apart, and
   scoping the fix to the media query made desktop worse rather than better: .btn-pill
   declares no min-height, so the global `button { min-height: 44px }` took over above the
   breakpoint and the trio went 44 / 28 / 44 — more mismatched than the inline styles this
   replaced. Unscoped, so mobile and desktop agree. */
.list-card .c-ctl select,
.list-card .c-ctl .btn-pill {
  /* 12px, not the original 10px: at 10px the closing pill's label sat hard against the
     pill's rounded edge and read as cramped (owner-reported 2026-08-19). Kept on the
     shared selector so the select and the toggle stay one geometry. */
  height: 30px; min-height: 30px; padding: 0 var(--space-3);
  font-size: var(--fs-xs); border-radius: var(--r-pill);
  margin-bottom: 0; box-sizing: border-box;
}
/* min-width, not a fixed width: the test-food table overflows its wrapper on desktop
   (1354px of content in 1248px), and `table-layout: auto` pays for that by shrinking
   whatever can shrink — a <select> collapses to well under its own longest option, so
   "Tangerang" rendered as "Tang…" (measured 66px client vs 89px scroll, 1280x900).
   max-content is the longest option's own width, so this self-sizes off the branch list
   and the closing states instead of a magic number that drifts when either changes. */
/* 30px right pad: Pico draws its chevron ~28px in from the right edge, so at 22px the
   longest branch ("Tangerang") ran under the arrow on /test-food (2026-09-26). */
.list-card .c-ctl select { padding-right: 30px; min-width: max-content; }

/* ---- Narrow-viewport row reflow (.list-card) ----
   Applied to /leads, /orders, /invoices, /test-food (owner-approved on-device
   2026-07-21). From the UI/UX audit: at 390px these tables measured 897 / 973 / 1111 /
   1181px inside a 358px wrapper — 60-70% of every row, including status controls and
   whole action columns, sat off-screen behind a horizontal scroll nobody performs.

   Deliberately CSS-only: the <table>/<tr>/<td> tags stay exactly as they are, because
   `hx-target="closest tr"` and outerHTML row swaps depend on them throughout. Only the
   *display* changes, so HTMX semantics are untouched. Views opt in by adding the table
   class plus a role class per cell — attributes, never structure.

   CELL VOCABULARY (generic on purpose — the first draft used leads-specific names like
   c-tgl/c-sumber, which did not survive contact with the other three screens):
     .c-primary   line 1 left  — the identifying field (customer name)
     .c-status    line 1 right — the status pill/select
     .c-meta      line 2       — passive supporting fields, muted, ellipsis-truncated
     .c-meta.c-wa               the phone cell; number is already a wa.me link
     .c-meta.c-elastic          the ONE field that absorbs slack so the meta line is
                                exactly one line for every row instead of "usually one".
                                Pick the least identifying field on the screen.
     .c-ctl       line 3+      — interactive cells that need their own room (toggles,
                                inline editors, WA buttons). Test-food needs these.
     .c-actions   pinned right — icon-button cluster, taken out of flow
   A row reserves the action lane only when it actually has .c-actions (:has()). */
.list-card { min-width: 560px; }
.m-brk { display: none; }   /* mobile-only line breaks inside a stacked row (.list-card--tf) */
/* Test-food keterangan trigger (views/test-food/list.ts). The note is CS feedback, so body
   ink — it used to be accent-coloured and read as a link. */
.ket-add, .ket-text { display: block; min-height: 24px; cursor: pointer; font-size: var(--fs-sm); }
/* Clamped: one live note is a whole pasted WA thread. Full text: the edit form. */
.ket-text {
  color: var(--ink-2);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden;
}

@media (max-width: 640px) {
  .list-card { min-width: 0; }
  .list-card thead { display: none; }            /* column headers are meaningless stacked */
  .list-card, .list-card tbody { display: block; }
  .list-card tr {
    position: relative;              /* anchor for the pinned action cluster */
    display: flex; flex-wrap: wrap; align-items: center;
    gap: 2px 5px; padding: 8px 2px;
    border-bottom: 1px solid var(--line);
  }
  /* Reserve the lane only where a cluster is actually pinned into it — /invoices has no
     action cell, and an unconditional 106px there is 106px stolen from the data.
     106px is the default for a list-card that opts into nothing (three buttons). Every
     list with a cluster now opts into --act1 — /leads too, since its 🗑 became a swipe
     (2026-09-26) and left it at most two — a slim lane instead — otherwise ~70px of empty gap sits
     between the data and the single button, which reads as a layout bug. */
  .list-card tr { padding-right: 106px; }
  .list-card--act1 tr { padding-right: 40px; }
  /* ...but --act1 is a claim about the screen, and /orders breaks it: its TRASH tab swaps the
     single 🗑 for ↩ + ✕. The 40px lane then reserved room for one button while two were
     pinned into it, so the cluster started at x=306 with the meta line still running to x=334
     — 28px of literal overlap (measured at 390x844, 2026-08-19; pre-existing, found by
     comparing the three list screens). Widened per ROW rather than per screen, so a list
     mixing both kinds is right row by row and no view has to pass a flag down.
     40 + 34 = 74px, the same arithmetic the 106px default uses for leads' three.
     Scope through .c-actions so hidden desktop controls cannot widen touch rows.
     :has() fallback is safe: a browser without it keeps 40px, i.e. exactly today's bug and
     nothing worse. */
  .list-card--act1 tr:has(.c-actions .row-action + .row-action) { padding-right: 74px; }
  /* Reclaim the lane where there is no cluster (/invoices has no action cell) — stated as
     :not(:has()) rather than :has() so the fallback is safe: a browser without :has()
     support keeps an unused lane (cosmetic) instead of dropping the reserve and rendering
     every meta cell UNDERNEATH the pinned buttons (measured: 150 overlaps). */
  .list-card tr:not(:has(.c-actions)) { padding-right: 2px; }
  .list-card td { display: block; border: none; padding: 0; }

  /* Line-1 budget, at 390px viewport: 358 wrapper - 2 left pad - lane. Basis must satisfy
     primary + gap + status <= budget  (or the status pill itself wraps, which pushed rows
     to 125px), while leaving < one date-width (~38px) of slack (or the date flows up onto
     line 1 and the pill overlaps it). Re-checked 2026-09-26 when /leads moved to --act1:
       40px lane: 316 budget, 55% = 174; 174 + 5 + 104 = 283, slack 33px.
       74px lane: 282 budget, 55% = 155; 155 + 5 + 104 = 264, slack 18px.
     (Leads rows always pin ⚠, so they never hit the 2px no-cluster lane.)
     Both hold, so the break between line 1
     and the meta line is deterministic rather than content-dependent; `grow` then
     reclaims the slack for the name. Status is a fixed width for the same reason —
     a variable-width pill makes the slack unpredictable. */
  .list-card .c-primary { order: 1; flex: 1 1 55%; min-width: 0; font-weight: 500; }
  /* Fixed 104px, not auto. Leads' status is a fixed-width <select>, but orders /
     invoices / test-food use variable-width <span> badges — with `auto` the line-1
     budget stopped holding on those screens and the date floated up beside the badge.
     A fixed lane makes the wrap deterministic everywhere and reads as a column. */
  .list-card .c-status  { order: 2; flex: 0 0 104px; align-self: center; text-align: right; }
  /* Pico puts margin-bottom:1rem on every form element, so a 24px <select> sat at the
     top of a 40px cell (40-24 = exactly that 16px) and read 7px high against the name.
     Zeroing it collapses the cell onto the control. (Not the name div's align-items —
     that is an *inline* style and a stylesheet rule cannot reach it.) */
  .list-card .c-status select,
  .list-card .c-status form { margin-bottom: 0; }

  /* meta line — muted, truncates rather than wrapping the row.
     min-width:0 + ellipsis matters: nowrap alone leaves the flex item at its auto
     min-content width, so a long value punches straight through the reserved action
     lane and gets clipped mid-word by the pinned buttons. */
  .list-card .c-meta {
    order: 3; flex: 0 0 auto;
    font-size: var(--fs-xs); color: var(--ink-3); white-space: nowrap;
    min-width: 0; overflow: hidden; text-overflow: ellipsis;
  }
  /* c-elastic: opt-in, and ONLY on a screen with few enough meta fields that one can
     absorb the slack (leads has 4). On orders (7) and invoices (6) it squeezed the
     elastic field down to "·…" — worse than an honest second meta line. Do not add it
     to a screen without measuring the result. */
  .list-card .c-meta.c-elastic { flex: 1 1 0; min-width: 3.5em; }
  /* .c-id — an opaque machine identifier (invoice UUID). Useful for matching against a
     WhatsApp thread, but 40 characters of it swallowed the whole card, so it is capped
     and truncated. Full value is one tap away on the detail page. */
  .list-card .c-meta.c-id { max-width: 11ch; }
  /* Peserta (orders' nama_aqiqah) is free customer-typed text, unlike every other meta
     field here — no enum/date caps its length. Un-capped it forced a 511px cell inside a
     358px row (order 87f4e473, 60-char name), blowing the table-wrapper's own scroll well
     past the row it belongs to. Same fix as .c-id: without a max-width, nowrap+ellipsis
     never engages because the box just grows to fit the content instead of clipping it. */
  .list-card .c-meta[data-label="Peserta"] { max-width: 220px; }

  /* .c-key / .c-key-end — the fields an operator scans down the list for (orders:
     delivery date + head count). Ordinary meta cells flow in content order, so their
     position shifted from card to card and could not be scanned vertically. These are
     pinned to the front of the meta run, and .c-key-end grows to eat the rest of the
     line so the remaining fields are pushed to the line below — giving every card an
     identical second line. Requires the matching cells to lead the row in DOM order
     (see orders/list.ts, whose <thead> was reordered in lockstep). */
  /* Order bands, deliberately non-adjacent so a later addition does not silently
     collide the way meta(4) and the old keterangan block(4) did — that put the CS name
     inside the control row. key(3) < meta(4) < controls(6); /test-food: .list-card--tf. */
  .list-card .c-meta.c-key { order: 3; color: var(--ink-2); }
  .list-card .c-meta.c-key-end { flex: 1 1 auto; }
  .list-card .c-meta:not(.c-key) { order: 4; }
  /* NB: no attempt to suppress a separator on the first cell of the wrapped-down run — moot
     now that there is no dot separator (see below), but the ordering it warns about still
     applies to spacing: CSS cannot express "first following sibling" for a `~` selector. */

  /* Un-labelled meta cells (no dot separator — see below) rely on the row's own
     `column-gap: 5px` (set on `.list-card tr` above) for spacing between them. */

  /* Inline field labels, mobile only. Stacking a table strips the column headers, which
     is fatal where the value is not self-describing: /invoices showed three bare currency
     figures (total / paid / outstanding) that could not be told apart. The label lives in
     a data-label attribute so DESKTOP keeps its real <th> headers with no duplication.
     No leading "·" here (or on the unlabelled rule above) on purpose — a `::before` dot
     sits INSIDE the cell it precedes, so when flex-wrap breaks the meta run onto a new
     line the dot rides down with it and opens the line ("· 62812…", "· Peserta …"),
     reading as a stray character rather than a separator (2026-09-26 audit). Cell-to-cell
     spacing comes entirely from the row's `column-gap: 5px` instead, which never
     produces a dangling leading glyph because it isn't content. */
  .list-card .c-meta[data-label]::before {
    content: attr(data-label) "\00a0"; color: var(--ink-4); font-weight: 400;
  }

  /* Phone: the number is ALREADY a wa.me link (phoneLink in views/utils.ts), so the
     separate tel: link and copy button are redundant weight on mobile (~40px of a
     250px meta line). Hidden here, still present on desktop and on detail pages. */
  .list-card .c-wa a[href^="tel:"],
  .list-card .c-wa .copy-btn { display: none; }
  .list-card .c-wa > a[href*="wa.me"] {
    display: inline-block; max-width: 96px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    vertical-align: bottom;
    color: color-mix(in srgb, var(--c-wa) 50%, var(--ink)); text-decoration: none; font-weight: 500;
  }
  /* /leads' meta run is only date + phone + source, and its lane is 40/74px since the 🗑
     became a swipe — 316 budget - 38 date - ~95 source ("Meta (FB/IG)") - gaps = ~170px,
     room for a full 15-digit number (~116px). The 96px cap (sized for the old 250 budget and
     for /orders' and /test-food's longer meta runs, which still need it) truncated every
     /leads number with space to spare (owner report, 2026-09-26). */
  #leads-content .list-card .c-wa > a[href*="wa.me"] { max-width: none; }
  /* /leads: the lane is ALWAYS two slots wide. Only closing leads get ⭐ beside the ⚠, so the
     per-row 40→74px widening moved the status pill 34px left on exactly those rows — a
     ragged pill column (owner, 2026-09-26). ⚠ stays rightmost, ⭐ fills the left slot.
     The 74px line-1 budget is the one already checked above. */
  #leads-content .list-card tr:has(.c-actions) { padding-right: 74px; }

  /* /test-food (`.list-card--tf`) — FIXED lines, not flex-wrap luck. The old run let
     content decide where things landed: the CS name wrapped alone onto a 4th line, the
     phone/datang cells ellipsed to "Data…", and "+ catatan" drifted along the controls
     (owner, 2026-09-26: "messier"). Two `.m-brk` cells (display:none on desktop) force the
     breaks, so every row reads the same:
       1  Nama (full width — long names never truncate) ........ CS
       2  Test 27 Sep  Est 03 Okt  62812… ........ Dikirim   (phone absorbs the slack)
       3  [closing ▾] [terkirim] [cabang ▾] ✎
       4  note text — only when there is one, or while editing
     All three pills side by side on line 3 (owner, same day): the closing <select> used to
     sit in the line-1 status lane and crowd the name. Line 3 measured ~72 + 97 + 107 + 3×8
     = 300 of 354px — "+ catatan" (67px) does not fit, so the trigger is ✎ with its word
     visually hidden (still read by a screen reader). */
  .list-card--tf tr { column-gap: 8px; row-gap: 4px; padding: 10px 2px; }
  .list-card--tf .m-brk { display: block; order: 3; flex: 0 0 100%; height: 0; }
  .list-card--tf .m-brk + .m-brk { order: 5; }
  .list-card--tf td:empty:not(.m-brk) { display: none; }
  .list-card--tf .c-meta { order: 4; }
  .list-card--tf .c-wa { flex: 1 1 0; min-width: 5em; }
  .list-card--tf .c-wa > a[href*="wa.me"] { max-width: 100%; }
  .list-card--tf .c-primary { flex: 1 1 0; }
  .list-card--tf .c-meta.c-cs { order: 2; }
  .list-card--tf .c-ctl.c-status { order: 6; flex: 0 0 auto; text-align: left; }
  .list-card--tf .c-ctl { order: 7; flex: 0 0 auto; align-self: center; }
  .list-card--tf .c-ctl.c-cab { order: 8; }
  .list-card--tf .c-ctl.c-ket { order: 9; }
  .list-card--tf .ket-add-label {
    position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
  }
  .list-card--tf .c-ctl.c-ket:has(.ket-text, .is-editing) { order: 10; flex: 1 1 100%; }
  /* The note reads as a quote under the controls, two lines on a phone. */
  .list-card--tf .ket-text {
    min-height: 0; border-left: 2px solid var(--line); padding-left: 8px;
    -webkit-line-clamp: 2; line-clamp: 2;
  }

  .list-card .c-ctl form { margin-bottom: 0; }
  /* Keterangan editor: input on its own line, OK + Batal side by side under it. Pico's
     full-width buttons stacked them into two bars (edge sweep, 2026-09-26). */
  .list-card .c-ket form { display: flex; flex-wrap: wrap; gap: 6px; }
  .list-card .c-ket form input { flex: 1 1 100%; }
  .list-card .c-ket form button { width: auto; flex: 0 0 auto; }
  /* Pinned, NOT in flow. In flow a 3-button cluster forced a third line and a 137px
     row; the lane reserved above keeps anything from running underneath. */
  .list-card .c-actions {
    position: absolute; right: 0; top: 50%; transform: translateY(-50%);
    display: flex; gap: 0; align-items: center;
  }
  /* 34px: below the 44px WCAG 2.5.5 (AAA) guideline, above the 24px 2.5.8 (AA) minimum.
     Narrow viewports only — at 44px a three-button cluster no longer fits beside the
     meta line. Owner-confirmed on device. */
  .list-card .row-action { min-height: 34px; min-width: 34px; }

  /* Fixed width, not min/max — see the basis note above; a predictable pill keeps the
     line-1 wrap deterministic. 104px = widest label ("Tidak Relevan", 83px at 600 12px)
     + 16px padding + slack.
     The padding override is why it fits: Pico ships selects with 40px of RIGHT padding
     to clear a dropdown arrow, but .status-select sets appearance:none so no arrow is
     ever drawn — that dead space truncated "Tidak Relevan" to "Tidak R". Reclaimed here
     rather than widening the pill, which would have cost the name its width. */
  .list-card .c-status .status-select {
    min-width: 104px; max-width: 104px; padding: 2px 8px;
  }
}

/* Touch-friendly buttons */
button, .btn { min-height: 44px; }

/* Standardized small action-button sizes — collapses the ad-hoc 0.75–0.85rem
   inline sizing scattered across views. Color/layout stays inline/Pico. */
.btn-sm { font-size: 0.8rem; padding: 4px 10px; }
.btn-xs { font-size: var(--fs-xs); padding: 2px 8px; }
/* Pill toggle button (active/inactive state buttons that re-implemented Pico):
   size + chrome here, only the dynamic state background stays inline. Keeps the
   global 44px min-height (touch target) — deliberately no min-height override. */
/* State-toggle pill (Aktif/Nonaktif, Terkirim/Belum). The fill used to be set inline per
   call site while the white ink lived here, so no single file ever showed the pair — which
   is how it survived a contrast audit that fixed everything around it. The fill is a
   modifier class now: both halves are in one rule, visible to the eye and to the lock in
   contrast.test.ts. `-solid` tokens because these stay solid under white ink in BOTH modes,
   which is the one case the light/dark --c-* split cannot serve. */
.btn-pill { font-size: var(--fs-xs); padding: 4px 10px; border: none; border-radius: 4px; cursor: pointer;
  box-shadow: 0 0 0 1px var(--solid-ring); }
.btn-pill--danger  { background: var(--c-danger-solid);  color: #ffffff; }
.btn-pill--success { background: var(--c-success-solid); color: #ffffff; }
.btn-pill--cold    { background: var(--c-cold-solid);    color: #ffffff; }

/* Horizontal button row for a detail-page "Aksi" card. The buttons used to be
   string-concatenated with nothing between them, so /orders rendered "Edit Lengkap" flush
   against "🗑 Hapus Order" and the two read as one control.
   The margin reset is not cosmetic tidying — it is what makes the row work: Pico puts
   margin-bottom on <form> AND <button>, which inflated the flex line to 82px while the
   tallest real control measured 62px, so align-items:stretch handed the neighbouring
   anchor 82px and the pair looked more mismatched than before the gap existed. */
/* ── CS order editor (/orders/:id) field scale ──────────────────────────────────────────
   ONE compact scale for every field on the page: 0.8rem/600 label, --fs-sm control, 10px
   row rhythm. These were ~25 inline style strings, and the drift was measurable: Pico fixes
   an <input>'s height from 1rem but lets a <select> follow its font, so a 0.85rem select
   rendered 46px beside a 50px input in the same row; `1fr 1fr` let a date input's
   min-content steal width from its neighbour (173px vs 113px); and a grid declared INLINE on
   an x-show element is lost when Alpine clears the inline display (the Dokumentasi / Kulit
   Kambing pair stacked with a 0px gap). A class survives x-show. */
.ed-label { display: block; margin-bottom: 3px; font-size: 0.8rem; font-weight: 600; }
/* Element-qualified on purpose: Pico's own rule is `input:not([type=checkbox],…)` (0,1,1),
   which outranks a bare class and silently restores its 16px margin and 1rem-based height. */
input.ed-ctl, select.ed-ctl, textarea.ed-ctl,
.ed-items .detail-panel .field-group select,
.ed-items .detail-panel .field-group input:not([type="checkbox"]):not([type="radio"]) {
  width: 100%; margin: 0; font-size: var(--fs-sm);
  height: calc(var(--fs-sm) * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2);
}
textarea.ed-ctl { height: auto; }
/* Same x-show trap: this row's inline display:flex was cleared on show, so the amount sat
   beside the word "Subtotal" instead of at the right edge. */
.ed-subtotal {
  display: flex; justify-content: space-between; padding: 10px 0 2px;
  font-weight: 700; font-size: 0.9rem; border-top: 2px solid var(--pico-muted-border-color);
}
.ed-row2 { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 10px; margin-bottom: 10px; }
/* The shared panels (SELECTOR_CSS) are sized for the public form; inside the editor they
   follow the editor's scale so a slot reads as one form, not two. */
.ed-items .detail-panel label { font-size: var(--fs-sm); }
.ed-items .detail-panel .field-group > label { margin-bottom: 3px; font-size: 0.8rem; }
.ed-items .detail-panel .field-group { margin-bottom: 10px; }
.ed-items .detail-panel { margin-top: 10px; }

/* Layout for an element that ALSO carries x-show. Alpine's show() removes the inline
   `display`, so `style="display:flex"` on such a tag silently becomes block the moment it is
   shown. Put the display in one of these classes instead (locked repo-wide by
   affordances.test.ts). */
.d-flex { display: flex; }
/* One CONSTRAINED column by default: a bare `display:grid` sizes its implicit column to the
   widest child's min-content, so a filter form's selects ran off the right edge. An inline
   grid-template-columns still overrides this. */
.d-grid { display: grid; grid-template-columns: minmax(0, 1fr); }
.d-inline-flex { display: inline-flex; }

/* The Simpan / Batal pair that ends a form: equal halves, equal height. Pico gives a
   <button> a 16px bottom margin and an <a role="button"> none, so in a bare flex row the two
   stretched to different heights (74px vs 90px) and the anchor's label sat at the top. */
.form-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: stretch; }
/* Element-qualified: Pico's `[role=button] { display: inline-block }` ties a bare `> *`. */
.form-actions > a, .form-actions > button {
  flex: 1 1 140px; margin: 0; min-height: 44px;
  display: inline-flex; align-items: center; justify-content: center; text-align: center;
}

/* Invoice detail footer: up to seven actions. A flex row squeezed them to ~80px columns with
   three-line labels (124px tall); two equal columns keep every label on one or two lines. */
.action-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); margin-top: 8px; padding-bottom: 16px; }
.action-grid > form { margin: 0; display: flex; }
.action-grid > a, .action-grid > form > button {
  display: flex; align-items: center; justify-content: center; width: 100%; min-height: 44px; margin: 0;
}
.action-grid > :last-child:nth-child(odd) { grid-column: 1 / -1; }

.action-row { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: stretch; }
.action-row form, .action-row button { margin-bottom: 0; }

/* Full-width action button/anchor in the invoice detail footer row. Was three copies of
   the same inline style; 10px / 0.9rem / 6px are off-scale — no --space-*, --fs-* or --r-*
   rung lands on them (verified against public/style.css:37-42, 29-34, 21-25), so they stay
   literal. Rule 28: adopting a token is only ever a pixel-identical swap, and these are
   pre-existing drift. */
.btn-action {
  width: 100%;
  padding: 10px;
  font-size: 0.9rem;
  text-align: center;
  border-radius: 6px;
  text-decoration: none;
}
.btn-action--danger { background: var(--c-danger-solid); color: #ffffff; border: none; cursor: pointer; }
.btn-pill--neutral { background: var(--c-neutral-solid); color: #ffffff; }

/* Floating "+" add button (leads/invoices/test-food/products list screens) —
   was copy-pasted per screen with drifting right/z-index/shadow-alpha values. */
.fab-add {
  position: fixed; bottom: 80px; right: 16px; width: 52px; height: 52px;
  border-radius: 50%; background: var(--pico-primary); color: var(--accent-ink);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--fs-xl); text-decoration: none; box-shadow: var(--shadow-card); z-index: 100;
}

/* Collapsible filter toggle ("Filter ▼"). Tokenized so text stays readable and
   the chrome flips with light/dark — the old inline --pico-secondary-background +
   --pico-color paired dark-on-dark in light mode and didn't adapt to theme. */
.btn-toggle { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-sm);
  padding: 6px 12px; border-radius: 6px; cursor: pointer;
  background: var(--surface-2); color: var(--ink);
  border: 1px solid color-mix(in srgb, var(--ink) 15%, var(--surface)); }

/* Filter header row, shared by all four list screens — see filterHeader() in
   src/views/shared/filter-select.ts. wrap so the date-range chip drops to its own
   line at 390px instead of pushing the row wide (coding rule 7). */
.filter-header { display: flex; gap: var(--space-2); align-items: center;
  flex-wrap: wrap; margin-bottom: var(--space-2); }
.filter-flag  { font-size: var(--fs-xs); color: var(--accent-text); }
.filter-reset { font-size: var(--fs-xs); color: var(--ink-3); }

/* Date-range chip in the filter header (dateRangeChip()). Reads as a statement of
   scope, not a control — no border, no pointer, so it doesn't compete with the
   adjacent "Filter ▼" button or read as tappable. --ink-2, not --ink-3/-4: it must
   stay legible at 0.75rem since it is the only on-screen notice that the list is
   month-scoped. */
.filter-chip { font-size: var(--fs-xs); color: var(--ink-2); white-space: nowrap; }

/* Segmented view switch (Aktif / 🗑 Sampah), trashViewSwitch(). Layout only — the
   active half's accent fill comes from dropping Pico's `secondary outline`, not from
   a class here (the Pico bridge above already maps primary onto the tenant accent). */
.view-switch { display: flex; gap: var(--space-2); margin-bottom: 10px; align-items: center; }

/* Multi-select dropdown-checklist (replaces the old chip-pill widget — see
   docs/superpowers/specs/2026-07-03-filter-dropdown-checklist-and-orders-filters-design.md).
   Closed control has the same single-line footprint as the <select> it mirrors;
   tapping it reveals a scrollable checklist of real, visible checkboxes (native
   form semantics unchanged) styled via accent-color rather than a hidden-checkbox
   + custom-glyph trick — simpler and just as legible as a checkmark. */
.ms-filter { position: relative; }
.ms-filter-toggle {
  display: flex; align-items: center; justify-content: space-between; gap: 6px;
  width: 100%; font-size: var(--fs-sm); padding: 8px 12px; border-radius: var(--r-sm);
  cursor: pointer; background: var(--surface-2); color: var(--ink);
  border: 1px solid var(--line-2);
  margin: 0;   /* Pico's 16px button margin tripled the gap under every multi-select in a filter panel */
}
.ms-filter-panel {
  margin-top: 6px; border: 1px solid var(--line-2); border-radius: var(--r-sm);
  background: var(--surface); max-height: 220px; overflow-y: auto;
}
.ms-filter-row {
  display: flex; align-items: center; gap: var(--space-2); padding: 10px 12px; min-height: 44px;
  cursor: pointer; font-size: var(--fs-sm); color: var(--ink); margin: 0;
}
.ms-filter-row:hover { background: var(--surface-2); }
.ms-filter-row input[type="checkbox"] {
  accent-color: var(--accent); width: 18px; height: 18px; flex-shrink: 0;
}

/* Long unbroken tokens (imported emails / UTM source_info / run-on addresses)
   must not blow out key/value flex rows on mobile. overflow-wrap:anywhere is
   inherited AND shrinks the flex item's min-content size, so min-width:auto no
   longer forces horizontal overflow — no per-child min-width override needed
   (which would have clamped class-sized children like .status-select).
   ponytail: blanket net over .app-main beats editing ~20 inline row helpers;
   a row that must stay nowrap gives itself a class and overrides there. */
.app-main [style*="space-between"] { overflow-wrap: anywhere; }

/* Status badges */
/* Status badges. Tint fill + ink mixed toward --ink, the recipe already proven by .flash
   (5.26) and .search-result-tag--order (4.57). All five previously shipped `color: white`
   on a vivid fill and NONE passed AA — .badge-closing measured 2.28, .badge-warm 2.80.
   Mixing the ink toward var(--ink) rather than #000 is what makes one rule serve both
   modes: --ink flips to #ededed in dark, so the ink lightens as the tint darkens.
   Ink at 60%: the worst case across every tone is .tag--wa at 5.10 (light), and WA needs
   its own 50% because it is the lightest hue in the palette.
   Locked by src/lib/contrast.test.ts — it parses these percentages out of this file. */
.badge-hot, .badge-warm, .badge-cold, .badge-closing, .badge-dead {
  padding: 1px var(--space-2); border-radius: 12px; font-size: var(--fs-xs); font-weight: 600;
  border: 1px solid currentColor;
}
.badge-hot     { background: color-mix(in srgb, var(--c-hot) 25%, var(--surface));     color: color-mix(in srgb, var(--c-hot) 60%, var(--ink)); }
.badge-warm    { background: color-mix(in srgb, var(--c-warm) 25%, var(--surface));    color: color-mix(in srgb, var(--c-warm) 60%, var(--ink)); }
.badge-cold    { background: color-mix(in srgb, var(--c-cold) 25%, var(--surface));    color: color-mix(in srgb, var(--c-cold) 60%, var(--ink)); }
.badge-closing { background: color-mix(in srgb, var(--c-closing) 25%, var(--surface)); color: color-mix(in srgb, var(--c-closing) 60%, var(--ink)); }
.badge-dead    { background: color-mix(in srgb, var(--c-dead) 25%, var(--surface));    color: color-mix(in srgb, var(--c-dead) 60%, var(--ink)); }

/* HTMX loading indicator */
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: block; }

/* Bottom nav */
.bottom-nav {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background: var(--pico-background-color);
  border-top: 1px solid var(--pico-muted-border-color);
  display: flex;
  justify-content: space-around;
  padding: 8px 0 max(8px, env(safe-area-inset-bottom));
  z-index: 100;
}
/* The `···` more-menu is a <button>, not an <a> — it opens a disclosure rather
   than navigating, so it needs to answer Enter/Space and carry aria-expanded.
   Pico gives buttons a fill, border and padding that would break the nav strip,
   hence the reset.

   Every selector here is a CHILD combinator or an explicit class — never a bare
   descendant. The dropdown this button opens lives INSIDE .bottom-nav, so
   `.bottom-nav <element>` reaches into it and wins at (0,1,1) over the menu's own
   (0,1,0) classes. That bit twice: `.bottom-nav button` shrank `.more-logout` from
   a 178px red 1rem row to a 44px muted 10.4px blob (the whole menu, for a `kitchen`
   user), and `.bottom-nav a` did the same to all five `.more-link` items —
   178x53 column/10.4px instead of 178x44 row/16px. The second one predated this
   work and survived the first fix because the lock I wrote for it listed
   button/input/select/textarea and not `a`. Measured at 390px both times. */
.bottom-nav > a,
.bottom-nav .nav-more {
  display: flex;
  flex-direction: column;
  align-items: center;
  font-size: var(--fs-2xs);
  text-decoration: none;
  color: var(--pico-muted-color);
  gap: 2px;
  min-width: 44px;
}
.bottom-nav .nav-more {
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  width: auto;
  font-family: inherit;
  font-weight: inherit;
  cursor: pointer;
}
.bottom-nav > a.active,
.bottom-nav .nav-more.active { color: var(--accent-text); }
.bottom-nav > a svg,
.bottom-nav .nav-more svg { width: 22px; height: 22px; }

/* Nav badge (test food count) */
.nav-badge {
  position: absolute;
  top: -4px;
  right: -6px;
  background: var(--c-danger-solid);
  color: #ffffff;
  font-size: 0.55rem;
  padding: 1px 4px;
  border-radius: 8px;
  line-height: 1.4;
  min-width: 14px;
  text-align: center;
  pointer-events: none;
}

/* Page content padding above bottom nav */
main { padding-bottom: 70px; }


/* Amber highlight for stale lead rows (7+ days no FU) */
.row-stale { background: color-mix(in srgb, var(--c-warm) 8%, var(--surface)) !important; }
/* Cells carry Pico's surface background, which tiled over the row tint: in `.list-card`
   card mode the tint showed only through the flex gaps (white boxes on a tinted card), and
   in the desktop table the cells hid it entirely. Clear the cell fill so the row tint is the
   single source and the whole card reads as one colour, both modes. */
.row-stale td { background: transparent; }

/* Dashboard cards */
.dash-card {
  background: var(--surface);
  border-radius: 12px;
  padding: 16px;
  margin-bottom: var(--space-3);
  box-shadow: var(--shadow-card);
}
.dash-card-title {
  font-size: 0.9rem;
  color: var(--ink-2);
  margin: 0 0 10px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: center;
  gap: 6px;
}
.dash-badge {
  background: var(--accent);
  color: var(--accent-ink);
  font-size: var(--fs-2xs);
  padding: 1px 7px;
  border-radius: 8px;
  margin-left: auto;
}
.dash-more {
  display: block;
  text-align: center;
  font-size: 0.8rem;
  color: var(--accent-text);
  margin-top: 8px;
  text-decoration: none;
}

/* Inline status select on leads list */
/* appearance:none + explicit sizing ensures it renders on mobile */
.status-select {
  /* 1px solid currentColor, not `none`: the fill is a tint now rather than a saturated
     block, so the border is what keeps the control's edge (and its status colour) visible
     against the row. currentColor means the per-status rules below only set `color`. */
  border: 1px solid currentColor;
  cursor: pointer;
  font-size: var(--fs-xs);
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 12px;
  -webkit-appearance: none;
  appearance: none;
  min-width: 90px;
  max-width: 145px;
  display: inline-block;
  background: var(--surface-2) !important;
  color: var(--ink-2) !important;
}
.status-select:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 1px; }
/* !important overrides Pico CSS custom property cascade on <select> */
/* Shared status tag — the generic sibling of .badge-*, for the ad-hoc pills that were
   inlined as `background:var(--c-X);color:white;padding:2px 8px;border-radius:12px`
   in ~15 places across the views (Aktif/Nonaktif, Terkirim, Perlu ditautkan, ⚠ Komplain,
   ⭐ Review, order status, …). Every one of them failed AA — white on --c-success is 2.28.
   Same tint recipe as .badge-*, so the whole app states status one way.
   Usage: <span class="tag tag--success">Aktif</span>. */
.tag {
  display: inline-block; padding: 1px 8px; border-radius: var(--r-pill);
  font-size: 0.7rem; font-weight: 600; white-space: nowrap;
  border: 1px solid currentColor;
  background: var(--surface-2); color: var(--ink-2);
}
.tag--success  { background: color-mix(in srgb, var(--c-success) 25%, var(--surface));  color: color-mix(in srgb, var(--c-success) 60%, var(--ink)); }
.tag--danger   { background: color-mix(in srgb, var(--c-danger) 25%, var(--surface));   color: color-mix(in srgb, var(--c-danger) 60%, var(--ink)); }
.tag--warm     { background: color-mix(in srgb, var(--c-warm) 25%, var(--surface));     color: color-mix(in srgb, var(--c-warm) 60%, var(--ink)); }
.tag--cold     { background: color-mix(in srgb, var(--c-cold) 25%, var(--surface));     color: color-mix(in srgb, var(--c-cold) 60%, var(--ink)); }
.tag--invoiced { background: color-mix(in srgb, var(--c-invoiced) 25%, var(--surface)); color: color-mix(in srgb, var(--c-invoiced) 60%, var(--ink)); }
.tag--dead     { background: color-mix(in srgb, var(--c-dead) 25%, var(--surface));     color: color-mix(in srgb, var(--c-dead) 60%, var(--ink)); }
/* 50%, not the 60% the others use: WhatsApp green is the lightest hue in the palette and
   lands on 4.22 at 60%, just under the floor. */
.tag--wa       { background: color-mix(in srgb, var(--c-wa) 25%, var(--surface));       color: color-mix(in srgb, var(--c-wa) 50%, var(--ink)); }
/* Tighter variant for pills packed into a table row. */
.tag--xs { padding: 1px 6px; font-size: var(--fs-2xs); }

/* The leads status <select> wears the same badge, so it takes the same recipe — a border
   in the status colour keeps it reading as an editable control rather than a flat chip. */
.status-select.badge-hot     { background: color-mix(in srgb, var(--c-hot) 25%, var(--surface)) !important;     color: color-mix(in srgb, var(--c-hot) 60%, var(--ink)) !important;     border-color: currentColor !important; }
.status-select.badge-warm    { background: color-mix(in srgb, var(--c-warm) 25%, var(--surface)) !important;    color: color-mix(in srgb, var(--c-warm) 60%, var(--ink)) !important;    border-color: currentColor !important; }
.status-select.badge-cold    { background: color-mix(in srgb, var(--c-cold) 25%, var(--surface)) !important;    color: color-mix(in srgb, var(--c-cold) 60%, var(--ink)) !important;    border-color: currentColor !important; }
.status-select.badge-closing { background: color-mix(in srgb, var(--c-closing) 25%, var(--surface)) !important; color: color-mix(in srgb, var(--c-closing) 60%, var(--ink)) !important; border-color: currentColor !important; }
.status-select.badge-dead    { background: color-mix(in srgb, var(--c-dead) 25%, var(--surface)) !important;    color: color-mix(in srgb, var(--c-dead) 60%, var(--ink)) !important;    border-color: currentColor !important; }

/* ---- Callout banners ----
   The error/success/warning strip that sits above a form or beside a field. Counts and
   ratios live in docs/theming.md ("Callout banners") — one place, because three slightly
   different sets of "measured" numbers in three files is how a doc stops being evidence.

   The short version: a raw status token used as TEXT on a 12-18% TINT OF ITSELF fails AA,
   and each tone fails in only ONE mode, so it looks correct to whoever writes it.

   Phase 2 fixed ~120 sites that use `color: var(--c-danger)` on `--surface` by darkening
   the tokens per mode, and locked that. It could not catch this shape, because the lock
   compares the token against `--surface` — and these sit on a tint, which is much closer
   to the token than the surface is. Same token, different background, 4.4 instead of 5.9.

   The ink here is the `.tag` recipe (60% toward `--ink`), which is what makes one rule work
   in both modes. The tint stays at 15% because the border carries the tone — dropping the
   border would need a stronger fill to stay legible as a region. */
.callout {
  padding: 10px 14px;
  border-radius: 6px;   /* off-scale (--r-* is 4/8/16/24); stays literal like off-scale fs/space — rule 28 */
  margin-bottom: var(--space-3);
  border: 1px solid;
}
.callout--danger  { background: color-mix(in srgb, var(--c-danger) 15%, var(--surface));  border-color: color-mix(in srgb, var(--c-danger) 40%, var(--surface));  color: color-mix(in srgb, var(--c-danger) 60%, var(--ink)); }
.callout--success { background: color-mix(in srgb, var(--c-success) 15%, var(--surface)); border-color: color-mix(in srgb, var(--c-success) 40%, var(--surface)); color: color-mix(in srgb, var(--c-success) 60%, var(--ink)); }
.callout--warm    { background: color-mix(in srgb, var(--c-warm) 15%, var(--surface));    border-color: color-mix(in srgb, var(--c-warm) 40%, var(--surface));    color: color-mix(in srgb, var(--c-warm) 60%, var(--ink)); }
.callout--cold    { background: color-mix(in srgb, var(--c-cold) 15%, var(--surface));    border-color: color-mix(in srgb, var(--c-cold) 40%, var(--surface));    color: color-mix(in srgb, var(--c-cold) 60%, var(--ink)); }
/* Inline note under a field or inside a product panel, rather than a page-level banner. */
.callout--sm { padding: 8px 10px; font-size: 0.78rem; }
/* Left-edge rule instead of a full box — used where the note hangs off a list item. The
   left border keeps the full-strength token rather than the 40% tone border: it is a
   decorative accent bar, not text, so 1.4.3 does not apply, and at 40% on a 15% tint of the
   same hue the bar all but disappears. The site this replaced used `3px solid var(--c-warm)`
   for that reason. */
.callout--bar { border-width: 1px 1px 1px 3px; border-radius: 0 6px 6px 0; }
.callout--bar.callout--danger  { border-left-color: var(--c-danger); }
.callout--bar.callout--success { border-left-color: var(--c-success); }
.callout--bar.callout--warm    { border-left-color: var(--c-warm); }
.callout--bar.callout--cold    { border-left-color: var(--c-cold); }

/* Copy-to-clipboard button */
.copy-btn {
  background: none;
  border: none;
  cursor: pointer;
  padding: 0 2px;
  font-size: 0.8rem;
  min-height: unset;
  line-height: 1;
  /* De-emphasis comes from the token, NOT from opacity on top of it. --pico-muted-color is
     already the muted step and is tuned to clear AA on --surface; compositing it at 0.6
     re-muted an already-muted colour and landed on 2.52:1 in light mode — under even the
     3.0 non-text floor. The button swaps its glyph to a bare "✓" after a copy, so this is
     real text, not only an icon. Hover de-emphasises less by darkening, not by un-fading. */
  color: var(--pico-muted-color);
  vertical-align: middle;
}
.copy-btn:hover { color: var(--ink); }

/* Global search results */
.search-result-item {
  display: block;
  padding: 8px 12px;
  border-radius: 6px;
  text-decoration: none;
  color: inherit;
  font-size: var(--fs-sm);
  border-bottom: 1px solid var(--line);
}
.search-result-item:last-child { border-bottom: none; }
.search-result-item:hover { background: var(--surface-2); }
.search-result-tag {
  font-size: var(--fs-2xs);
  background: color-mix(in srgb, var(--accent) 14%, var(--surface));
  color: var(--accent-text);
  padding: 1px 6px;
  border-radius: 8px;
  margin-right: 4px;
}
.search-result-tag--order {
  background: color-mix(in srgb, var(--c-success) 18%, var(--surface));
  color: color-mix(in srgb, var(--c-success) 60%, var(--ink));
}
.search-result-tag--tf {
  background: color-mix(in srgb, var(--c-warm) 18%, var(--surface));
  color: color-mix(in srgb, var(--c-warm) 60%, var(--ink));
}

/* ---- Shell components (T2) ---- */
.app-header {
  position: sticky; top: 0; z-index: 50;
  background: var(--pico-background-color);
  border-bottom: 1px solid var(--pico-muted-border-color);
  padding: 8px 16px;
  display: flex; align-items: center; justify-content: space-between;
  backdrop-filter: blur(12px);
}
.app-brand { text-decoration: none; color: inherit; }
.app-brand strong { font-size: var(--fs-base); letter-spacing: -0.2px; }
.header-actions { display: flex; align-items: center; gap: var(--space-2); }

.icon-btn {
  background: none; border: none; cursor: pointer; padding: 4px;
  color: var(--pico-muted-color);
  display: flex; align-items: center; min-height: unset; line-height: 1;
}
.icon-btn svg { width: 20px; height: 20px; }
.theme-toggle.icon-btn { margin-bottom: 0; }

.user-name { color: var(--pico-muted-color); }

/* Role pill: a solid identity chip, so it uses the --c-role-* solid family rather than
   the badge tint recipe. The default fill is its own token, NOT var(--c-dead) — that one
   lightens to #9ca3af in dark mode, where white ink on it drops to 2.44. */
/* .role-pill = a user's role (has [data-role]); .tag-pill = any other neutral
   label (kitchen assign/stage/kemasan). Same shape, split for clarity — rule 28. */
.role-pill, .tag-pill {
  color: #ffffff; padding: 2px 8px; border-radius: var(--r-pill);
  font-size: 0.7rem; background: var(--c-neutral-solid);
  box-shadow: 0 0 0 1px var(--solid-ring);
}
.role-pill[data-role="owner"]   { background: var(--c-role-owner); }
.role-pill[data-role="cs"]      { background: var(--c-role-cs); }
.role-pill[data-role="admin"]   { background: var(--c-role-admin); }
.role-pill[data-role="kitchen"] { background: var(--c-role-kitchen); }
.role-pill[data-role="driver"]  { background: var(--c-role-driver); }

.search-panel {
  position: sticky; top: 49px; z-index: 49;
  background: var(--pico-background-color);
  border-bottom: 1px solid var(--pico-muted-border-color);
  padding: 8px 16px;
}
.search-panel input[type="search"] { margin: 0; font-size: 0.9rem; }
.search-results { max-height: 320px; overflow-y: auto; margin-top: 8px; }

.app-main { padding: 16px; padding-bottom: 80px; }
/* The floating "+" (`.fab-add`) is fixed at bottom:80px height:52px, so its top sits 132px
   above the viewport bottom — any content that scrolls to rest in that band is covered. Reserve
   enough bottom scroll room on FAB screens so the last row always clears it. Replaces a
   mobile-only `.list-card` last-row margin that missed products (no `.list-card`) and every
   list-card screen at desktop (the margin lived inside the ≤640px query). One rule, all widths,
   all four FAB screens. `:has()` is baseline in the Android Chrome this app targets.
   150 must stay > the FAB's 132 (bottom:80 + height:52) — keep it in step if that geometry moves. */
.app-main:has(.fab-add) { padding-bottom: 150px; }

.flash {
  background: color-mix(in srgb, var(--c-success) 25%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--c-success) 35%, var(--surface));
  color: color-mix(in srgb, var(--c-success) 55%, var(--ink));
  padding: 10px 14px; border-radius: var(--r-md);
  margin-bottom: var(--space-3);
  display: flex; justify-content: space-between; align-items: center;
}
.flash button {
  background: none; border: none; cursor: pointer;
  font-size: var(--fs-lg); line-height: 1; color: inherit;
}
/* Danger variant — used by the global HTMX failure toast (public/htmx-errors.js). */
.flash-danger {
  background: color-mix(in srgb, var(--c-danger) 25%, var(--surface));
  border-color: color-mix(in srgb, var(--c-danger) 40%, var(--surface));
  color: color-mix(in srgb, var(--c-danger) 55%, var(--ink));
}
/* Toast is appended to <body>, so it positions itself. It must clear whatever fixed
   bar sits at the bottom of the page: .bottom-nav on internal pages, .cart-bar on the
   public order form (both are fixed, bottom:0, direct children of <body>, so the toast
   — appended after them — can use a sibling selector). Anything without such a bar
   keeps the plain 16px inset. Getting this wrong is not cosmetic on the order form:
   at 16px the toast (z-index 300) covers the checkout button. */
#htmx-error-toast {
  position: fixed; left: 16px; right: 16px;
  bottom: calc(16px + env(safe-area-inset-bottom));
  z-index: 300; margin: 0; box-shadow: var(--shadow-card);
}
.bottom-nav ~ #htmx-error-toast,
.cart-bar ~ #htmx-error-toast { bottom: calc(72px + env(safe-area-inset-bottom)); }

.nav-badge-anchor { position: relative; }
.nav-icon-wrap { position: relative; display: inline-flex; }

.more-wrap { position: relative; }
.more-menu {
  position: absolute; bottom: 60px; right: 0;
  background: var(--pico-background-color);
  border: 1px solid var(--pico-muted-border-color);
  border-radius: var(--r-md); min-width: 180px;
  box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1); z-index: 200;
}
.more-link {
  display: flex; align-items: center; gap: var(--space-2);
  padding: 10px 16px; text-decoration: none; color: var(--pico-color);
}
/* The menu sizes its own icons. It used to inherit this from `.bottom-nav a svg` — the
   over-broad selector that also wrecked `.more-link`'s layout — so tightening that one to a
   child combinator took the sizing away with it, and two icons (Dapur, Invoices, whose SVGs
   carry no width/height attributes) rendered at 91x91, making 111px and 96px menu rows.
   Caught on production immediately after deploy; the local check missed it because the probe
   markup used a bare <span> where the real menu has an inline SVG. */
.more-link svg { width: 22px; height: 22px; flex: 0 0 auto; }

.more-logout-form { margin: 0; border-top: 1px solid var(--pico-muted-border-color); }
.more-logout {
  width: 100%; text-align: left; padding: 10px 16px;
  background: none; border: none; cursor: pointer;
  color: var(--c-danger); font-size: var(--fs-base); min-height: 44px;
}

/* ---- De-inlined recurring inline styles (T5 follow-up) ----
   Named classes for the highest-frequency var(--pico-*) inline styles.
   One-off inline styles are intentionally left inline (a single-use class
   is not clearer than the inline rule). */
.t-muted        { color: var(--pico-muted-color); }
.t-muted-xs     { color: var(--pico-muted-color); font-size: var(--fs-xs); }
.bg-muted       { background: var(--surface-2); }
.icon-link-muted { color: var(--pico-muted-color); text-decoration: none; font-size: var(--fs-lg); }
/* Empty states — see src/views/shared/empty-state.ts. The layout lives here so views pass
   copy, not CSS; the old bare class was `text-align:center;padding:24px` and every richer
   variant re-inlined its own version of that. */
.empty-state       { text-align: center; color: var(--ink-3); padding: 32px 16px; }
.empty-state-icon  { font-size: var(--fs-xl); margin-bottom: 8px; line-height: 1; }
.empty-state-title { font-weight: 600; margin-bottom: 4px; color: var(--ink-2); }
.empty-state-hint  { font-size: var(--fs-sm); }
/* Per-column placeholder on the kitchen board — a lone "—" said nothing about whether
   the column was empty or still loading. Too small a space for the full block. */
.kit-col-empty     { color: var(--ink-3); font-size: 0.8rem; }

/* ---- Kitchen KDS (Jadwal / Papan / Detail) ----
   Time-first calendar rows, colour-by-stage, one big bump button — see docs/kitchen.md.
   Reuses existing tokens/tones throughout (rule 28/37d); no new hex anywhere below. */
.kit-nav     { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-bottom: var(--space-3); }
.kit-summary { font-size: var(--fs-sm); color: var(--ink-3); margin-bottom: var(--space-2); }

/* -- Papan (kanban): mobile stage tabs, desktop 3-col grid, all three columns stay in the
   DOM (the optimistic-move script in board.ts depends on `[data-stage]` columns). -- */
.kit-papan { margin-top: var(--space-2); }
.kit-stage-tabs { display: flex; gap: var(--space-2); margin-bottom: var(--space-3); }
.kit-stage-tab {
  flex: 1; padding: 8px 4px; border-radius: var(--r-sm); border: 1px solid var(--line-2);
  background: var(--surface-2); color: var(--ink-2); font-size: var(--fs-sm); cursor: pointer;
}
.kit-stage-tab[aria-selected="true"] {
  background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 600;
}
@media (min-width: 768px) { .kit-stage-tabs { display: none; } }

.kit-kanban-grid { display: block; }
@media (min-width: 768px) {
  .kit-kanban-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-3); align-items: start; }
}
/* Mobile: only the active stage's column shows (Alpine toggles this class on click).
   Desktop: all three always show and the tabs above are hidden instead. */
.kit-col-wrap { display: none; }
.kit-col-wrap--active { display: block; }
@media (min-width: 768px) { .kit-col-wrap { display: block; } }
.kit-col-heading { font-size: var(--fs-sm); margin-bottom: var(--space-2); }
.kit-col { display: flex; flex-direction: column; gap: var(--space-2); }

.kit-card {
  border: 1px solid var(--line-2); border-radius: var(--r-card); padding: var(--space-3);
  background: var(--surface); box-shadow: var(--shadow-card);
}
.kit-card-title { display: block; text-decoration: none; color: var(--ink); margin-bottom: 2px; }
.kit-card-time  { font-size: var(--fs-sm); color: var(--ink-2); }
.kit-card-meta  { font-size: var(--fs-sm); margin-top: 2px; }
.kit-card-chips { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; font-size: var(--fs-xs); color: var(--ink-3); margin-top: 4px; }
/* Clamped rather than server-truncated so the class alone controls line count at any width. */
.kit-notes {
  font-size: var(--fs-xs); color: var(--ink-3); margin-top: 4px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.kit-card-actions   { display: flex; gap: var(--space-2); margin-top: var(--space-3); }
.kit-advance-btn    { flex: 1; margin: 0; }
.kit-back-btn       { flex: 0 0 auto; min-width: 56px; margin: 0; }
.kit-assign-select  { margin-top: var(--space-2); font-size: var(--fs-sm); width: 100%; }

.kit-range        { margin-bottom: var(--space-3); font-size: var(--fs-sm); }
.kit-range summary { cursor: pointer; color: var(--ink-2); }
.kit-range-form   { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-2); margin-top: var(--space-2); }
.kit-range-form label { display: flex; flex-direction: column; gap: 2px; margin: 0; font-size: var(--fs-xs); }
.kit-range-form input[type="date"] { margin: 0; padding: 4px 6px; }

.kit-selesai     { margin-top: var(--space-4); font-size: var(--fs-sm); }
.kit-selesai summary { cursor: pointer; }
.kit-selesai-row {
  display: flex; align-items: center; gap: var(--space-2); padding: 5px 0;
  border-bottom: 1px dashed var(--line-2); opacity: 0.7;
}
.kit-selesai-link { flex: 1; text-decoration: none; color: inherit; font-size: var(--fs-sm); }

/* -- Jadwal (calendar) -- */
.kit-day { margin-bottom: var(--space-3); }
/* Sticky just below .app-header (49px matches .search-panel's own offset below it). */
.kit-day-header {
  position: sticky; top: 49px; z-index: 20; background: var(--surface);
  font-size: var(--fs-sm); padding: 6px 0; margin: 0; border-bottom: 1px solid var(--line-2);
}
.kit-day-header small { color: var(--ink-3); font-weight: 400; }
.kit-day-empty { font-size: var(--fs-sm); color: var(--ink-3); padding: 4px 0; margin: 0; }

.kit-rekap         { font-size: var(--fs-sm); margin: var(--space-2) 0; }
.kit-rekap summary { cursor: pointer; color: var(--ink-2); }
.kit-rekap h5       { font-size: var(--fs-xs); color: var(--ink-3); margin: var(--space-2) 0 2px; }
.kit-rekap ul        { margin: 0 0 0 1.1em; padding: 0; }
.kit-rekap li        { font-size: var(--fs-sm); }

.kit-row {
  display: grid; grid-template-columns: auto 1fr auto; gap: var(--space-2);
  align-items: start; padding: var(--space-2) 0; border-bottom: 1px dashed var(--line-2);
}
.kit-row--done  { opacity: 0.6; }
.kit-row-time {
  font-variant-numeric: tabular-nums; font-weight: 700; font-size: var(--fs-lg);
  color: var(--ink); text-decoration: none; line-height: 1.2; padding-top: 2px;
  white-space: nowrap; min-width: 3.2rem; /* "09:00" wrapped to "09:0/0" at a fixed 3.5rem */
}
.kit-row-main   { text-decoration: none; color: inherit; min-width: 0; }
.kit-row-line1  { font-size: var(--fs-base); }
/* Test food's "product" is the free-text request_tambahan — one live row is a whole
   sentence, which made a 4-line bold title and broke the Jadwal's scan. Two lines max;
   the full text is on the detail page one tap away. TEST FOOD ONLY: an order row carries
   its " · 40 porsi" inside the same line box, and a clamp would clip it silently on a long
   paket name (review, 2026-09-26); test food never has a porsi. */
.kit-row-line1--clamp { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; }
.kit-row-line2  { font-size: var(--fs-sm); color: var(--ink-2); overflow-wrap: anywhere; }
/* -- Riwayat (past test food + CS feedback, views/kitchen/riwayat.ts): the whole row is one link. */
.kit-hist      { text-decoration: none; color: inherit; }
.kit-hist-date { font-size: var(--fs-sm); padding-top: 3px; }
.kit-feedback {
  margin-top: 4px; padding-left: 8px; border-left: 2px solid var(--line);
  font-size: var(--fs-sm); color: var(--ink-2); overflow-wrap: anywhere;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden;
}
.kit-row-chips  { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
.kit-row-actions  { display: flex; flex-direction: column; gap: 4px; align-items: stretch; }
.kit-row-advance  { margin: 0; min-width: 72px; }

/* -- Detail -- */
.kit-detail-header      { margin-bottom: var(--space-3); }
.kit-detail-header-top  { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.kit-back-link          { color: var(--ink-3); font-size: var(--fs-sm); text-decoration: none; }
.kit-detail-delivery    { font-size: var(--fs-lg); font-weight: 600; margin-top: var(--space-2); }
.kit-detail-actions     { display: flex; gap: var(--space-2); margin-top: var(--space-3); }
.kit-detail-advance     { flex: 1; margin: 0; }
.kit-detail-back        { flex: 0 0 auto; min-width: 56px; margin: 0; }
.kit-section-heading    { font-size: var(--fs-base); margin: var(--space-4) 0 var(--space-2); }
.kit-items              { display: flex; flex-direction: column; gap: var(--space-3); }
.kit-item-card          { border: 1px solid var(--line-2); border-radius: var(--r-card); padding: var(--space-3); }
.kit-item-head          { font-size: var(--fs-base); }
.kit-item-qty           { font-size: var(--fs-sm); color: var(--ink-2); margin-top: 2px; }
.kit-item-slots         { margin-top: var(--space-2); }
/* No transitions declared anywhere above, so nothing here needs its own
   prefers-reduced-motion override — the global `*, *::before, *::after` rule further down
   already covers every element in the app. */

/* Legend for icon-only row actions. `title` needs a hover the Android target cannot
   produce, so glyph meanings have to be on the page, not in an attribute. */
.row-action-legend { font-size: 0.72rem; color: var(--ink-3); margin: 0 0 6px; }
/* Scoped to the reflow it exists to neutralise, and ONLY there. Unscoped, `display: block`
   on a <tr> takes the row out of table layout at every width, which makes `colspan` inert —
   the block then sits in an anonymous single-column box (measured 405px inside a 1168px
   table) instead of spanning it. Products is the only list table without `.list-card`, so it
   never needed this at any width — its colspan already does the centring.
   `.list-card tr.empty-state-row` is (0,2,1), matching `.list-card tr:not(:has(.c-actions))`
   above, and is declared later — so order alone resolves the 2px lane reserve. */
@media (max-width: 640px) {
  .list-card tr.empty-state-row { display: block; padding: 0; }
  .list-card .empty-state-row td { display: block; width: 100%; }
}
/* ---- Row swipe actions (public/swipe-delete.js) ----
   /orders, /leads, /test-food active-view rows only — see docs/screens.md. `data-swipe` on
   the <tr> opts in (swipe right → WA); `data-swipe-delete` adds swipe left → delete, only
   where the row's own delete permission allows it. `touch-action: pan-y` keeps native
   vertical scroll working: the JS only takes over once a horizontal drag is detected (~10px
   axis lock), so a vertical swipe is never intercepted here. */
[data-swipe] { touch-action: pan-y; position: relative; }
/* Unscoped (not just the ≤640px card layout): kept in the DOM, not removed, until the
   5s undo window resolves or the delete request completes — see public/swipe-delete.js.
   The gesture itself is touch-only, but hiding must work at any width it can fire at. */
[data-swipe].swipe-hidden { display: none; }
/* Touch targets (2026-10-06 touch audit). These glyph-sized controls measured 16x15 (📞),
   20x13 (📋), 16x29 (←) and 18x15 (the Closing counts) — a fingertip is ~44px. The glyph keeps
   its size; a centred pseudo-element carries a 40x36 hit area (44x44 for the back arrow). Coarse pointers only, so the
   dense mouse layout is untouched. The margins keep neighbouring hit areas from stacking: a
   later element's area would otherwise sit on top of the WA number's last digits. */
@media (pointer: coarse) {
  .icon-link-muted, .tap-sm, .copy-btn, #leads-summary-strip a { position: relative; }
  .icon-link-muted::after, .tap-sm::after, .copy-btn::after, #leads-summary-strip a::after {
    content: ''; position: absolute; top: 50%; left: 50%;
    width: 40px; height: 36px; transform: translate(-50%, -50%);
  }
  /* 36px tall, not 44: detail rows (No HP / No HP 2) sit 38px apart, and taller areas would
     stack across rows. */
  .icon-link-muted::after { height: 44px; width: 44px; }
  .tap-sm { margin-left: 14px; margin-right: 12px; }
  .tap-sm + .copy-btn { margin-left: 12px; }
  /* List rows are left exactly as they were, at every width. Under 640px these two are
     display:none in a list anyway; on a tablet or a landscape phone they are visible inside
     a table row, where 38px of extra margin would widen the phone column. */
  .list-card .tap-sm, .list-card .tap-sm + .copy-btn { margin: 0; }
  .list-card .tap-sm::after, .list-card .copy-btn::after { content: none; }
}

/* Swiping is touch-only; fine-pointer users have equivalent visible row actions instead. */
@media (hover: hover) and (pointer: fine) {
  .swipe-hint { display: none; }
  .list-card .desktop-row-action-cell { display: table-cell; white-space: nowrap; }
  .desktop-row-actions { display: inline-flex; align-items: center; gap: 4px; }
  .desktop-row-actions .row-action { text-decoration: none; }
  .desktop-row-delete { display: inline-flex; }
  .desktop-action-hint, .desktop-action-label { display: inline; }
  .c-actions .row-action + .row-action { margin-left: 4px; }
}

@media (max-width: 640px) and (hover: hover) and (pointer: fine) {
  .list-card .desktop-row-action-cell {
    position: absolute; right: 0; top: 50%; transform: translateY(-50%);
    display: block; padding: 0;
  }
  .list-card--act1 tr:has(.desktop-row-action-cell) { padding-right: 40px; }
  .list-card--act1 tr:has(.desktop-row-actions .row-action + .row-action) { padding-right: 74px; }
  #leads-content .list-card tr:has(.desktop-row-delete) { padding-right: 110px; }
  .list-card .desktop-row-actions { gap: 0; }
  .list-card .desktop-row-actions .row-action { min-height: 34px; min-width: 34px; }
}

@media (max-width: 640px) {
  /* The backdrop is a pseudo-element on the ROW, not a cell, so it stays still while the
     cells slide (JS transforms each <td>, never the <tr> — a transform would carry the
     row's own pseudo along). It is exactly as wide as the strip the cells have uncovered
     (--swipe-dx, set by the JS) and anchored to the edge the swipe opens, so it is solid
     colour with nothing behind the cells. It used to span the whole row at 60% opacity:
     red bled through every flex gap and the empty action lane (stripes, pink), and the
     cells needed an opaque --surface fill that also painted over `.row-stale`'s tint.
     Cells stay transparent now — no fill rule here. */
  [data-swipe]::after {
    content: "🗑 Hapus"; position: absolute; top: 0; bottom: 0; right: 0; left: auto;
    width: var(--swipe-dx, 0px); overflow: hidden; white-space: nowrap; z-index: 0;
    display: flex; align-items: center; justify-content: flex-end; padding: 0;
    font-weight: 600; font-size: var(--fs-sm);
    background: var(--c-danger-solid); color: #ffffff;
    transition: width 0.2s ease, font-size 0.1s ease;
  }
  [data-swipe].swipe-right::after {
    content: "💬 Chat WA"; left: 0; right: auto; justify-content: flex-start;
    background: var(--c-wa-solid);
  }
  /* Label keeps a 16px inset from its edge without eating width while the strip is thin. */
  [data-swipe]::after { padding-right: min(16px, var(--swipe-dx, 0px)); }
  [data-swipe].swipe-right::after { padding-right: 0; padding-left: min(16px, var(--swipe-dx, 0px)); }
  /* Past the commit threshold: the label grows — the "let go now" cue (iOS Mail / Gmail). */
  [data-swipe].swipe-armed::after { font-size: var(--fs-lg); }
  [data-swipe] > td { position: relative; z-index: 1; transition: transform 0.2s ease, opacity 0.2s ease; }
  /* No transition while the finger is down — the row must track the pointer 1:1,
     not ease toward it. */
  [data-swipe].swipe-dragging > td,
  [data-swipe].swipe-dragging::after { transition: none; }
}

/* Deferred-undo toast. Unlike #htmx-error-toast (which also has to clear the public
   order form's .cart-bar), the bottom-nav clearance here is unconditional — swipe-
   delete only ever runs on internal list pages, which always render .bottom-nav. */
.swipe-undo-toast {
  position: fixed; left: 16px; right: 16px;
  bottom: calc(72px + env(safe-area-inset-bottom));
  z-index: 300; margin: 0; box-shadow: var(--shadow-card);
  background: var(--ink); color: var(--surface);
  border-radius: var(--r-md); padding: 12px 14px;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  font-size: var(--fs-sm);
}
.swipe-undo-toast .swipe-undo-btn {
  background: none; border: none; color: var(--surface); font-weight: 700;
  text-decoration: underline; cursor: pointer; min-height: 32px; padding: 4px 8px;
}

.metric-val     { font-size: 0.8rem; font-weight: 700; color: var(--accent-text); margin-top: 4px; }
.stat-box {
  background: var(--pico-card-background-color);
  border: 1px solid var(--pico-muted-border-color);
  border-radius: 8px; padding: 10px; text-align: center;
}
.split-row {
  display: flex; justify-content: space-between;
  padding: 10px 14px; border-bottom: 1px solid var(--pico-muted-border-color);
  font-size: 0.875rem;
}
.bordered-box {
  border: 1px solid var(--pico-muted-border-color);
  border-radius: 8px; padding: 14px; margin-bottom: var(--space-4);
}

/* ---- Theme toggle (System/Light/Dark) ---- */
.theme-toggle .ic { display: none; align-items: center; }
.theme-toggle .ic svg { width: 20px; height: 20px; }
/* show the icon matching the current preference; default (no themePref yet) = system */
:root[data-theme-pref="light"] .theme-toggle .ic-light,
:root[data-theme-pref="dark"]  .theme-toggle .ic-dark,
:root:not([data-theme-pref="light"]):not([data-theme-pref="dark"]) .theme-toggle .ic-system {
  display: inline-flex;
}

/* Inline field validation (public/field-validation.js) */
.field-error { font-size: 0.78rem; color: var(--c-danger); margin-top: 3px; display: none; }
.field-error.visible { display: block; }
input.input-error, textarea.input-error, select.input-error { border-color: var(--c-danger) !important; }

/* ---- Reduced motion (WCAG 2.3.3) ----
   There were zero prefers-reduced-motion rules before 2026-07-22. This lives in
   style.css rather than beside each @keyframes because it also has to cover
   Pico's own transitions, which we don't author.

   `!important` is load-bearing, not defensive: layout.ts and order-form/page.ts
   both declare `animation: htmx-progress …` in a page-level <style> parsed AFTER
   this file, so an equal-specificity rule here would lose.

   The one thing worth checking when editing this is that #htmx-indicator — the
   3px in-flight bar — stays VISIBLE. Killing its motion must not kill the busy
   affordance with it. It survives because `animation-fill-mode` is `none`, so
   once the fast-forwarded animation ends the element reverts to its own style
   (full width, opacity 1) rather than resting on the 100% keyframe's opacity:0.
   Measured under emulated reduce: 1280×3, opacity 1. An earlier version of this
   block carried an `animation: none !important` override for that element on the
   theory that the blanket rule would hide it; the browser said otherwise and the
   override was dead weight. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ── Customer order-revision panel (/orders/:id) ──────────────────────────────
   The old -> new diff is a REQUIREMENT, not decoration: the owner kept alamat_kirim on the
   applied whitelist (2026-08-26) on the basis that a CS sees both values before pressing
   Terapkan. Tones come from the shared --c-* families (rule 28); nothing is hand-inlined. */
.rev-panel .rev-req { border: 1px solid var(--line-2); border-radius: var(--r-md); padding: 12px; margin-bottom: 10px; }
.rev-panel .rev-req:last-child { margin-bottom: 0; }
.rev-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.rev-diff { width: 100%; font-size: 0.82rem; margin: 0; }
.rev-diff th { font-weight: 600; color: var(--ink-3); font-size: var(--fs-2xs); text-transform: uppercase; }
.rev-diff td, .rev-diff th { padding: 4px 6px; vertical-align: top; }
.rev-diff .rev-old { color: var(--ink-3); text-decoration: line-through; }
.rev-diff .rev-new { color: var(--ink); }
.rev-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 10px; }
.rev-actions form { margin: 0; }
.rev-actions button { margin: 0; }

/* ── /performa ─────────────────────────────────────────────── */
.perf-series-1 { background: var(--c-series-1); } .perf-series-2 { background: var(--c-series-2); }
.perf-series-3 { background: var(--c-series-3); } .perf-series-4 { background: var(--c-series-4); }
.perf-series-5 { background: var(--c-series-5); } .perf-series-6 { background: var(--c-series-6); }
.perf-series-7 { background: var(--c-series-7); } .perf-series-8 { background: var(--c-series-8); }
.perf-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; flex: none; }
.perf-dot--total { background: var(--ink-4); }
.perf-dot--libur { background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-sm); }
.perf-dot--team { width: 16px; height: 0; border-top: 2.5px dashed var(--ink-2); border-radius: 0; }
.perf-filterbar { position: sticky; top: 0; z-index: 2; display: flex; gap: var(--space-2); align-items: center; padding: var(--space-2); background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-md); }
.perf-active { display: flex; gap: var(--space-1); flex-wrap: wrap; overflow: hidden; max-height: 44px; }
.perf-count { display: inline-flex; min-width: 18px; justify-content: center; border-radius: var(--r-pill); background: var(--c-warm-solid); color: #fff; font-size: var(--fs-xs); }
.perf-chips { display: flex; flex-wrap: wrap; gap: var(--space-1); }
.perf-chip { display: inline-flex; align-items: center; gap: 6px; min-height: 40px; padding: 0 var(--space-2); border: 1px solid var(--line); border-radius: var(--r-pill); }
.perf-save-row { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; margin-block: var(--space-2); }
.perf-save-row form { margin: 0; }
.perf-cards { display: grid; gap: var(--space-2); }
.perf-card { margin: 0; padding: var(--space-3); display: grid; gap: var(--space-2); }
.perf-card__head { display: flex; align-items: center; gap: var(--space-2); }
.perf-card__head strong { flex: 1; }
.perf-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-2); }
.perf-stat { display: flex; flex-direction: column; }
.perf-stat__label { font-size: var(--fs-xs); color: var(--ink-3); }
.perf-stat__value { font-size: var(--fs-lg); font-weight: 700; white-space: nowrap; }
@media (max-width: 480px) { .perf-stat__value { font-size: var(--fs-base); } } /* "Rp 515,4 jt" fits a third of a phone */
.perf-ok { color: var(--c-success); } .perf-bad { color: var(--c-danger); }
.perf-delta { font-size: var(--fs-xs); color: var(--ink-3); }
.perf-delta--good { color: var(--c-success); font-weight: 600; } .perf-delta--bad { color: var(--c-danger); font-weight: 600; }
.perf-target__head { display: flex; justify-content: space-between; font-size: var(--fs-sm); }
.perf-target__bar { position: relative; height: 10px; border-radius: var(--r-pill); background: var(--surface-2); }
.perf-target__fill { position: absolute; inset: 0 auto 0 0; border-radius: var(--r-pill); }
.perf-ok-bg { background: var(--c-success-solid); } .perf-warn-bg { background: var(--c-warm-solid); }
.perf-target__pace { position: absolute; top: -4px; width: 2px; height: 18px; background: var(--ink); }
.perf-legend { display: flex; flex-wrap: wrap; gap: var(--space-3); font-size: var(--fs-sm); margin-block: var(--space-2); }
.perf-legend__item { display: inline-flex; align-items: center; gap: 6px; }
.perf-chart { margin: 0 0 var(--space-3); padding: var(--space-3); }
.perf-chart__head { display: flex; align-items: center; gap: var(--space-2); }
.perf-chart__head strong { flex: 1; }
.perf-seg { display: inline-flex; width: auto; border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; }
.perf-seg button { margin: 0; border: 0; border-radius: 0; padding: 0 var(--space-2); min-height: 36px; background: var(--surface); color: var(--ink); font-size: var(--fs-xs); }
.perf-seg button[aria-pressed="true"] { background: var(--accent); color: var(--accent-ink); }
.perf-canvas { position: relative; aspect-ratio: 16 / 10; width: 100%; }
.perf-detail { margin-top: var(--space-2); padding: var(--space-2); border: 1px solid var(--line); border-radius: var(--r-md); background: var(--surface-2); font-size: var(--fs-sm); }
.perf-detail__rows { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-3); margin: var(--space-1) 0; }
.perf-channel-bars { display: grid; gap: var(--space-2); }
.perf-channel-bar { display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: var(--space-2); align-items: center; }
.perf-channel-bar .perf-legend__item { display: flex; }
.perf-channel-bar span.bar { display: block; height: 8px; border-radius: 2px; }

/* Keep these defaults identical to the current phone layouts. */
.admin-settings { max-width: 480px; }
.lead-action-row { display: grid; gap: var(--space-2); }

@media (min-width: 768px) {
  .perf-chart-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-3);
  }
  .perf-chart-grid .perf-chart { margin: 0; }
  .perf-chart-grid .perf-chart[data-chart="leads"],
  .perf-chart-grid .perf-chart[data-chart="rt"],
  .perf-chart-grid .perf-chart[data-chart="val"] { grid-column: 1 / -1; }
  .perf-chart-grid .perf-canvas { height: 340px; }

  .admin-settings { max-width: none; }
  .admin-settings-form {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
  }
  .admin-settings-form > .admin-settings-save-top[hidden] {
    display: inline-block;
    grid-column: 1 / -1;
    justify-self: start;
    width: auto;
  }
  .admin-settings-form > button[type="submit"]:last-child { grid-column: 1 / -1; }

  .lead-action-row { display: flex; flex-wrap: wrap; align-items: center; }
  .lead-action-row > :is(a, button, form) { width: auto; flex: 0 1 auto; }
  .lead-action-row > form > button { width: auto; }

  thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--surface-2);
  }
  .table-wrapper:not(.products-table-wrap) thead th,
  .lp-tw thead th,
  .wa-tmpl-table-wrap thead th { position: static; }
  .products-table-wrap { overflow: visible; }
}
