/* YOKU ONE — this app's own layer, loaded after the shared static/style.css.
   style.css is the green stylesheet copied verbatim across the sibling Yoku
   webapps and MUST stay byte-identical to them so it keeps diffing cleanly,
   so every YOKU ONE-specific colour decision is a :root override here rather
   than an edit there.

   Contents: design tokens · the YOKU ONE brand lockup · the branch-facing
   (phone) screens. The admin console's shell lives in static/admin.css,
   loaded only by templates/admin/base_admin.html. */

/* ── Design tokens ─────────────────────────────────────────────────────────
   YOKU ONE carries its own identity instead of the shared green sibling
   apps use. These names are exactly the ones style.css declares, so every
   rule in that file (buttons, alerts, fields, cards) re-themes with no
   further edits.

   Values alias onto static/tokens.css (the canonical token layer — the One
   Platform palette; see DESIGN.md) rather than carrying their own
   literals — tokens.css is the single source of truth; this block just maps
   the legacy names onto it, and it re-themes automatically for both
   `[data-theme="light"]`/`[data-theme="dark"]` since it only ever points at
   variable names, never a literal value. A few names (--border, --danger,
   --success, --warning, --radius-sm, --radius-lg) exist in BOTH systems
   under the identical name, so they are deliberately left undeclared here:
   tokens.css's own :root already supplies the current value. */
:root {
  --primary: var(--primary-500);
  --primary-dark: var(--primary-600);
  --primary-light: var(--primary-100);
  --bg: var(--surface);
  --card: var(--surface-raised);
  --text: var(--ink);
  --muted: var(--ink-secondary);
  --danger-bg: var(--danger-tint);
  --warning-bg: var(--warning-tint);
  --radius: var(--radius-md);

  /* New tokens (style.css declares none of these) */
  --accent: var(--accent-500);
  --accent-bright: var(--aqua-400);   /* the wordmark's second half: One Platform aqua-400, not accent-500 (2026-10-05) */
  --success-bg: var(--success-tint);
  --info: var(--accent-700);
  --info-bg: var(--accent-100);
  --neutral-bg: var(--surface-sunken);

  /* VARIANT A ("One Platform", local-only): that source's shadow-card and
     shadow-raised — two levels, never stacked, real elevation instead of
     the flat border-only system this replaces for this branch. */
  --shadow-sm: 0 1px 2px rgba(15,27,45,0.04), 0 4px 16px rgba(15,27,45,0.06);
  --shadow-md: 0 1px 2px rgba(15,27,45,0.04), 0 4px 16px rgba(15,27,45,0.06);
  --shadow-lg: 0 8px 24px rgba(13,102,249,0.18);
  /* The evidence viewer (features/hub/static/hub-media.js, 2026-10-01) is a media
     surface: near-black in BOTH themes so a photo reads true, so these have no
     dark-mode counterpart on purpose. */
  --viewer-scrim: rgba(5, 8, 15, 0.72);  /* the page stays faintly visible behind (Kuang, 2026-10-01) */
  --viewer-ink: #ffffff;
  --viewer-control: rgba(255, 255, 255, 0.1);
  --viewer-control-hover: rgba(255, 255, 255, 0.2);
  --viewer-edge: rgba(255, 255, 255, 0.22);
  --ring: var(--focus);
  --sidebar-w: 244px;
  /* A flat brand-deep fill, not a multi-stop gradient; the bands that use it
     layer a soft radial highlight on top. The name is historical — kept
     because many rules already reference it. */
  --brand-gradient: var(--brand-deep);
}

/* VARIANT A dark-mode shadows — heavier black alpha, same two levels,
   matching One Platform's own dark-theme shadow-card/shadow-raised. */
[data-theme="dark"] {
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.40), 0 4px 16px rgba(0,0,0,0.32);
  --shadow-md: 0 1px 2px rgba(0,0,0,0.40), 0 4px 16px rgba(0,0,0,0.32);
  --shadow-lg: 0 8px 24px rgba(0,0,0,0.50);
}

/* The +2pt root bump and body's font/size moved to tokens.css (Fleet base)
   — they are identical in every app, and tokens.css is the file each app
   copies as-is. Defined once there, not here. */

.card {
  box-shadow: var(--shadow-sm);
  border: 1px solid var(--border);
}

/* Page rhythm (2026-09-25): .section carries its own bottom margin, but a
   bare card or table frame carries none, so whatever came next sat flush
   against it (a data-map heading, the routing page's two cards, the stock
   items table and the section after it). Same 1.5rem gap a section gets.
   :where() keeps this at zero specificity, so anything that sets its own
   top margin (a hint tucked under a filter bar) keeps it; block margins
   collapse, so a following .section's gap doesn't double. */
:where(.page-content) > :where(.card, .admin-table-wrap) + * {
  margin-top: 1.5rem;
}

/* ── App shell: sidebar + topbar + content ───────────────────────────────
   ONE responsive shell for every authenticated screen, branch and admin
   alike (2026-09-16) — replaces what used to be two separate shells: the
   branch side's fixed 480px column and the admin console's own desktop-
   first grid, previously duplicated in static/admin.css under .page-admin.
   .page stays a 480px column (style.css, byte-identical across the sibling
   apps) for pages that don't carry .page-app — the pre-login screens
   (login, pick-name is NOT one of these, admin/login, the 403 page) keep
   that simple centered layout by overriding {% block header %} to empty. */
.page.page-app {
  max-width: none;
  width: 100%;
  margin: 0;
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr);
  grid-template-areas:
    "side top"
    "side main";
  min-height: 100vh;
  /* Shared square-button size for every .app-icon-btn in the shell (help,
     bell, logout, the in-row desktop theme toggle). Declared here, on the
     shell's own top-level container, rather than on .app-topbar-actions
     directly, purely so a future .app-icon-btn elsewhere in the shell
     inherits it too without needing to sit inside that specific row.
     .mobile-theme-toggle does NOT use this — it isn't an .app-icon-btn and
     has its own fixed 34px sizing, see its own rule. */
  --topbar-action-size: clamp(
    32px,
    8vw,
    calc((0.85rem + 0.72rem) * 1.25 + 2 * (0.28rem + 1px))
  );
}

/* ── Sidebar ───────────────────────────────────────────────────────────────── */
.app-sidebar {
  grid-area: side;
  position: sticky;
  top: 0;
  align-self: start;
  height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--card);
  border-right: 1px solid var(--border);
  z-index: 6;
}

.app-brand {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 1.1rem 1.05rem 2rem;
  /* the nav below is the sidebar's own surface, not the page's */
  --wave-fill: var(--card);
  --wave-height: 36px;
  background:
    radial-gradient(115% 130% at 100% 0%, color-mix(in srgb, var(--on-brand-deep) 42%, transparent) 0%, transparent 60%),
    var(--brand-gradient);
  color: var(--on-brand-deep);
  text-decoration: none;
}

/* ── Wave divider (fleet component) ────────────────────────────────────────
   The layered wave edge between a brand-coloured band and whatever sits
   directly below it. Drawn as a MASK over a plain background-colour, not as
   a filled SVG background: the shape is one data-URI and the COLOUR is a
   token, so dark mode needs no second copy and a baked hex can no longer
   drift from the surface it is supposed to match (the old wave still said
   #e3eaf2 long after --surface-raised had moved).

   Three paths, all black — the mask reads their ALPHA, so the two low-alpha
   ones show as lighter ribbons over the band and the opaque one is the cut.

   Per use, set on the BAND (custom properties inherit into ::after):
     --wave-fill    whatever is directly below it — var(--surface) for a page
                    header, var(--card) for a sidebar or card-backed one.
     --wave-height  ~52px full-width, ~36px in a sidebar.
   preserveAspectRatio="none" squashes the shape, so amplitude only reads at
   the real height — judge it there, never in the viewBox. Give the band
   enough padding-bottom to clear the wave, or it eats its own text. Any new
   element can opt in with class="wave-divider"; the pseudo-element hooks are
   listed so every app's existing band picks it up unchanged. */
:root {
  --wave-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 120' preserveAspectRatio='none'%3E%3Cpath fill='%23000' fill-opacity='.18' d='M0 18c240 74 470 60 720 8s470-30 720 44V120H0Z'/%3E%3Cpath fill='%23000' fill-opacity='.3' d='M0 48c220-34 450 60 700 46s520-70 740 6V120H0Z'/%3E%3Cpath fill='%23000' d='M0 66c230 54 450-40 700-20s540 66 740 28V120H0Z'/%3E%3C/svg%3E")
    no-repeat center / 100% 100%;
}

