/* Harbor Mist — YOKU ONE design system applied to the claim webapp.
   Loaded LAST on every page, after static/style.css and the per-portal
   sheet, so it can override both without editing either. style.css is the
   shared green stylesheet copied byte-identically across the sibling Yoku
   webapps -- it stays untouched; everything here is an override layer.
   Token values come from static/tokens.css (the canonical layer). */

/* ── Legacy variable aliases ───────────────────────────────────────────────
   style.css's own :root names, re-pointed at tokens.css. Every rule in
   style.css/admin.css/my.css/... re-themes from this block alone, in both
   light and dark, because nothing here is a literal value.
   --border, --danger, --success, --warning exist under identical names in
   tokens.css, so they are deliberately left undeclared. */
: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);
  /* No drop shadows anywhere in this system -- elevation reads through
     border/surface steps instead. */
  --shadow-sm: none;
  --shadow-md: none;
  --shadow-lg: none;
  --sidebar-w: 244px;

  /* Currently unused: the claim form's submit button was the one place an
     accent fill was spent, and it went back to .btn-primary when One Platform
     replaced Harbor Mist -- their Button spec is "primary is brand-600, one
     per screen", and their aqua-600 (what --accent-500 now resolves to) is
     documented for chart series and illustration, not for a call to action.
     Kept because the reasoning still holds for any future accent fill:
     tokens.css's --accent-700 is "= ink", which FLIPS light in dark mode --
     correct for text ON the pale accent-100 tint, wrong for text on an
     accent-500 FILL, since the accent itself stays bright in both themes.
     Measured on the old gold: light ink on its dark-mode value was ~1.3:1.
     So the on-accent pairing is deliberately theme-invariant. */
  --on-accent: #001d3d;
}

/* One global bump, +2pt over the browser's 16px default -- matching YOKU
   ONE, which carries the same rule. Almost every size in this codebase is
   rem-based, so scaling the root scales the whole app proportionally from a
   single declaration. style.css sets body's font-size as a literal 16px
   (not rem, so it would NOT follow the bump); overridden to 1rem below. */
html {
  font-size: calc(16px + 2pt);
}

/* ── Type ──────────────────────────────────────────────────────────────────
   Mitr on headings/nav/wayfinding, Noto Sans Thai on body. Applied to the
   selectors these stylesheets already target rather than new ones. */
body {
  font-family: var(--font-noto-sans-thai);
  font-size: 1rem;
}

/* <button> does not inherit body's font-family in any browser -- form
   controls fall back to the platform UI font unless told otherwise. This
   sits BEFORE the Mitr block below on purpose: .btn is listed there, and an
   `inherit` rule of equal specificity placed after it would silently win. */
button, input, select, textarea {
  font-family: inherit;
}

/* Headings are 600 everywhere, console and branch-facing pages alike. With
   one typeface, weight is the ONLY thing separating a heading from the
   sentence under it, so 500 against a 400 body is too thin a line to carry
   it. One Platform's `title`, `title-sm` and `d-title-sm` are all 600. */
h1, h2, h3,
.section-title,
.login-title,
.success-title {
  font-family: var(--font-mitr);
  font-weight: 600;
}

/* Navigation and chrome stay a step lighter -- their `d-nav` is 500, and a
   nav item is not a heading. */
.admin-nav-link,
.header-logout,
.tl-head {
  font-family: var(--font-mitr);
  font-weight: 500;
}

/* One Platform's `button` / `d-button` are 600 -- a step above the 500 nav
   items above. Must stay AFTER the `font-family: inherit` reset higher up:
   a <button> takes the platform UI font unless told otherwise, and at equal
   specificity the later rule wins. */
.btn {
  font-family: var(--font-mitr);
  font-weight: 600;
}


/* SIZE only. The console's section heading steps up from style.css's
   0.95rem, which read smaller than the same heading in YOKU ONE. Weight is
   deliberately NOT set here any more: it comes from the global heading rule
   above, so the console and the branch-facing pages cannot drift apart
   (2026-10-01, asked for directly -- the old comment here said the branch
   pages keep their own heading "until somebody decides otherwise"). */
.page-app .section-title {
  font-family: var(--font-mitr);
  font-size: 1.02rem;
}

/* ── Elevation: none ───────────────────────────────────────────────────────
   Several literal (non-variable) box-shadows are spread across style.css,
   admin.css, my.css and purchasing.css; killing them by variable name alone
   would miss every one of those. */
* { box-shadow: none !important; }

/* ── Shapes: the 2/4/6/8px scale ───────────────────────────────────────────
   Every selector here was derived from the app's own stylesheets, not
   guessed -- an earlier pass listed .admin-card/.claim-card/.queue-card/
   .supplier-card/.detail-card/.admin-note, none of which exist, and CSS is
   silent about a selector that matches nothing. Regenerate with

     grep -n 'border-radius' static/*.css | grep -v 'var(--radius'

   Circular photo masks (.header-logo, .login-logo-img, .page-logo,
   .tl-item::before) keep their 50% -- an avatar/photo mask is the system's
   one accepted exception to "never a full round". */
.card,
.login-card,
.conditional-block,
.alert,
.error-detail,
.remark-item,
.attach-thumb,
.admin-attachment-link,
.admin-subcard,
.admin-callout,
.hub-card,
.hub-closed-row,
.outcome-banner,
.attachment-link,
.queue-item,
.email-recipient-note,
.email-preview,
.email-preview-body,
.supplier-claim-row,
.supplier-attachment-link {
  border-radius: var(--radius-md);
}

.field select,
.field input:not([type="radio"]):not([type="checkbox"]):not([type="hidden"]):not([type="submit"]),
.field textarea,
.radio-pill,
.combobox-results,
.remark-form textarea,
.admin-nav-link,
.purchasing-nav-link,
.intake-link-url,
.header-logout,
.qc-group-tag,
.btn-copy {
  border-radius: var(--radius-sm);
}

/* Buttons are rounder than fields. One Platform's own rule is "radius-xl for
   buttons" (20px) against radius-lg for cards — a button is the one control
   it deliberately rounds hardest. This scale's --radius-lg is already exactly
   that 20px, so no new token is needed. .btn-copy stays on the small step: it
   is a compact button, which the source also keeps a step tighter. */
.btn {
  border-radius: var(--radius-lg);
}

/* The 999px pills -- small tags, not controls, so the tightest step. */
.admin-badge,
.hub-column-count,
.status-badge,
.qc-badge,
.qc-age-badge,
.supplier-status-pill,
.tl-gap {
  border-radius: var(--radius-xs);
}

/* ── Dark-mode safety: hardcoded white on an identity-colour fill ──────────
   --primary flips light in dark mode, so every literal `color: white` on a
   primary background becomes illegible. --on-primary is the paired token. */
.page-header,
.page-header h1,
.header-logout,
.btn-primary,
.admin-nav-link.active,
.hub-column-count {
  color: var(--on-primary);
}


/* ── Form fields inside a tinted card ──────────────────────────────────────
   --card (surface-raised) and a field's own fill are both pale washes of the
   same blue under this palette; without a distinct surface + a readable
   border the control disappears into its card. */
/* Selected by EXCLUSION, not by listing types: the enumerated version
   missed input[type="number"] (the "กี่วัน" field), which fell back to the
   browser's white default and showed up as one light box on a dark page
   the moment the page background stopped being white. A type added later
   is covered by construction. */
.field select,
.field input:not([type="radio"]):not([type="checkbox"]):not([type="hidden"]):not([type="submit"]),
.field textarea,
.remark-form textarea {
  background: var(--surface);
  border-color: color-mix(in srgb, var(--border-strong) 55%, var(--border) 45%);
  color: var(--ink);
}