.wave-divider,
.page-header::after,
.app-brand::after,
.app-band::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  pointer-events: none;
  height: var(--wave-height, 52px);
  background: var(--wave-fill, var(--surface));
  -webkit-mask: var(--wave-mask);
  mask: var(--wave-mask);
}

.app-brand .yo-wordmark {
  font-size: 1.02rem;
}

.app-nav {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.85rem 0.65rem 0.5rem;
  overflow-y: auto;
  /* The Hidden Scrollbar Rule (DESIGN.md, 2026-10-05): it scrolls, it never shows a scrollbar
     or scrolls sideways. In the 76px rail a scrollbar pushed every icon off the rail's centre. */
  overflow-x: hidden;
  scrollbar-width: none;
}

.app-nav::-webkit-scrollbar {
  display: none;
}

.app-nav-item {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.62rem 0.7rem;
  border-radius: var(--radius-sm);
  color: var(--text);
  text-decoration: none;
  font-family: var(--font-mitr);
  font-size: 0.9rem;
  font-weight: 400;
  line-height: 1.35;
}

.app-nav-item:hover {
  background: var(--neutral-bg);
}

.app-nav-icon {
  display: inline-flex;
  color: var(--muted);
  flex: 0 0 auto;
}

.app-nav-item.is-active {
  background: var(--primary-light);
  color: var(--brand-text);
}

.app-nav-item.is-active .app-nav-icon {
  color: var(--brand-text);
}

/* Feature-module nav: a group heading above a module's items, and a count
   pill (nav_badges) at the right edge of an item. Ported onto the unified
   .app-* shell from the hub merge, which had styled .ac-* in admin.css — the
   shell moved here, so these live with the rest of the nav rather than in
   admin.css, which now carries only page-accent overrides. */
/* Group labels are Thai ("ฝ่ายบริการลูกค้า (CS)", "สต็อกสาขา"): no uppercase and no
   tracking (the Thai Room Rule) — both do nothing for Thai and crowd its tone marks. */
.app-nav-group {
  margin: 0.85rem 0.7rem 0.3rem;
  font-family: var(--font-mitr);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--muted);
}

/* A labelled group folds (<details class="app-nav-fold">, base_admin.html). The heading
   is the summary: the same small muted label, now a full-width click target with a
   chevron that points down when open (it turns with the fold animation below). */
.app-nav-fold-head {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0.55rem 0 0.1rem;
  padding: 0.35rem 0.7rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  list-style: none;
  transition: color 120ms ease, background-color 120ms ease;
}

.app-nav-fold-head::-webkit-details-marker {
  display: none;
}

.app-nav-fold-head:hover {
  color: var(--text);
  background: var(--neutral-bg);
}

.app-nav-fold-head:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
}

.app-nav-chevron {
  display: inline-flex;
  margin-left: auto;
}

.app-nav-fold[open] .app-nav-chevron {
  transform: rotate(90deg);
}

/* Fold / unfold motion (Kuang asked for it, 2026-09-28; the second bounded exception to
   the Colour-Only Motion Rule). The group's height eases open and its items fade in; the
   chevron turns. Sidebar posture only (the pill row just snaps), only after the first
   frame (.app-nav-ready, set by the inline script, so a restored group never animates on
   page load), and never under reduced motion. Browsers without ::details-content or
   interpolate-size simply snap open, as before. */
@media (min-width: 981px) and (prefers-reduced-motion: no-preference) {
  .app-nav-ready .app-nav-fold {
    interpolate-size: allow-keywords;
  }

  .app-nav-ready .app-nav-fold::details-content {
    block-size: 0;
    opacity: 0;
    /* Clip the height animation only: hidden clipped sideways too and cut the rail's
       corner badges at the row's edge (Kuang, 2026-10-02). clip, unlike hidden, leaves
       the other axis visible. */
    overflow-x: visible;
    overflow-y: clip;
    transition: block-size 220ms ease, opacity 220ms ease, content-visibility 220ms allow-discrete;
  }

  .app-nav-ready .app-nav-fold[open]::details-content {
    block-size: auto;
    opacity: 1;
  }

  .app-nav-ready .app-nav-chevron {
    transition: transform 220ms ease;
  }
}

.app-nav-fold-items > .app-nav-item + .app-nav-item {
  margin-top: 0.15rem;
}

.app-nav-badge {
  margin-left: auto;
  flex: 0 0 auto;
  min-width: 1.5rem;
  padding: 0.05rem 0.45rem;
  border-radius: 999px;
  background: var(--primary);
  color: var(--on-primary, #fff);
  font-size: 0.7rem;
  font-weight: 700;
  line-height: 1.5;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.app-nav-item.is-active .app-nav-badge {
  background: var(--primary-dark);
}

/* Digits centred in the pill (Kuang, 2026-10-02). Prompt's line box keeps room below
   the baseline for Thai vowels, so centring the line put the digits ~1.5px high. Trim
   the box to the digits themselves (cap height to baseline) and pad it evenly. Where
   text-box isn't supported the rule above stays as it was. */
@supports (text-box: trim-both cap alphabetic) {
  .app-nav-badge {
    display: inline-block;
    line-height: 1;
    text-box: trim-both cap alphabetic;
    padding: 0.33rem 0.45rem;
  }
}

/* Over 99: the full count in the sidebar, 99+ in the rail (see html.nav-compact). */
.app-nav-badge-short {
  display: none;
}

/* A branch sub-page's way back to its parent page (← เช็คสต็อก, ← เลือกงาน):
   the first line of the content. Home needs no link — the sidebar is always there. */
.page-back {
  display: inline-block;
  margin-bottom: 0.75rem;
  font-size: 0.88rem;
  color: var(--brand-text);
  text-decoration: none;
}

.page-back:hover {
  text-decoration: underline;
}

/* ── Topbar ────────────────────────────────────────────────────────────────── */
.app-topbar {
  grid-area: top;
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding: 0.95rem 1.6rem;
  /* A frosted 92%-opacity version of --surface, not a hardcoded hex — was a
     literal light-mode-only rgba before, which would have sat as a pale
     near-white bar over the dark-mode page underneath it. */
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border);
}

/* min-width:0 lets a long Thai page title shrink instead of shoving the
   topbar actions off the right edge of the flex row. */
.app-topbar-head {
  min-width: 0;
}

.app-nav-label {
  min-width: 0;
}

.app-page-title {
  font-family: var(--font-mitr);
  font-size: 1.26rem;
  font-weight: 600; /* Prompt everywhere: a title stands apart by weight (2026-10-01). */
  letter-spacing: 0;
  color: var(--text);
  line-height: 1.3;
}

/* A page with nothing to add under its title (home) renders no empty line. */
.app-page-sub:empty { display: none; }

.app-page-sub {
  font-size: 0.85rem;
  color: var(--muted);
  margin-top: 0.12rem;
}

.app-topbar-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  /* Scales down with viewport width instead of a hard breakpoint jump —
     the row (theme toggle + help + bell + admin chip + logout) measured
     414px wide at a real 390px phone width, so logout sat 43px past the
     viewport edge with nothing to scroll it into view (root font-size is
     18.67px per the +2pt rule, so 0.55rem = 10.27px, not 8.8 — do the
     math off that, not a bare 16px assumption). 1.5vw wins in roughly the
     370-680px range; below that the 0.3rem floor holds, above it the
     0.55rem ceiling (the original fixed value) holds, so desktop is
     byte-identical to before. flex-wrap above is the backstop — if a
     long branch/admin name ever doesn't fit even at the floor, the row
     wraps to a second line instead of clipping off-screen again. */
  gap: clamp(0.3rem, 1.5vw, 0.55rem);
  /* --topbar-action-size lives on .page.page-app, not here — see the
     comment there for what the formula is tied to. */
}

.app-icon-btn {
  /* A true fixed square, sized off the shared --topbar-action-size above
     rather than a flex-stretch + aspect-ratio combo, which measured out
     to a 21px-wide sliver here — aspect-ratio doesn't reliably derive a
     flex item's main-axis size from a stretched cross-axis size. */
  width: var(--topbar-action-size);
  height: var(--topbar-action-size);
  border-radius: var(--radius-sm);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--card);
  border: 1px solid var(--border);
  color: var(--muted);
  text-decoration: none;
  position: relative;
}

.app-icon-btn:hover {
  color: var(--brand-text);
  border-color: var(--primary);
  background: var(--primary-light);
}

/* ── Content column ────────────────────────────────────────────────────────── */
.page-app .page-content {
  grid-area: main;
  padding: 1.4rem 1.6rem 3rem;
  max-width: 1340px;
  width: 100%;
}

.page-app .section {
  margin-bottom: 1.5rem;
}