/* ONE blue edge, not two. A 2px outline at a 2px offset sitting on top of an
   already-recoloured border drew a second concentric ring with a gap between
   them -- obvious on every select in the admin filter bar. Splitting pointer
   from keyboard focus would not have helped: Chrome matches :focus-visible on
   a <select> even for a mouse click (measured, both true). So the recoloured
   border carries focus on its own, the same treatment yoku-one uses on its
   filter controls. It stays a real indicator -- --focus is the design system's
   focus-ring blue at 4.9:1 on this surface -- it just stops being drawn twice.
   Buttons and links keep their own outline; this rule is fields only. */
.field select:focus,
.field input:focus,
.field textarea:focus,
.remark-form textarea:focus {
  background: var(--surface);
  border-color: var(--focus);
  outline: none;
}

/* ── Status badges: literal tints promoted to tokens ───────────────────────
   Every selector below was READ OUT of the app's own stylesheets, not
   guessed from the page it appears on. The first pass at this block invented
   four class names (.admin-note, .my-status-pending, .my-status-progress,
   .purchasing-status-pending / .qc-status-pending) that exist nowhere --
   CSS never errors on a selector that matches nothing, so the rules looked
   present and the boxes stayed light-mode-only until somebody opened the
   page in dark mode. The real names are below; find them with

     grep -n '#[0-9a-f]\{3,6\}' static/*.css

   which lists exactly the rules that carry a literal colour and therefore
   have no dark-mode counterpart. */
.admin-callout {
  background: var(--warning-tint);
  border-color: var(--warning);
  color: var(--on-warning);
}

.status-badge--pending,
.status-badge-wait,
.qc-badge-progress,
.supplier-status-pending {
  background: var(--warning-tint);
  color: var(--on-warning);
}

.status-badge--progress {
  background: var(--primary-100);
  color: var(--primary-500);
}

.status-badge--closed,
.outcome-banner--closed {
  background: var(--surface-sunken);
  color: var(--ink-secondary);
}

.supplier-status-internal {
  background: var(--violet-tint);
  color: var(--on-violet);
}

.alert-danger,
.error-detail {
  border-color: var(--danger);
}

.alert-success {
  background: var(--success-tint);
  color: var(--on-success);
  border-color: var(--success);
}

/* A thumbnail's frame and its letterbox fill, both literal greys. */
.attach-thumb {
  border-color: var(--border);
  background: var(--surface-sunken);
}

/* ── Brand header: sheen + drawn wave ──────────────────────────────────────
   The fleet's own .page-header treatment, lifted from stock-input-webapp's
   harbor.css (same shared style.css, same class name) so every Yoku app
   carries an identical header. .page-header stays position:sticky from
   style.css -- sticky is itself a valid containing block for ::after, and
   overriding it to relative silently breaks the sticky header.

   The wave itself is the shared .wave-divider component below; this header
   takes its default --wave-fill (var(--surface), the page background), so
   the content appears to rise into it. stock-input-webapp overrides the fill
   to --card instead -- its header sits above a card-coloured bar. */
.page-header {
  background:
    radial-gradient(115% 135% at 100% 0%, color-mix(in srgb, var(--on-primary) 40%, transparent) 0%, transparent 58%),
    var(--brand-deep);
  padding-bottom: 2.2rem;
}

/* ── 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);
}

/* ── Theme toggle ──────────────────────────────────────────────────────────
   Same two variants the rest of the fleet uses: embedded in the header row
   where there is one, floating where the page has no header at all (login /
   success / error wrappers). Injected by templates/_head_assets.html.

   Every functional icon is a line-style SVG (viewBox 0 0 24 24, stroke-width
   1.75, round caps, fill:none, stroke:currentColor), never an emoji glyph --
   and the sun/moon swap is CSS-only off <html data-theme>, no JS. */
.theme-toggle-btn {
  width: 36px;
  height: 36px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--text);
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
}

.theme-toggle-btn svg {
  width: 18px;
  height: 18px;
  stroke: currentColor;
}

.theme-toggle-btn .icon-sun { display: none; }
[data-theme="dark"] .theme-toggle-btn .icon-moon { display: none; }
[data-theme="dark"] .theme-toggle-btn .icon-sun { display: block; }

.theme-toggle-btn-floating {
  position: fixed;
  top: 0.75rem;
  right: 0.75rem;
  z-index: 30;
}

/* --on-primary is white on the dark (light-mode) header and dark navy on
   the pale (dark-mode) one -- a translucent WHITE chip would vanish in dark
   mode, since the header is nearly white there. Tinting off --on-primary
   keeps it visible against either. .header-logout takes the identical fill
   (style.css hardcodes rgba(255,255,255,...), the same bug), so the two
   buttons are pixel-identical in both themes rather than only alike in one. */
.theme-toggle-btn-header,
.header-logout {
  background: color-mix(in srgb, var(--on-primary) 15%, transparent);
  border-color: color-mix(in srgb, var(--on-primary) 35%, transparent);
  color: var(--on-primary);
}

.header-logout:hover {
  background: color-mix(in srgb, var(--on-primary) 25%, transparent);
}

/* Sized to match its neighbour .header-logout rather than the 36px square
   the floating variant uses. Height parity: the base rule pins line-height
   to 1 (a flex-centred icon square) while .header-logout is an <a>
   inheriting body's 1.5 -- equal font-size and padding still render at
   different heights until the line box itself matches. */
.theme-toggle-btn-header {
  width: auto;
  height: auto;
  /* Identical box to .header-logout in style.css, so the two never drift. */
  padding: 0.35rem 0.7rem;
  font-size: 0.85rem;
}

/* .theme-toggle-btn is display:flex, and a flex container generates NO line
   box -- so `line-height` cannot give it the height its <a> neighbour gets
   for free from body's 1.5. The icon has to BE that line box: 0.85rem x 1.5
   = 1.275rem. Measured before this: toggle 28px vs logout 34px. */
.theme-toggle-btn-header svg {
  width: 1.275rem;
  height: 1.275rem;
}

/* claim_form.html's header is a plain stacked block with no .page-header-row
   to sit in, so the toggle is pinned to the header's own top-right instead.
   .page-header is sticky, which is a valid containing block for this. */
.theme-toggle-btn-header-solo {
  position: absolute;
  top: 0.9rem;
  right: 1.25rem;
  z-index: 2;
}

/* ── Controls that carry meaning get the readable border ──────────────────
   tokens.css splits --border (decorative hairlines) from --border-strong
   (borders that ARE the control). A radio pill is a control, so it takes
   the same blend the fields above use rather than the hairline. */
.radio-pill {
  border-color: color-mix(in srgb, var(--border-strong) 55%, var(--border) 45%);
}

.radio-pill:has(input:checked) {
  border-color: var(--primary-500);
  background: var(--primary-100);
  color: var(--primary-500);
}

/* .btn-copy is styled in purchasing.css, but _intake_link.html renders it on
   the admin dashboard too, where that sheet never loads -- so it fell back
   to the browser's default button chrome (a pale grey fill), invisible
   against a dark surface. Styled here so it is themed wherever it appears. */
.btn-copy {
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.25rem 0.6rem;
  margin-left: 0.4rem;
  border: 1px solid color-mix(in srgb, var(--border-strong) 55%, var(--border) 45%);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink-secondary);
  cursor: pointer;
}

.btn-copy:hover {
  border-color: var(--primary-500);
  color: var(--primary-500);
}

/* The toggle is injected as a THIRD child of .page-header-row, which is
   justify-content: space-between -- that pushed .header-logout out to the
   middle of the header. An auto margin on the title block absorbs all the
   free space, so the two buttons stay clustered at the right edge (the row's
   own 0.75rem gap keeps them apart). */
.page-header-row > :first-child {
  margin-right: auto;
}

/* ══ Admin console shell ═══════════════════════════════════════════════════
   Ported from YOKU ONE's static/yoku-one.css (the .page-app grid), so the
   two admin consoles share one layout: a fixed sidebar on landscape that
   collapses at 980px into a horizontally-scrolling nav row. Only /admin/*
   carries .page-app; every portal page keeps style.css's 480px .page
   column and the wave header above. */