.page-app .section-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--text);
  font-family: var(--font-mitr);
  font-size: 1.02rem;
  font-weight: 600; /* Prompt everywhere: a title stands apart by weight (2026-10-01). */
  margin-bottom: 0.75rem;
}

/* A readability cap for list-shaped content (notifications, the name
   picker) — the tile grid and admin tables use the full width instead. */
.content-narrow {
  max-width: 720px;
}

.page-app .field label {
  color: var(--text);
}

/* style.css (the byte-identical fleet sheet, not editable here) styles
   .field inputs by an explicit type whitelist — text, password, date — so
   any other type silently rendered as the browser default: the branch
   ticket form's phone field (type="tel") was a bare grey box (found
   2026-09-25). Same declarations as that rule, for the missing types. */
.field input[type="tel"],
.field input[type="email"],
.field input[type="number"],
.field input[type="search"],
.field input[type="url"],
.field input[type="time"] {
  width: 100%;
  padding: 0.75rem 1rem;
  font-size: 1rem;
  border: 2px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text);
  font-family: inherit;
}

.page-app .field input[type="text"],
.page-app .field input[type="password"],
.page-app .field input[type="date"],
.page-app .field input[type="tel"],
.page-app .field input[type="email"],
.page-app .field input[type="number"],
.page-app .field input[type="search"],
.page-app .field input[type="url"],
.page-app .field input[type="time"],
.page-app .field select,
.page-app .field textarea {
  /* Was background:var(--card) — identical to the surrounding .card, with
     only a faint var(--border) from the shared style.css rule to separate
     the two, so every form field app-wide read as barely-visible against
     its own card (found live on /admin/notify's "หัวข้อ" input, but the
     rule is global — every .field on every page inherited it). Full
     var(--border-strong) then read as too stark against var(--surface);
     toned down to a derived midpoint between --border and --border-strong
     — still a real visible boundary, without the harsh line. */
  border-width: 1.5px;
  border-color: var(--border-blend);
  border-radius: var(--radius-sm);
  background: var(--surface);
  font-size: 0.95rem;
}

.page-app .field input:disabled {
  background: var(--neutral-bg);
  color: var(--muted);
}

/* style.css's own :focus rule hardcodes background:white, which was fine
   while every field's base background was already white but would flash
   literal white over a dark-mode field on focus now that fields carry a
   real background of their own. */
.page-app .field select:focus,
.page-app .field input:focus,
.page-app .field textarea:focus {
  background: var(--surface);
}

/* The authenticated shell's second, mobile-only toggle (see
   app_shell_base.html) — hidden by default, shown fixed top-right of the
   viewport only below 980px (rule inside the @media block below), where
   it replaces the in-row one that .app-topbar-actions hides at the same
   breakpoint. MUST come before that @media block: both rules share the
   same specificity (one class each), so with equal specificity the LATER
   source-order rule always wins regardless of which media query matches
   — this default has to lose the cascade at narrow widths, not win it.
   Selector is `.notif-bell.mobile-theme-toggle`, not just the latter, for
   the same reason as the @media rule below: `.notif-bell` on its own sets
   `display: inline-flex` unconditionally, which would otherwise leak this
   button onto desktop too. */
.notif-bell.mobile-theme-toggle {
  display: none;
}

.app-burger,
.app-rail-row {
  display: none;
}

/* ── Desktop: the sidebar collapses to an icon rail (2026-10-01, Kuang) ──────
   A |< button on its own row at the top of the menu (>| in the rail) collapses it,
   saved per browser (localStorage "yo-nav-collapsed") and restored before the aside
   paints. Two classes on <html>, so the text never reflows while the width moves:
     .nav-collapsed  the WIDTH (76px rail); the column eases over 200ms after load.
     .nav-compact    the CONTENT: labels screen-reader-only (each link has a title
                     for the hover name), band text hidden, badges on the icon's
                     corner, group headings a thin divider with their chevron.
   Collapsing sets both at once (text goes, then the width shrinks). Expanding drops
   .nav-collapsed first and .nav-compact only once the width has grown, then fades
   the text in (.nav-reveal), so Thai labels never wrap mid-animation (the wobble
   Kuang saw). The icons sit at the same x in both states (28px: the rail's centre),
   so nothing hops sideways either. */
@media (min-width: 981px) {
  .app-rail-row {
    display: flex;
    justify-content: flex-end;
    padding: 0.5rem 0.65rem 0;
  }

  .app-rail-row + .app-nav {
    padding-top: 0.35rem;
  }

  /* In the rail the toggle slides to the rail's centre ((76 - 34) / 2 = 21px from the
     edge, the row pads 0.65rem) by transform, not by animating the row's padding. */
  html.nav-collapsed .app-rail-toggle {
    transform: translateX(calc(0.65rem - 21px));
  }

  .app-rail-toggle {
    display: inline-grid;
    place-items: center;
    width: 34px;
    height: 34px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--ink-secondary);
    cursor: pointer;
    transition: color 120ms ease, background-color 120ms ease;
  }

  .app-rail-toggle:hover {
    background: var(--neutral-bg);
    color: var(--ink);
  }

  .app-rail-collapse,
  .app-rail-expand {
    display: inline-flex;
  }

  .app-rail-expand,
  html.nav-collapsed .app-rail-collapse {
    display: none;
  }

  html.nav-collapsed .app-rail-expand {
    display: inline-flex;
  }

  html.nav-collapsed {
    --sidebar-w: 76px;
  }

  /* 0.65rem nav padding + 0.85rem item padding = 28px: the icon's x in the wide
     sidebar equals the rail's centre ((76 - 20) / 2), so it never moves. */
  .app-nav-item {
    position: relative;
    padding-left: 0.85rem;
    /* One row height with or without its label (label line + the item's padding), so
       the rows don't grow 3px each when the labels come back. */
    min-height: calc(0.9rem * 1.35 + 1.24rem);
  }

  html.nav-compact .app-brand > span,
  html.nav-compact .app-brand-text,
  html.nav-compact .yo-branch-user {
    display: none;
  }

  /* The band keeps its expanded height, so the wave sits on the same line in both
     states (measured by the shell's script into --brand-h). */
  html.nav-compact .app-brand {
    min-height: var(--brand-h, auto);
  }

  html.nav-compact .app-nav-label,
  html.nav-compact .app-nav-fold-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  html.nav-compact .app-nav-badge-full {
    display: none;
  }

  html.nav-compact .app-nav-badge-short {
    display: inline;
  }

  html.nav-compact .app-nav-badge {
    position: absolute;
    top: 2px;
    left: 27px; /* over the icon's top-right corner (the icon spans 16-36px in the item) */
    min-width: 1.15rem;
    margin: 0;
    padding: 0 0.3rem;
    font-size: 0.62rem;
  }

  @supports (text-box: trim-both cap alphabetic) {
    html.nav-compact .app-nav-badge {
      padding: 0.27rem 0.3rem;
    }
  }

  html.nav-compact .app-nav-fold-head {
    justify-content: center;
    margin: 0.45rem 0 0.1rem;
    padding: 0.4rem 0;
    border-top: 1px solid var(--border);
    border-radius: 0;
  }

  html.nav-compact .app-nav-chevron {
    margin-left: 0;
  }
}

/* The full-screen menu itself appears solid at once (fading the whole sheet let the
   page show through it for a beat, which read as a flash: Kuang, 2026-10-01); only
   the list inside eases in. */
@media (max-width: 980px) and (prefers-reduced-motion: no-preference) {
  html.nav-open .app-nav {
    animation: nav-list-in 160ms ease both;
  }

  @keyframes nav-list-in {
    from { opacity: 0; transform: translateY(-6px); }
  }

  /* ...and on the page it opens, the menu rolls up into the band while its list fades,
     revealing the page. The band itself never fades: fading the whole sheet took the
     blue band to white for a beat before it snapped back (Kuang, 2026-10-01). It ends
     clipped to exactly the band's height, where the in-flow band takes over. */
  html.nav-exit-go .app-sidebar {
    animation: nav-sheet-up 160ms cubic-bezier(0.4, 0, 0.2, 1) forwards; /* 260 until Kuang asked for faster */
  }

  html.nav-exit-go .app-nav {
    animation: nav-list-out 110ms ease forwards;
  }

  @keyframes nav-sheet-up {
    from { clip-path: inset(0 0 0 0); }
    to { clip-path: inset(0 0 calc(100% - var(--closed-side-h, 0px)) 0); }
  }

  @keyframes nav-list-out {
    to { opacity: 0; }
  }
}