.page.page-app {
  max-width: none;
  width: 100%;
  margin: 0;
  display: grid;
  grid-template-columns: var(--sidebar-w, 244px) minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr);
  grid-template-areas:
    "side top"
    "side main";
  min-height: 100vh;
}

.page-app .page-content {
  grid-area: main;
  padding: 1.5rem 1.6rem 3rem;
}

/* ── 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;
}

/* Same sheen + wave as .page-header, so the brand block reads as the same
   surface -- but --wave-fill is the SIDEBAR's own background (--card), not
   the page background, because that is what sits directly below it. */
.app-brand {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 1.1rem 1.05rem 2rem;
  --wave-fill: var(--card);
  --wave-height: 36px;
  background:
    radial-gradient(115% 130% at 100% 0%, color-mix(in srgb, var(--on-primary) 42%, transparent) 0%, transparent 60%),
    var(--brand-deep);
  color: var(--on-primary);
  text-decoration: none;
}

.app-brand-mark {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--surface);
  object-fit: cover;
  flex-shrink: 0;
}

.app-brand-name {
  display: block;
  font-family: var(--font-mitr);
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.25;
}

.app-brand-tag {
  display: block;
  font-size: 0.72rem;
  opacity: 0.85;
}

/* ── Nav ─────────────────────────────────────────────────────────────────── */
.app-nav {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.85rem 0.65rem 0.5rem;
  overflow-y: auto;
}

.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: 500; /* their `d-nav`; the active item steps to 600 below. */
  line-height: 1.35;
}

.app-nav-item:hover {
  background: var(--surface-sunken);
}

.app-nav-icon {
  display: inline-flex;
  color: var(--muted);
  flex: 0 0 auto;
}

/* NOT --primary-light: it aliases to --primary-100, which tokens.css sets
   to the SAME value as --surface-raised (#e3eaf2 light / #001d3d dark) --
   and --card is --surface-raised, so the active row painted itself in the
   sidebar's own colour and vanished in both themes. Mixing real primary
   into the card guarantees a step away from whatever the sidebar is. */
.app-nav-item.is-active {
  background: color-mix(in srgb, var(--primary-500) 16%, var(--card));
  color: var(--primary-dark);
  /* No weight change: DESIGN.md's Wayfinding-label role is Mitr 400 at rest
     AND active, and YOKU ONE's own active row does not bold. A 500 here made
     this console's active item visibly heavier than the same row there. */
}

.app-nav-item.is-active .app-nav-icon {
  color: var(--primary);
}

/* ── 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% version of --surface, never a literal rgba -- a hardcoded
     near-white bar would sit over the dark page in dark mode. */
  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 title shrink instead of shoving the actions
   off the right edge of the flex row. */
.app-topbar-head,
.app-nav-label {
  min-width: 0;
}

/* The screen title -- their `title-lg` / `d-title-lg` are both 700. */
.app-page-title {
  font-family: var(--font-mitr);
  font-size: 1.26rem;
  font-weight: 700;
  color: var(--text);
  line-height: 1.3;
}

.app-page-sub {
  font-size: 0.85rem;
  color: var(--muted);
  margin-top: 0.12rem;
}

.app-topbar-actions {
  display: flex;
  align-items: center;
  gap: 0.55rem;
}

/* On the shell there is no brand-coloured field behind these -- they sit on
   the page surface, so the translucent on-primary chip the wave header uses
   would be invisible. Give them the ordinary control treatment instead. */
/* A true square sized off .header-logout's own box model (style.css:
   font-size .85rem x line-height 1.5, plus .35rem padding and a 1px border
   each side) rather than the base rule's 36px -- a shared formula, so the
   two can't drift the way they did in the wave header. */
.app-topbar-actions .theme-toggle-btn {
  width: calc(0.85rem * 1.5 + 2 * (0.35rem + 1px));
  height: calc(0.85rem * 1.5 + 2 * (0.35rem + 1px));
}