@media (min-width: 981px) and (prefers-reduced-motion: no-preference) {
  html:has(.app-nav-ready) .page.page-app {
    transition: grid-template-columns 200ms ease;
  }

  html:has(.app-nav-ready) .app-rail-toggle {
    transition: color 120ms ease, background-color 120ms ease, transform 200ms ease;
  }

  html.nav-reveal .app-nav-label,
  html.nav-reveal .app-nav-fold-label,
  html.nav-reveal .app-nav-badge,
  html.nav-reveal .app-nav-chevron,
  html.nav-reveal .app-brand > span,
  html.nav-reveal .app-brand-text,
  html.nav-reveal .yo-branch-user {
    animation: nav-reveal 160ms ease both;
  }

  @keyframes nav-reveal {
    from { opacity: 0; }
  }
}

/* ── Responsive: the sidebar becomes a scrollable top nav row ───────────────
   Below 980px the grid collapses to one column — brand + nav first, then the
   page's own topbar, then the content. Nothing overflows the viewport: the
   nav row scrolls horizontally, and every table scrolls inside its own wrap.
   This is the one breakpoint every authenticated page shares now. */
@media (max-width: 980px) {
  .page.page-app {
    grid-template-columns: minmax(0, 1fr);
    /* Three explicit row tracks, matching the three stacked areas below —
       without this the collapsed layout still inherited the desktop rule's
       TWO tracks (auto, minmax(0,1fr)) for what are now three named-area
       rows, and the browser filled the row Grid couldn't otherwise size
       with a large implicit gap between the nav and the topbar. Found via
       a real mobile-width screenshot, not visible at any width ≥980px. */
    grid-template-rows: auto auto minmax(0, 1fr);
    grid-template-areas:
      "side"
      "top"
      "main";
  }

  .app-sidebar {
    /* relative, not static — .mobile-theme-toggle (inside .app-brand's
       sibling position, see app_shell_base.html) anchors to this box via
       position:absolute, so it scrolls away with the sidebar instead of
       staying pinned to the viewport. */
    position: relative;
    height: auto;
    border-right: none;
    border-bottom: 1px solid var(--border);
  }

  .app-brand {
    padding: 0.85rem 1rem 1.4rem;
  }

  /* The menu is folded away under a burger (2026-10-01, Kuang; it was one sideways-
     scrolling pill row). Open, the whole sidebar becomes a full-screen menu over the
     page: the brand band with who/where on top (the burger now an ×), then the same
     vertical list as the desktop sidebar, group headings and all; the page behind
     stops scrolling. Choosing an item navigates (a full page load closes it). */
  .app-nav {
    display: none;
    padding: 0.5rem 0.75rem 0.85rem;
  }

  html.nav-open .app-nav {
    display: flex;
    padding: 0.75rem 1rem 2.5rem;
    gap: 0.25rem;
  }

  .app-nav-item {
    padding: 0.7rem 0.75rem;
  }

  html.nav-open .app-sidebar {
    position: fixed;
    inset: 0;
    z-index: 60; /* over the floating chat bubble (41/42) and the preview pill (40) */
    height: 100dvh;
    overflow-y: auto;
    overscroll-behavior: contain;
    border-bottom: 0;
  }

  html.nav-open,
  html.nav-open body {
    overflow: hidden;
  }

  /* Locking the page behind the menu removed the window's scrollbar, so the page
     widened by its width and re-laid out (the dashboard cards jumped 1 to 2 columns
     and back: Kuang, 2026-10-01). Reserving the gutter keeps the width constant. */
  html {
    scrollbar-gutter: stable;
  }

  /* The sidebar is out of flow while the menu is up; the topbar takes its height so the
     page underneath already sits where it will once the menu is gone. */
  html.nav-open .app-topbar {
    margin-top: var(--closed-side-h, 0px);
  }

  /* Arriving with the menu up (nav-exit): the list is already in place, no list ease. */
  html.nav-exit .app-nav {
    animation: none;
  }

  html.nav-open .app-nav-item {
    padding: 0.85rem 0.9rem;
    font-size: 1rem;
  }

  /* The burger: the band's top-right corner, the theme toggle moves one step left. */
  .app-burger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: 0.85rem;
    right: 1rem;
    z-index: 2;
    width: 34px;
    height: 34px;
    padding: 0;
    border-radius: var(--radius-sm);
    color: var(--on-brand-deep);
    background: color-mix(in srgb, var(--on-brand-deep) 14%, transparent);
    border: 1px solid color-mix(in srgb, var(--on-brand-deep) 22%, transparent);
    cursor: pointer;
  }

  .app-burger:hover {
    background: color-mix(in srgb, var(--on-brand-deep) 24%, transparent);
  }

  .app-burger:focus-visible {
    outline-color: var(--on-brand-deep);
  }

  /* inline-flex (not a bare inline <span>), so the icon centres in the button rather
     than sitting on the text baseline, which had pushed the three lines off-centre. */
  .app-burger-open,
  .app-burger-close {
    display: inline-flex;
    line-height: 0;
  }

  .app-burger-close,
  html.nav-open .app-burger-open {
    display: none;
  }

  html.nav-open .app-burger-close {
    display: inline-flex;
  }

  .app-topbar {
    position: static;
    padding: 0.9rem 1rem;
  }

  /* Hide the in-row toggle below 980px — .mobile-theme-toggle (a sibling
     of .app-brand inside .app-sidebar, see app_shell_base.html) replaces
     it. */
  .app-topbar-actions .theme-toggle-btn {
    display: none;
  }

  /* Sits over the brand/wave block, absolutely positioned within
     .app-sidebar (not fixed to the viewport — see that rule's comment).
     top/right match .app-brand's own mobile padding just above, so it
     lines up with the block's edge instead of floating at an arbitrary
     offset. Styled after .notif-bell (the plain pre-shell header's own
     toggle) rather than .app-icon-btn: a muted on-brand-deep tint that
     sits quietly on the gradient, not a solid opaque chip standing out
     against it — reported directly from a side-by-side screenshot
     comparison, .notif-bell was the requested reference. Fixed 34x34px
     size (matching .notif-bell exactly) rather than --topbar-action-size:
     this button no longer needs to height-match the topbar row's chip,
     since it isn't in that row anymore.

     Selector is `.notif-bell.mobile-theme-toggle`, not just the latter:
     the button also carries the plain `.notif-bell` class (for its base
     colour/size role), and that class's OWN rule sets `position: relative`
     further down this file. Same specificity, later source order — it was
     winning over this `position: absolute` regardless of which media query
     matched, and the button rendered as an in-flow relatively-positioned
     element instead of an overlay, with its `right: 1rem` offset shoving
     it left off the edge of the sidebar (confirmed by screenshot: a stray
     sliver at x≈0, not a button at top-right). Two classes beats one
     regardless of source order, so this can't drift again the same way. */
  .notif-bell.mobile-theme-toggle {
    display: inline-flex;
    position: absolute;
    top: 0.85rem;
    right: calc(1rem + 34px + 0.5rem); /* one step left of the burger */
    z-index: 2;
    width: 34px;
    height: 34px;
    color: var(--on-brand-deep);
    background: color-mix(in srgb, var(--on-brand-deep) 14%, transparent);
    border: 1px solid color-mix(in srgb, var(--on-brand-deep) 22%, transparent);
  }

  .notif-bell.mobile-theme-toggle:hover {
    background: color-mix(in srgb, var(--on-brand-deep) 24%, transparent);
  }

  .page-app .page-content {
    padding: 1.1rem 1rem 2.5rem;
  }
}

@media (max-width: 560px) {
  .app-page-title { font-size: 1.12rem; }
}

/* ── Focus visibility ──────────────────────────────────────────────────────
   Everything interactive gets the same visible ring. :focus-visible (not
   :focus) so a mouse click never paints a ring, but keyboard traversal
   always does. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid var(--ring);
  outline-offset: 2px;
  border-radius: var(--radius-md);
}

/* On the gradient header the blue ring would disappear into the background. */
.page-header a:focus-visible,
.page-header button:focus-visible {
  outline-color: var(--on-brand-deep);
}

/* ── Brand lockup ──────────────────────────────────────────────────────────
   Drawn in templates/partials/icons.html (brand_mark + wordmark macros) and
   coloured here. The default styling is for a DARK background — the branch
   header and the admin sidebar's brand block are both the brand-deep field.
   Every white/rgba(255,255,255,X) below is on-brand-deep at that same
   opacity via color-mix(), not a literal white — on-brand-deep flips to a
   dark ink in dark mode (brand-deep itself flips light there), so a literal
   white would go invisible against a light field. */
.yo-brand {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  text-decoration: none;
  color: inherit;
}

.yo-mark {
  flex: 0 0 auto;
  vertical-align: middle;
}

.yo-wordmark {
  display: inline-flex;
  align-items: baseline;
  gap: 0.32em;
  font-weight: 800;
  letter-spacing: 0.08em;
  line-height: 1.1;
  white-space: nowrap;
}

.yo-word-yoku { color: var(--on-brand-deep); }
.yo-word-one { color: var(--accent-bright); }

/* No letter-spacing: every tag is Thai (คอนโซลผู้ดูแลระบบ, a branch name) — the Thai
   Room Rule. It carried 0.02em until 2026-09-29. */
.yo-brand-tag {
  display: block;
  font-size: 0.68rem;
  font-weight: 600;
  color: color-mix(in srgb, var(--on-brand-deep) 78%, transparent);
  margin-top: 0.1rem;
}

/* ── Identity slot: the branch shell's who/where ───────────────────────────
   The Identity Slot Rule (DESIGN.md, Kuang 2026-09-29): every shell's sidebar
   header says who and where, in the slot the console uses for its role line.
   A branch shows its name, "<brand> · <code>", then "ผู้ใช้: <name> · เปลี่ยน"
   (templates/branch_base.html). Same band, lockup and .yo-brand-tag lines as the
   console; the block is a <div> holding the home link rather than one <a>,
   because the user line carries a link of its own and links cannot nest. */
.app-brand.app-brand-id {
  flex-direction: column;
  align-items: stretch;
  gap: 0.35rem;
}

.app-brand-home {
  display: flex;
  align-items: flex-start;
  gap: 0.65rem;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}

/* min-width:0 lets the column shrink inside the flex row, so a long name wraps
   instead of pushing the band wider than the sidebar. */
.app-brand-text {
  min-width: 0;
  padding-top: 0.2rem;
}

/* Thai wraps at word boundaries; overflow-wrap is the backstop for a run with
   no break opportunity at all (a long code, a name typed without spaces). */
.yo-branch-where {
  display: block;
  margin-top: 0.3rem;
  overflow-wrap: anywhere;
}

/* The branch name is the line a person checks, so it reads at full strength and
   a step larger than the tag lines under it. */
.yo-brand-tag.yo-branch-name {
  font-size: 0.8rem;
  color: var(--on-brand-deep);
}

/* The user line: outside the home link, indented to the text column above it
   (the 36px mark + the link's gap) so the three lines read as one column. */
.yo-branch-user {
  margin: 0 0 0 calc(36px + 0.65rem);
  overflow-wrap: anywhere;
}

.yo-branch-change {
  white-space: nowrap;
}

/* The negative margin grows the hit area to about 26px tall without moving
   the line; the underline says "link" on a band where blue text cannot. */
.yo-branch-user a {
  display: inline-block;
  padding: 0.3rem 0.25rem;
  margin: -0.3rem -0.25rem;
  color: var(--on-brand-deep);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* On the band a blue ring would disappear, as on .page-header. */
.app-brand-id a:focus-visible {
  outline-color: var(--on-brand-deep);
}

/* The pill-row posture keeps the same three lines as the desktop — name, then
   "<brand> · <code>", then the user line — so the branch reads the same on every
   screen (Kuang, 2026-09-30, UAT A2; until then the phone dropped the brand and
   ran the name and the code together on one line). The right padding keeps every
   line clear of the theme toggle pinned in the band's top-right corner (34px at a
   1rem inset); a long name wraps inside that column. */
@media (max-width: 980px) {
  /* The bottom padding is a little deeper than the plain band's 1.4rem: the user
     line is the last line here and otherwise sits on the wave's first ribbon. */
  .app-brand.app-brand-id {
    padding-right: calc(1rem + 2 * 34px + 1rem); /* clear of the theme toggle AND the burger */
    padding-bottom: 1.8rem;
    gap: 0.25rem;
  }

  .app-brand-id .yo-branch-where {
    margin-top: 0.2rem;
  }
}

/* ── Branch header: gradient + wave ────────────────────────────────────────
   .page-header keeps its structure from style.css (sticky, h1 + subtitle);
   only its surface changes. The wave is the shared .wave-divider component
   above, at its default --wave-fill (the page background), so the content
   appears to rise into the header rather than sit under a hard edge. */
.page-header {
  background:
    radial-gradient(115% 135% at 100% 0%, color-mix(in srgb, var(--on-brand-deep) 40%, transparent) 0%, transparent 58%),
    var(--brand-gradient);
  /* style.css hardcodes color:white here — right for a brand-deep field
     that's always dark, wrong once brand-deep flips light in dark mode. */
  color: var(--on-brand-deep);
  padding-bottom: 2.2rem;
}

.page-header h1 {
  font-size: 1.15rem;
}

.page-header .subtitle {
  opacity: 0.9;
}

.header-logout {
  border-color: color-mix(in srgb, var(--on-brand-deep) 45%, transparent);
  border-radius: var(--radius-sm);
  font-weight: 600;
  backdrop-filter: blur(2px);
}

/* ── Header extras ─────────────────────────────────────────────────────────── */
.header-actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-shrink: 0;
}

.notif-bell {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  color: var(--on-brand-deep);
  text-decoration: none;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--on-brand-deep) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--on-brand-deep) 22%, transparent);
}

.notif-bell:hover {
  background: color-mix(in srgb, var(--on-brand-deep) 24%, transparent);
}

/* Dark-mode toggle — reuses .app-icon-btn (authenticated shell topbar) or
   .notif-bell (plain pre-login header, sits on brand-deep) for its size/
   colour/border; this class only resets native <button> chrome and swaps
   which of the two icons shows. The icon shown is what you'll SWITCH TO,
   not the current state — a moon in light mode ("go dark"), a sun in dark
   mode ("go light"). */
.theme-toggle-btn {
  appearance: none;
  font: inherit;
  cursor: pointer;
}

.theme-icon-sun,
.theme-icon-moon {
  display: inline-flex;
}

[data-theme="light"] .theme-icon-sun { display: none; }
[data-theme="dark"] .theme-icon-moon { display: none; }

/* The 3 pages with no header at all (login, admin/login, admin/denied) —
   floats top-right of the viewport since there's no topbar/gradient header
   to sit inside on these. */
.theme-toggle-fixed {
  position: fixed;
  top: 1rem;
  right: 1rem;
  z-index: 20;
}

.notif-badge {
  position: absolute;
  top: -5px;
  right: -6px;
  background: var(--danger-solid); /* was a literal #f54a45 — off-token, no dark-mode value, and white text on
     it measured 3.53:1 (fails WCAG AA's 4.5:1 floor at this badge's 0.65rem bold size). --danger itself flips to
     a pale salmon in dark mode (a text-on-surface value, not a fill), so this badge needs the separate fixed
     --danger-solid role instead — same fix applied to .btn-danger below (2026-09-25 design audit). */
  color: var(--on-danger-solid);
  border-radius: var(--radius-sm);
  font-size: 0.65rem;
  font-weight: 700;
  min-width: 17px;
  height: 17px;
  line-height: 17px;
  text-align: center;
  padding: 0 4px;
  /* A plain surface-colour notch, not the old blue-tinted ring: this badge
     used to sit only on the dark gradient header's .notif-bell button; now
     it also sits on the light .app-icon-btn in the topbar (2026-09-16), so
     the ring needs to read as "cut into whatever it's on" either way. */
  box-shadow: 0 0 0 2px var(--card);
}

.notif-badge-inline {
  position: static;
  display: inline-block;
  margin-left: 0.4rem;
  vertical-align: middle;
  box-shadow: none;
}

/* ── Greeting ──────────────────────────────────────────────────────────────── */
.greeting-block {
  margin: 0.25rem 0 1.1rem;
}

.greeting-hello {
  font-family: var(--font-mitr);
  font-size: 1.3rem;
  font-weight: 600; /* Prompt everywhere: a title stands apart by weight (2026-10-01). */
  letter-spacing: 0;
}


/* ── Tile grid ─────────────────────────────────────────────────────────────
   Markup is templates/partials/tile_grid.html — ONE copy, shared by the
   branch home and the admin preview. Only the surface is restyled here.
   Fixed 2 columns below 700px — byte-identical to the pre-2026-09-16 phone
   behaviour, on purpose: auto-fill's minmax() was tried first and actually
   collapsed to ONE column at a real 390px phone width once gaps are
   subtracted, which is a regression from "always exactly two on phone."
   Explicit breakpoints step the column count up as real desktop space
   becomes available instead. The admin preview's own .preview-phone wrapper
   caps at 480px regardless, so it always renders the same 2-column view a
   branch actually sees. */
.tile-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.75rem;
  margin-bottom: 1.25rem;
}

@media (min-width: 700px) {
  .tile-grid { grid-template-columns: repeat(3, 1fr); }
}