.app-topbar-actions .header-logout,
.app-topbar-actions .theme-toggle-btn {
  background: var(--card);
  border: 1px solid color-mix(in srgb, var(--border-strong) 55%, var(--border) 45%);
  color: var(--text);
  opacity: 1;
}

.app-topbar-actions .header-logout:hover,
.app-topbar-actions .theme-toggle-btn:hover {
  background: var(--surface-sunken);
}

/* ── Responsive: the sidebar becomes a scrolling nav row ─────────────────── */
@media (max-width: 980px) {
  .page.page-app {
    grid-template-columns: minmax(0, 1fr);
    /* Three explicit row tracks for the three stacked areas. Without this
       the collapsed layout inherits the desktop rule's TWO tracks and the
       browser opens a large implicit gap between nav and topbar -- YOKU
       ONE hit this, visible only at real mobile width. */
    grid-template-rows: auto auto minmax(0, 1fr);
    grid-template-areas:
      "side"
      "top"
      "main";
  }

  .app-sidebar {
    position: static;
    height: auto;
    border-right: none;
    border-bottom: 1px solid var(--border);
  }

  .app-brand {
    padding: 0.85rem 1rem 1.4rem;
  }

  .app-nav {
    flex-direction: row;
    gap: 0.35rem;
    overflow-x: auto;
    padding: 0.6rem 0.75rem;
    -webkit-overflow-scrolling: touch;
  }

  .app-nav-item {
    white-space: nowrap;
    flex: 0 0 auto;
    padding: 0.5rem 0.85rem;
  }

  .app-topbar {
    position: static;
    padding: 0.9rem 1rem;
  }

  .page-app .page-content {
    padding: 1.1rem 1rem 2.5rem;
  }
}

@media (max-width: 560px) {
  .app-page-title { font-size: 1.12rem; }
}

/* ══ Responsive page width ════════════════════════════════════════════════
   style.css hard-caps `.page` at 480px — right for a phone, but on a
   tablet/desktop viewport it strands a narrow column in a sea of empty
   margin. Ported verbatim from stock-input-webapp's own harbor.css so every
   non-shell page in the fleet grows on ONE ladder rather than each app
   inventing its own breakpoints.

   NOTE: style.css's comment above its `@media (min-width: 1024px)` block
   still says the open claim form "deliberately keeps its phone-width column
   on desktop too". That decision was reversed on 2026-09-21; the comment is
   stale but style.css stays byte-identical across the sibling apps, so it is
   NOT edited here — this block is the correction, and DESIGN.md carries the
   rule. style.css's own `.page-portal { max-width: 1100px }` is superseded
   by the 1080px step below (same specificity, this file loads later), which
   is deliberate: portal and form pages should not stop at different widths.

   `.page.page-app` (the console shell) keeps `max-width: none` — two class
   selectors beat the one below, so the shell is unaffected. */
:root {
  --page-max: 480px;
}
@media (min-width: 600px) {
  :root { --page-max: 640px; }
}
@media (min-width: 900px) {
  :root { --page-max: 880px; }
}
@media (min-width: 1200px) {
  :root { --page-max: 1080px; }
}

.page {
  max-width: var(--page-max);
}

/* ── Two-column fields at the 900px step ──────────────────────────────────
   The Page Ladder Rule's own follow-up: once a page reaches 880px a
   single-line control stretched across a full-width card reads badly (a
   ~1000px name field at the 1080px step). The fix named in the rule is a
   real two-column field layout, not a bare max-width on the control, which
   only strands the field in dead space.

   Opt-IN, not opt-out: every direct child spans both columns by default, and
   only a genuinely short, independent field takes `field-half`. That way a
   field added later is full-width until someone decides otherwise, rather
   than silently landing in a column and breaking a group. Textareas, file
   inputs, radio groups, the branch combobox (it owns an absolutely-
   positioned results dropdown) and any wrapper holding its own fields all
   keep the full width they need.

   `.field-grid` marks a conditional block that is itself a container of
   short sibling fields -- it is both a grid item in its card and a grid of
   its own. Applied only where a real group exists, never by matching
   .conditional-block wholesale: most of those hold one control.

   The console shell is excluded (`:not(.page-app)`): its cards are data
   panels, not forms, and admin.css already lays them out. */