@media (min-width: 1000px) {
  .tile-grid { grid-template-columns: repeat(4, 1fr); }
}

@media (min-width: 1300px) {
  .tile-grid { grid-template-columns: repeat(5, 1fr); }
}

.tile-card {
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: 1rem 0.9rem 1.05rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  text-decoration: none;
  color: var(--text);
  border: 1px solid var(--border);
  transition: border-color .15s, box-shadow .15s, transform .1s;
}

.tile-card:active {
  transform: scale(0.98);
}

.tile-card:hover {
  border-color: var(--primary);
  box-shadow: var(--shadow-md);
}

/* The emoji sits in a rounded chip tinted with the brand color rather than
   the original pale --primary-light wash — that read as generic on every
   tile alike. A full solid primary-500 fill then read as too much
   contrast against the card; blended down toward --card for a softer,
   still-branded tint instead of a flat saturated block. */
.tile-icon {
  font-size: 1.35rem;
  line-height: 1;
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-md);
  background: var(--icon-chip-bg);
  margin-bottom: 0.35rem;
}

.tile-title {
  font-family: var(--font-mitr);
  font-weight: 600; /* Prompt everywhere: a title stands apart by weight (2026-10-01). */
  font-size: 0.95rem;
  line-height: 1.35;
}

.tile-sub {
  font-size: 0.78rem;
  color: var(--muted);
}

/* ── Notifications ─────────────────────────────────────────────────────────── */
.notif-page-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.6rem;
  flex-wrap: wrap;
}

.notif-list {
  list-style: none;
}

.notif-item {
  position: relative;
  padding: 0.8rem 0.9rem;
  border-bottom: 1px solid var(--border);
}

/* Unread = Blue Mist (DESIGN.md, 2026-10-01): the faintest tint, with the dot and a
   bold title still saying it in more than colour; no left-edge bar (No-Rail). The
   rows round off so the tint does not run into the card's edge. */
.notif-unread {
  background: var(--primary-50);
  border-radius: var(--radius-md);
}
/* A linked row opens from anywhere on it: the เปิดดู link is stretched over the
   row, and the row's own button sits above that layer. */
/* The link shows only as a › at the row's right edge (2026-10-01: the row itself is
   the target, so the words เปิดดู → were dropped); the row keeps room for it. */
.notif-item.is-linked { padding-right: 2.6rem; }
.notif-item.is-linked .notif-link { color: var(--ink-secondary); }
.notif-chev {
  position: absolute;
  right: 0.8rem;
  top: 50%;
  transform: translateY(-50%);
  display: grid;
}
.notif-item.is-linked:hover .notif-link { color: var(--brand-text); }
.notif-item.is-linked .notif-link::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius-md);
}
.notif-item.is-linked:hover { background: var(--surface-sunken); border-radius: var(--radius-md); }
.notif-unread.is-linked:hover { background: var(--primary-100); }
.notif-item.is-linked .notif-link:focus-visible { outline: none; }
.notif-item.is-linked:has(.notif-link:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: var(--radius-md); }
.notif-actions form { position: relative; z-index: 1; }
/* No link but unread: a click on the row marks it read (templates/notifications.html
   submits the row's own read form; a stretched button does not take clicks in Chrome). */
.notif-item.is-markable { cursor: pointer; }
/* Desktop: the type and time sit top-right beside the title, so a row is a line
   shorter; the phone keeps them under the body (Kuang, 2026-10-01). */
@media (min-width: 700px) {
  .notif-item {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "title meta" "body body" "actions actions";
    column-gap: var(--space-16);
  }
  .notif-item > .notif-title { grid-area: title; }
  .notif-item > .notif-body { grid-area: body; }
  .notif-item > .notif-meta { grid-area: meta; margin-top: 0.1rem; text-align: right; white-space: nowrap; }
  .notif-item > .notif-actions { grid-area: actions; justify-self: start; }
}
.notif-unread.is-markable:hover { background: var(--primary-100); }
.notif-unread .btn-outline { background: var(--card); }  /* No Tint-on-Tint */

.notif-item:last-child {
  border-bottom: none;
}

.notif-unread .notif-title::before {
  content: "●";
  color: var(--brand-text);
  margin-right: 0.4rem;
  font-size: 0.7rem;
  vertical-align: middle;
}

.notif-title {
  font-weight: 600;
  font-size: 0.92rem;
}

.notif-body {
  font-size: 0.85rem;
  color: var(--text);
  margin-top: 0.15rem;
  white-space: pre-line;
}

.notif-meta {
  font-size: 0.75rem;
  color: var(--muted);
  margin-top: 0.2rem;
}

.notif-actions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: 0.45rem;
  flex-wrap: wrap;
}

.notif-link {
  color: var(--brand-text);
  font-size: 0.85rem;
  text-decoration: none;
  font-weight: 600;
}

.notif-more {
  margin-top: 0.6rem;
  text-align: center;
}

.notif-more a {
  color: var(--brand-text);
  font-size: 0.85rem;
  font-weight: 600;
  text-decoration: none;
}

.notif-empty {
  color: var(--muted);
  text-align: center;
  padding: 0.9rem 0;
}

/* ── Buttons (shared by both shells) ───────────────────────────────────────── */
.btn {
  border-radius: var(--radius-sm);
  /* <button> (.btn-primary) computes line-height:normal by default, while
     <a> (.btn-outline) inherits the body's 1.5 line-height — same padding,
     different tag, so they rendered at different total heights. Pin it
     explicitly so both compute the same regardless of element type. */
  line-height: 1;
  /* <button> elements don't inherit the body's font-family by default —
     browsers give form controls the platform UI font instead, so every
     .btn (กรอง, ส่งแจ้งเตือน, ดูหน้าจอสาขานี้, ...) was silently rendering
     off-system instead of Noto Sans Thai. <a>.btn-outline never had this
     problem (anchors inherit normally); this fixes it for both tags. */
  font-family: inherit;
}

.btn-primary {
  /* Matches .btn-outline's border-width below with a transparent border,
     not just a color swap — the shared .btn base has border:none, so
     without this the outline button's real 1.5px border made it visibly
     taller than the borderless primary button sitting right next to it. */
  border: 1.5px solid transparent;
  /* A hardcoded box-shadow (the OLD primary blue, #1d6fe8, as a literal
     rgba) had survived every earlier token pass on this system, despite
     the system's own Elevation rule being "zero drop shadows anywhere" —
     flagged as a known gap earlier and fixed here: removed outright rather
     than re-tinted to the new palette, matching the rule for real. */
  /* style.css hardcodes white text on .btn-primary — real bug found via a
     dark-mode screenshot: primary-500 flips to a LIGHT cyan in dark mode,
     so white text on it went nearly illegible. var(--on-primary) flips
     with it (white in light mode, dark ink in dark mode). */
  color: var(--on-primary);
}

.btn-outline {
  border-width: 1.5px;
  /* One Platform's secondary Button: the outline is the grey control border
     (border-strong, 3:1), the label stays brand blue (2026-09-25). style.css
     gave it a brand-blue border, which the design system reserves for the
     one primary action. */
  border-color: var(--border-strong);
  background: var(--card);
  /* The label is blue TEXT, so brand-text (the design system's rule), not the fill blue
     style.css gives it — that measured 3.76:1 on the dark card, below 4.5:1. */
  color: var(--brand-text);
}

.btn-outline:hover {
  background: var(--primary-light);
}

.btn-small {
  padding: 0.42rem 0.85rem;
  font-size: 0.82rem;
  width: auto;
  border-radius: var(--radius-lg);
}

/* The "Choose Files" button inside every <input type="file"> (2026-09-25).
   The browser draws it as a grey native button no stylesheet touched, so it
   sat inside our dashed drop box looking like a different app. Styled as
   the .btn-outline .btn-small pair above, so it reads as the same outline
   action as its neighbours; same rule in the One Platform design system,
   under FileDrop. The input itself keeps the focus ring — the pseudo-element
   can't take focus on its own. */
input[type="file"]::file-selector-button {
  margin-right: 0.75rem;
  padding: 0.42rem 0.85rem;
  font: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  line-height: 1.2;
  color: var(--brand-text);
  background: var(--card);
  border: 1.5px solid var(--border-strong);
  border-radius: var(--radius-lg);
  cursor: pointer;
  transition: background-color 0.12s ease;
}

input[type="file"]:hover::file-selector-button {
  background: var(--primary-light);
}

input[type="file"]:disabled::file-selector-button {
  opacity: 0.45;
  cursor: not-allowed;
}

.btn-block {
  width: 100%;
}

.btn .ic {
  flex: 0 0 auto;
}

/* ── Login / name picker ───────────────────────────────────────────────────── */
/* The tint is a viewport-fixed layer, not a background on .login-wrapper:
   .page caps at 480px, so a background painted on the wrapper stopped at the
   container edge and left a hard-edged rectangle on a desktop screen. On a
   phone the same edge was only a thin inset (the .page-content padding),
   which is why it went unnoticed there. */
.login-wrapper {
  background: transparent;
  /* style.css's min-height:100vh + justify-content:center vertically centres
     .login-wrapper's own content as a standalone unit — correct when the
     brand lockup lived INSIDE it, wrong now that .login-hero is a sibling
     block ABOVE it taking real vertical space: centring an already-shifted-
     down block within another 100vh would push the card off-screen. */
  min-height: 0;
  justify-content: flex-start;
}

.login-wrapper::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(90% 60% at 50% 0%, color-mix(in srgb, var(--primary-500) 12%, transparent) 0%, transparent 70%);
}

/* The tint above was the only colour on the old plain sign-in page; under a
   .login-hero it just leaks out below the band as a faint smudge. Screens
   without the hero (name picker, admin/denied) keep it. */
.login-hero + .login-wrapper::before {
  display: none;
}

.login-card {
  /* Un-boxed 2026-09-25 (design feedback): the fields float directly on
     the page instead of sitting inside a separate white card — each field
     already carries its own border (style.css's .field input rule), so
     nothing here needs a container to stay legible. */
  background: transparent;
  box-shadow: none;
  border: none;
  padding: 0;
}

.login-title {
  letter-spacing: -0.01em;
}

/* ── Login hero (2026-09-25, design feedback: "blue wave flow throughout
   the top of the screen, YOKU ONE name big in the center", replacing a
   small floating logo chip on an otherwise plain page) ─────────────────────
   Full-bleed gradient band — the exact brand-gradient + wave-cut language
   already used by .page-header/.app-brand, just taller and centred, so the
   wordmark reads as the screen's identity rather than a corner logo.
   Breaks out of .page's 480px cap with left:50% + translateX(-50%) — the
   transform shifts by the ELEMENT'S OWN width (100vw), not the containing
   block's, which the more common left/margin-only version of this trick
   conflates; that distinction is exactly what broke here the first time
   (as a flex item of the centring .login-wrapper, caught live in a
   screenshot — sitting as a plain block child of .page-content instead).
   .login-card/.login-alt stay inside the normal capped column below it. */
.login-hero {
  position: relative;
  left: 50%;
  /* Cancels .page-content's own 1rem top padding (style.css, the byte-
     identical fleet file — never overridden globally) so the hero sits
     flush against the very top of the viewport instead of leaving a thin
     gap above it. Only .login-hero opts out; every other page keeps that
     padding. */
  margin-top: -1rem;
  width: 100vw;
  transform: translateX(-50%);
  margin-bottom: 0.5rem;
  /* Bottom padding sets where the wave's lower edge lands: tuned by eye
     against an annotated screenshot (2026-09-25) so the band ends just
     above the first field label. */
  padding: 3rem 1.5rem 4rem;
  text-align: center;
  background:
    radial-gradient(115% 135% at 100% 0%, color-mix(in srgb, var(--on-brand-deep) 40%, transparent) 0%, transparent 58%),
    var(--brand-gradient);
}

.login-hero-mark {
  margin: 0 auto 1rem;
}

.login-hero-name {
  margin: 0;
  font-size: 2.15rem;
}

.login-hero-name .yo-wordmark {
  justify-content: center;
}

.login-hero-sub {
  margin: 0.6rem auto 0;
  max-width: 320px;
  color: var(--on-brand-deep);
  opacity: 0.85;
}

/* A moving wave, on the sign-in screens only (2026-09-25) — same technique
   as yoku-cs-case-intake's delivery_form.html. Every other wave in this app
   is one static shape whose ends don't meet, so sliding it would show a
   seam; these paths are tileable (each ends at the height and slope it
   starts with) and repeat-x, so shifting the mask by exactly one tile loops
   with no join. Two layers drifting opposite ways at different speeds read
   as a rolling swell: ::before carries the two faint ribbons, ::after the
   opaque edge the page rises into. Drawn as a MASK over var(--surface)
   rather than a baked-hex fill, so the colour tracks the token in both
   themes with no dark-mode copy — the stale-hex seam fixed on the sidebar
   wave earlier today cannot happen here. */
.login-hero::before,
.login-hero::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 70px;
  pointer-events: none;
  background: var(--surface);
}

.login-hero::before {
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 120' preserveAspectRatio='none'%3E%3Cpath fill='%23000' fill-opacity='.18' d='M0 45C240 -15 480 -15 720 45S1200 105 1440 45V120H0Z'/%3E%3Cpath fill='%23000' fill-opacity='.3' d='M0 70C240 115 480 115 720 70S1200 25 1440 70V120H0Z'/%3E%3C/svg%3E") repeat-x 0 0 / 960px 100%;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 120' preserveAspectRatio='none'%3E%3Cpath fill='%23000' fill-opacity='.18' d='M0 45C240 -15 480 -15 720 45S1200 105 1440 45V120H0Z'/%3E%3Cpath fill='%23000' fill-opacity='.3' d='M0 70C240 115 480 115 720 70S1200 25 1440 70V120H0Z'/%3E%3C/svg%3E") repeat-x 0 0 / 960px 100%;
  animation: login-wave-left 10s linear infinite;
}

.login-hero::after {
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 120' preserveAspectRatio='none'%3E%3Cpath fill='%23000' d='M0 88C240 52 480 52 720 88S1200 124 1440 88V120H0Z'/%3E%3C/svg%3E") repeat-x 0 0 / 1200px 100%;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 120' preserveAspectRatio='none'%3E%3Cpath fill='%23000' d='M0 88C240 52 480 52 720 88S1200 124 1440 88V120H0Z'/%3E%3C/svg%3E") repeat-x 0 0 / 1200px 100%;
  animation: login-wave-right 15s linear infinite;
}

@keyframes login-wave-left  { to { -webkit-mask-position: -960px 0; mask-position: -960px 0; } }
@keyframes login-wave-right { to { -webkit-mask-position: 1200px 0;  mask-position: 1200px 0; } }

@media (prefers-reduced-motion: reduce) {
  .login-hero::before,
  .login-hero::after { animation: none; }
}

/* HQ's way in from the branch sign-in page (2026-09-16). Matches .login-card's
   360px so the column stays aligned, and stays an outline button because
   branch staff — not admins — are the main audience of this screen. */
.login-alt {
  width: 100%;
  max-width: 360px;
  margin-top: 1rem;
  text-align: center;
}

.login-alt-hint {
  margin-top: 0.5rem;
}

.name-pick-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}

.name-pick-btn {
  width: 100%;
}

.name-pick-or {
  text-align: center;
  color: var(--muted);
  font-size: 0.8rem;
  margin: 0.5rem 0 0.75rem;
}

/* ── Status pills ──────────────────────────────────────────────────────────
   Used by the admin tables. Every pill keeps its Thai/English wording, so
   colour is never the only carrier of meaning. */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  border-radius: var(--radius-sm);
  padding: 0.18rem 0.62rem;
  font-size: 0.75rem;
  font-weight: 700;
  white-space: nowrap;
  line-height: 1.5;
}

.badge::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
}

/* Each status badge's dot is a BRIGHT shade of its hue; the word stays the readable one
   (2026-09-29). Neutral (.badge-off) has no hue and keeps its currentColor dot. */
.badge-warning::before { background: var(--warning-dot); }
.badge-on::before { background: var(--success-dot); }
.badge-danger::before { background: var(--danger-dot); }
.badge-info::before { background: var(--info-dot); }

/* The words take the vivid --pill-* colours (the Pill Contrast Rule, tokens.css). */
.badge-on {
  background: var(--success-bg);
  color: var(--pill-success);
}

.badge-off {
  background: var(--neutral-bg);
  color: var(--muted);
}

.badge-info {
  background: var(--info-bg);
  color: var(--pill-info);
}

.badge-warning {
  background: var(--warning-bg);
  color: var(--pill-warning);
}

.badge-danger {
  background: var(--danger-bg);
  color: var(--pill-danger);
}

.btn-danger {
  background: var(--danger-solid); /* was var(--danger), which flips to a pale salmon in dark mode — white text
     on that is unreadable. --danger-solid is the fixed fill role, same fix as .notif-badge above. */
  color: var(--on-danger-solid);
  border: none;
}

.btn-danger:hover {
  background: color-mix(in srgb, var(--danger-solid) 80%, black); /* was a literal #b91c1c with no dark-mode
     value; derived so hover always tracks the actual fill instead of drifting from it. */
}