@media (min-width: 900px) {
  .page:not(.page-app) .card.section,
  .page:not(.page-app) .field-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 1.25rem;
    align-items: start;
  }

  .page:not(.page-app) .card.section > *,
  .page:not(.page-app) .field-grid > * {
    grid-column: 1 / -1;
  }

  .page:not(.page-app) .card.section > .field-half,
  .page:not(.page-app) .field-grid > .field-half {
    grid-column: auto;
  }

  /* A .field-grid is a grid container, so its own bottom margin no longer
     collapses with its last child's -- zero it out and let the row gap of
     the card it sits in do the spacing. */
  .page:not(.page-app) .field-grid > .field:last-child {
    margin-bottom: 0;
  }
}

/* ── Pipeline hub: wrap, never scroll sideways ────────────────────────────
   admin.css lays the board out as a classic kanban -- `display: flex` with
   `overflow-x: auto` and `flex: 0 0 270px` columns -- so its 6 stages need
   1674px and scroll horizontally inside a 1136px content area. That is an
   ELEMENT-level scrollbar, not page overflow (the page measures 0 at every
   width), which is why a page-overflow sweep never flags it.

   A horizontal scroll hides content behind an interaction: a stage that is
   off-screen right is a stage nobody reads. The board becomes a wrapping
   grid instead, so every stage is visible at once at any width and the
   columns simply re-flow -- 4 across on a desktop content area, 1 on a
   phone. `flex: 0 0 270px` on the columns goes inert in a grid, so the
   children need no change.

   The nav row's own horizontal scroll at <=980px is deliberate and stays:
   that is the shell's collapsed posture (The One Shell Rule), where every
   item is reachable by swiping a single short row. */
.hub-board {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  overflow-x: visible;
  /* Grid's default `stretch`, stated rather than left implicit: every column
     in a row takes that row's height, so a tall one (ปิดเคสแล้ว, which lists
     five closed statuses) no longer leaves its neighbours as short stubs.
     `align-items: start` was tried first and is what produced the ragged
     row -- do not put it back. Rows are still independent, so an all-empty
     row stays short instead of inheriting the tallest card on the board. */
  align-items: stretch;
}

/* .hub-column is already a flex column and .hub-column-body already scrolls,
   so the extra height a stretched column gains simply becomes empty space
   below its content -- cards stay top-aligned, nothing re-centres. */
.hub-column-body {
  flex: 1;
}

/* ── Upstream components added after this layer was written ───────────────
   The QC queue's stage tabs and filter bar, the file chips, and the
   validation-error treatment all shipped with literal colours (#fff,
   #b3261e, #fdf3f2 …) and so had no dark-mode value -- a white bar on a
   #000814 page. Found by scripts/check-dark-mode.mjs, and every selector
   below was read out of static/qc.css / static/purchasing.css with

     grep -n '#[0-9a-f]\{3,6\}' static/*.css

   rather than inferred from the page (The Silent Selector Rule). */
.qc-tab,
.qc-filter-bar,
.qc-filter-toggle,
.file-chip {
  background: var(--card);
  color: var(--text);
  border-color: var(--border);
}

/* Upstream styles the ACTIVE tab as a filled primary pill, and its count
   chip is white-on-translucent-white to sit on that fill. Tinting the tab
   the way a sidebar nav row is tinted broke that contract and left a white
   chip on a white tab -- so the fill is kept and the chip is re-pointed at
   --on-primary, the same pairing .header-logout uses on the brand header.
   (A tab is not a nav row; this is upstream's component, and its design
   reads correctly once the tokens are right.) */