/* ── Alerts ────────────────────────────────────────────────────────────────── */
.alert {
  border-radius: var(--radius-sm);
  line-height: 1.55;
}

.alert-success {
  background: var(--success-bg);
  color: var(--on-success);
  border: 1px solid color-mix(in srgb, var(--success) 45%, transparent);
}

.alert-danger {
  border-color: color-mix(in srgb, var(--danger) 45%, transparent);
}

/* ── View-as-branch preview banner (templates/base.html) ─────────────────────
   Shown on every branch page while an HQ/CS user is in preview mode. Amber
   warning tokens only, so it follows light/dark with the rest of tokens.css.
   The banner sits in normal flow at the very top (so it never covers the
   sticky topbar/sidebar); the fixed pill keeps the mode visible — and
   exitable — after the banner has scrolled away. */
.preview-mode-banner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.5rem 1rem;
  padding: 0.6rem 1rem;
  background: var(--warning-tint);
  color: var(--on-warning);
  border-bottom: 3px solid var(--warning);
  font-size: 0.92rem;
  line-height: 1.5;
  text-align: center;
}

.preview-mode-exit,
.preview-mode-pill {
  margin: 0;
}

.preview-mode-exit-btn,
.preview-mode-pill-btn {
  background: var(--on-warning);
  color: var(--warning-tint);
  border: 1px solid var(--warning);
  border-radius: var(--radius-sm);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.preview-mode-exit-btn {
  padding: 0.35rem 0.8rem;
}

.preview-mode-pill {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 40;
}

.preview-mode-pill-btn {
  padding: 0.45rem 0.9rem;
  font-size: 0.85rem;
  border-radius: 999px;
  border-width: 2px;
}

/* ── Page transitions (2026-09-25, after yoku-cs-case-intake's nav + delivery form) ──
   Two halves. While the next page loads, static/nav.js shows a thin brand bar across the
   top and dims the clicked link, so a click visibly registered even on a page that waits
   seconds on Lark. When the new page arrives, the browser's own cross-document view
   transition fades the content area up 10px (the delivery form's rise-in). That runs on a
   snapshot, never on the live DOM: a real transform on the content would move position:
   fixed children (the theme toggle) and fight the login hero's own translateX. The sidebar
   is untouched, so only the content changes. Browsers without view transitions just
   navigate as before. Everything here is off under prefers-reduced-motion. */
.nav-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  z-index: 2000;
  overflow: hidden;
  pointer-events: none;
  background: color-mix(in srgb, var(--primary-500) 16%, transparent);
}

.nav-progress[hidden] {
  display: none;
}

.nav-progress-bar {
  display: block;
  width: 38%;
  height: 100%;
  background: var(--primary-500);
  border-radius: 0 3px 3px 0;
  animation: nav-progress-slide 1.05s ease-in-out infinite;
}

@keyframes nav-progress-slide {
  0% { transform: translateX(-105%); }
  100% { transform: translateX(365%); }
}

.is-nav-pending {
  opacity: 0.55;
  transition: opacity 0.12s ease;
}

@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }

  .page-content {
    view-transition-name: page-content;
  }

  /* Under the open phone menu the page content is hidden; never lift it into the
     page-change snapshot (the shell's pageswap handler also skips the fade). */
  html.nav-open .page-content {
    view-transition-name: none;
  }

  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 0.18s;
  }

  ::view-transition-old(page-content) {
    animation: page-out 0.14s ease-in both;
  }

  ::view-transition-new(page-content) {
    animation: page-in 0.3s cubic-bezier(0.2, 0.7, 0.2, 1) both;
  }
}

@keyframes page-out {
  to { opacity: 0; }
}

@keyframes page-in {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  /* Keep the indicator, drop the movement: a solid band instead of a sliding one. */
  .nav-progress-bar {
    width: 100%;
    animation: none;
  }
}

/* ── Floating แจ้งปัญหา (แชท) (branch_base.html, 2026-09-28) ─────────────────────
   A bubble bottom-right on every branch page; tapping it opens a panel holding the
   real chat (/ticket-chat?embed=1 in an iframe). A white card surface with a blue
   label and the control edge (Kuang's pick 2026-09-28, over aqua, brand blue and
   ink): the outline-button look, so it never competes with the page's own blue next
   action (the One Blue Rule). It sits in the SAME spot on every page: it lifts only above
   HQ's preview pill, which is on every page. /stock's sticky save bar instead leaves a gap
   for it at its right end (the phone, where the bar is full width, lifts it instead). */
.chat-fab {
  --fab-lift: 0px;
  position: fixed;
  right: 1.25rem;
  bottom: calc(1.25rem + var(--fab-lift));
  z-index: 41;
}

body:has(.preview-mode-pill) .chat-fab { --fab-lift: 3.25rem; }

/* /stock on desktop: the save bar ends before the bubble (bubble 56px tall at 1.25rem
   from the bottom sits beside the bar's buttons). Only while the content reaches under
   it: past ~1800px the content column (capped at 1340px) ends before the bubble. */
@media (min-width: 981px) and (max-width: 1800px) {
  body:has(.chat-fab:not([hidden])) .stk-actions { padding-right: 9rem; }
}

.chat-fab-open {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  height: 56px;
  padding: 0 1.25rem 0 1rem;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: var(--card);
  color: var(--brand-text);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  box-shadow: var(--shadow-lg);
  transition: background-color 120ms ease;
}

.chat-fab-open:hover {
  background: var(--primary-100);
}

.chat-fab-open:focus-visible,
.chat-fab-dismiss:focus-visible,
.chat-fab-close:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* The × to hide the bubble: a small chip on its top-left edge. */
.chat-fab-dismiss {
  position: absolute;
  top: -6px;
  left: -6px;
  display: inline-grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  background: var(--card);
  color: var(--ink-secondary);
  cursor: pointer;
}

.chat-fab-panel {
  position: fixed;
  right: 1.25rem;
  bottom: calc(1.25rem + var(--fab-panel-lift, 0px) + 56px + 0.75rem);
  z-index: 42;
  display: flex;
  flex-direction: column;
  width: min(400px, calc(100vw - 2.5rem));
  height: min(640px, calc(100dvh - 8rem));
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}

.chat-fab-panel[hidden] { display: none; }
body:has(.preview-mode-pill) .chat-fab-panel { --fab-panel-lift: 3.25rem; }

.chat-fab-head {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 0.6rem 0.6rem 1rem;
  border-bottom: 1px solid var(--border);
  background: var(--surface-sunken);
}

.chat-fab-title {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-family: var(--font-mitr);
  font-weight: 600;
  color: var(--ink);
}

.chat-fab-full {
  margin-left: auto;
  font-size: 0.82rem;
  color: var(--brand-text);
}

.chat-fab-close {
  display: inline-grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink-secondary);
  cursor: pointer;
}

.chat-fab-close:hover { background: var(--neutral-bg); }

.chat-fab-frame {
  flex: 1;
  width: 100%;
  border: 0;
  background: var(--bg);
}

/* Phone: an icon-only bubble, and the panel is the whole screen. */
@media (max-width: 980px) {
  .chat-fab-open {
    width: 56px;
    padding: 0;
    justify-content: center;
  }

  .chat-fab-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  /* /stock: 12px above the full-width save bar (its top measured 169px from the bottom).
     The same with HQ's preview pill: that pill sits over the bar, not above it, so it
     adds nothing (stacking both lifts put the bubble ~90px above the bar). */
  body:has(.stk-actions) .chat-fab { --fab-lift: 8.45rem; }

  .chat-fab-panel {
    inset: 0;
    width: auto;
    height: auto;
    border: 0;
    border-radius: 0;
  }
}

/* A search box with a magnifier in front (2026-10-02, Kuang: every search box has one).
   Markup: <span class="filter-search">{{ icon("search", 16) }}<input type="search" …></span>;
   the input's own sheet adds the left padding the icon needs. Shared by the console's
   filter bars (admin.css sizes .filter-input inside it) and the branch stock search. */
.filter-search {
  position: relative;
  display: flex;
  align-items: stretch; /* so the actual <input> inside also fills this wrapper's stretched height, not just centers within it */
  flex: 1 1 240px;
  min-width: 0;
}
.filter-search .ic {
  position: absolute;
  left: 0.7rem;
  top: 50%;
  transform: translateY(-50%);
  color: var(--muted);
  pointer-events: none;
}

/* A thin rule splitting one nav group into its sets (NavItem.divider_before; the CS
   group: campaigns & evidence, then tickets — Kuang, 2026-10-02). */
.app-nav-divider {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 0.4rem 0.75rem;
}