.qc-tab.is-active,
.qc-tab[aria-current="page"] {
  background: var(--primary-500);
  color: var(--on-primary);
  border-color: var(--primary-500);
}

.qc-tab.is-active .qc-tab-count {
  color: var(--on-primary);
  background: color-mix(in srgb, var(--on-primary) 22%, transparent);
  border-radius: var(--radius-xs);
}

.file-chip-x,
.file-chip-count {
  color: var(--muted);
}

.file-chip-x:hover {
  background: var(--surface-sunken);
  color: var(--danger);
}

/* The error treatment: one danger token instead of a hand-picked red, and a
   tint that has a dark counterpart instead of a near-white wash. */
.validation-summary,
.field-invalid textarea,
.supplier-missing-warn {
  background: var(--danger-tint);
  border-color: var(--danger);
  color: var(--on-danger);
}

.validation-summary-head,
.validation-summary a,
.field-invalid > label,
.field-hint-error {
  color: var(--danger);
}

/* ── /help (the Thai user manual) ──────────────────────────────────────────
   static/help.css is a standalone sheet written before this layer existed:
   ~20 rules carrying literal colours (#fff, #f1f5f9, #e2e8f0 …) and so no
   dark-mode value at all — 72 light elements on a #000814 page. The page
   also had its own <head> that never included the design system, which is
   the real reason it looked untouched in production.

   Selectors below were read out of static/help.css with
     grep -n '#[0-9a-f]\{3,6\}' static/help.css
   not inferred from the rendered page (The Silent Selector Rule). */
.help-nav {
  background: var(--surface-sunken);
  border-bottom-color: var(--border);
}

.help-nav a {
  background: var(--card);
  border-color: var(--border);
  color: var(--text);
}

.help-section,
figure.help-shot img,
details.help-faq {
  background: var(--card);
  border-color: var(--border);
}

.help-section h3 {
  border-bottom-color: var(--border);
}

.help-step {
  background: var(--surface-sunken);
  border-color: var(--border);
}

.help-step .who {
  color: var(--on-primary);
}

.help-step.is-end {
  background: var(--surface-sunken);
}

.help-step.is-end .who {
  background: var(--ink-secondary);
  color: var(--surface);
}

.help-kbd {
  background: var(--surface-sunken);
  border-color: var(--border-strong);
  color: var(--text);
}

/* Callouts: the info one had no token pair in this app, so it borrows the
   accent tint (gold carries ink, per the accent rule) rather than inventing
   a blue that would need its own dark counterpart. */
.help-note {
  background: var(--primary-100);
  color: var(--text);
}

.help-warn {
  background: var(--warning-tint);
  color: var(--on-warning);
}

table.help-table td {
  border-color: var(--border);
}

table.help-table th {
  background: var(--surface-sunken);
  color: var(--text);
}

.help-pill.open {
  background: var(--warning-tint);
  color: var(--on-warning);
  border-color: var(--warning);
}

.help-pill.done {
  background: var(--success-tint);
  color: var(--on-success);
  border-color: var(--success);
}

details.help-faq p,
dl.help-glossary dd {
  color: var(--muted);
}

/* ── No left-edge accent rails, anywhere ──────────────────────────────────
   The system dropped the left-edge inset bar on 2026-09-17 and it stays
   dropped: a tint or a full border carries the signal, a rail is a second
   one competing with it. Five of them survived in this app's own sheets --
   help.css's two callouts, purchasing's supplier warning, QC's validation
   summary, and the kanban's carried-over card.

   `.tl-list`'s 2px left border in style.css is deliberately NOT in this
   list: that is the timeline's vertical spine, the track the dots sit on,
   not an accent on a box. Removing it would delete the component.

   `.hub-card-carry` loses its only per-card marker, which is fine here:
   carried-over cards are already grouped above their own divider label
   (.hub-carry-label), so the grouping still says it. */
.help-note,
.help-warn {
  border-left: 0;
}

.supplier-missing-warn,
.validation-summary {
  border-left-width: 1px;
}

.hub-card-carry {
  border-left: 0;
}
