/* ============================================================================
   visitor-ui.css — the ONE stylesheet every devhost page uses.
   B-6 · DEC-VM-2026-08-10-20 item 6.

   Change a value HERE and every screen changes. Before this file, four pages
   each carried a private <style> block and drifted from the approved mockups
   without anything noticing — the owner found it, not a gate.

   TOKEN VALUES ARE NOT OURS TO INVENT. They mirror max-hrm's V2 design system
   (design-v2.md §2-§4 · apps/web/app/globals.css, surveyed read-only
   2026-08-10).

   Precisely (Codex b6-ui-kit-r4 FAIL — the earlier wording claimed
   "byte-identical", which this file is not):
     · every token docs/mockups/v2-shared.css declares — the 32 the owner
       approved — appears here with the SAME value, and
       scripts/check-ui-tokens.sh fails on any disagreement or omission;
     · this file ADDS five the mockups wrote as literals rather than
       variables (--r-sm/--r-md/--r-lg, --shadow-card, --shadow-pop). Their
       values come from the same approved screens; tokenising them is the
       point of a kit. The gate lists them as additive on every run.
   Adding a token that CHANGES an approved value is the case the gate exists
   to stop — that needs the owner's approval on the mockups first.

   Class names deliberately match the mockups' vocabulary (.btn .badge .input
   .tbl .card …) so a devhost page and its mockup can be read side by side.

   NOT the product frontend. The MAX-HRM build consumes their live V2 React
   components (DEC-VM-2026-08-08-09); the standalone edition (B-8) grows its
   own frontend from these same tokens.
   ============================================================================ */

/* ----------------------------------------------------------------------------
   The typeface travels WITH the product (E-18 · Codex `c1-login-r7`).

   This was an @import from fonts.googleapis.com, which meant every page waited
   on a third-party request before rendering text — and a customer installed
   behind a firewall, on an isolated network, or offline got a fallback face
   instead of the approved one. For software that runs on someone else's
   machine that is the wrong dependency, and it was the only outbound call the
   product made.

   Four weights, because those are the four the design system uses. IBM Plex
   Sans Thai is SIL OFL; fonts/LICENSE.txt ships beside the files, as the
   licence requires.

   `font-display:swap` keeps text readable while the file loads. It is local
   now, so the swap window is milliseconds — but the property is what
   guarantees text is never invisible, not the speed of the disk.
   ------------------------------------------------------------------------- */
@font-face{font-family:'IBM Plex Sans Thai';font-style:normal;font-weight:400;
  font-display:swap;src:url('/fonts/IBMPlexSansThai-Regular.woff2') format('woff2')}
@font-face{font-family:'IBM Plex Sans Thai';font-style:normal;font-weight:500;
  font-display:swap;src:url('/fonts/IBMPlexSansThai-Medium.woff2') format('woff2')}
@font-face{font-family:'IBM Plex Sans Thai';font-style:normal;font-weight:600;
  font-display:swap;src:url('/fonts/IBMPlexSansThai-SemiBold.woff2') format('woff2')}
@font-face{font-family:'IBM Plex Sans Thai';font-style:normal;font-weight:700;
  font-display:swap;src:url('/fonts/IBMPlexSansThai-Bold.woff2') format('woff2')}

:root{
  /* primary scale (indigo default theme) */
  --primary-50:#EEF2FF; --primary-100:#E0E7FF; --primary:#3C50E0;
  --primary-600:#2A3BB5; --primary-700:#1E2A8A;
  /* surfaces */
  --app-bg:#F1F5F9; --page-bg:#FFFFFF; --section-bg:#F8FAFC;
  --border:#E2E8F0; --border-soft:#F1F5F9; --input-border:#CBD5E1;
  /* text */
  --text:#020617; --body:#0F172A; --muted:#334155; --faint:#64748B; --ph:#94A3B8;
  /* semantic */
  --success:#10B981; --success-bg:#D1FAE5; --success-text:#047857;
  --danger:#EF4444;  --danger-bg:#FEE2E2;  --danger-text:#B91C1C;
  --warning:#F59E0B; --warning-bg:#FEF3C7; --warning-text:#B45309;
  /* status (fixed, never theme-dependent) */
  --st-active:#3B82F6; --st-active-bg:#EFF6FF; --st-active-border:#BFDBFE;
  --st-all:#475569; --st-all-bg:#F1F5F9; --st-all-border:#CBD5E1;
  --rail:#1C2434;
  /* radius + shadow (design-v2 §4) */
  --r-sm:6px; --r-md:8px; --r-lg:12px;
  --shadow-card:0 1px 2px rgba(15,23,42,.06);
  --shadow-pop:0 10px 30px rgba(15,23,42,.12);
}

/* ---------- base ---------- */
*{box-sizing:border-box;margin:0;padding:0}
html{-webkit-text-size-adjust:100%}
body{font-family:'IBM Plex Sans Thai',-apple-system,'Segoe UI',sans-serif;
     font-size:15px;line-height:1.55;color:var(--body);background:var(--app-bg)}
a{color:var(--primary);text-decoration:none}
a:hover{text-decoration:underline}

/* ---------- typography ---------- */
h1,.h1{font-size:24px;font-weight:600;color:var(--text)}
h2,.h2{font-size:20px;font-weight:600;color:var(--text)}
h3,.sec-h{font-size:15px;font-weight:500;color:var(--text)}
.lbl,label{font-size:12px;color:var(--muted);display:block;margin-bottom:4px}
.meta,.note{font-size:12px;color:var(--faint)}
.num{font-variant-numeric:tabular-nums}
.hint{font-size:12px;color:var(--faint);margin-top:4px}
.req{color:var(--danger-text)}

/* ---------- app chrome (harness header + nav) ---------- */
.app-header{background:var(--rail);color:#E2E8F0;padding:12px 20px;display:flex;
            align-items:center;gap:14px;flex-wrap:wrap}
.app-header b{color:#fff;font-size:16px}
/* the lockup replaces the typed product name here too — same dark surface */
.app-header .portal-logo{height:28px}
.dev-badge{font-size:12px;font-weight:600;background:var(--warning-bg);
           color:var(--warning-text);border-radius:999px;padding:2px 10px}
/* `nav.tabs` + `.active` are the hooks the existing pages already use
   (showTab toggles `.active`) — the kit adapts to the page's JS, not the
   other way round, so re-skinning a page never means rewriting its code. */
.app-nav,nav.tabs{background:var(--page-bg);border-bottom:1px solid var(--border);
         padding:0 20px;display:flex;gap:4px;flex-wrap:wrap;margin:0}
.app-nav button,nav.tabs button{font:inherit;font-size:15px;font-weight:500;background:none;
                border:0;border-radius:0;height:auto;border-bottom:2px solid transparent;
                color:var(--muted);padding:12px 14px;cursor:pointer}
.app-nav button:hover,nav.tabs button:hover{color:var(--text);background:var(--section-bg);
                border-color:transparent}
.app-nav button.on,nav.tabs button.active{color:var(--primary);border-bottom-color:var(--primary)}
main{max-width:1280px;margin:20px auto;padding:0 20px 64px}

/* ---------- surfaces ---------- */
.panel,.card{background:var(--page-bg);border:1px solid var(--border);
             border-radius:var(--r-lg);padding:20px;box-shadow:var(--shadow-card);
             margin-bottom:16px}
.card.flat{box-shadow:none}
.panel > h2,.card > h2{margin-bottom:4px}
.panel > h2 + .note,.card > h2 + .note{margin-bottom:16px;display:block}
.section-title{display:flex;align-items:center;gap:10px;margin:24px 0 12px}
.divider{height:1px;background:var(--border);margin:16px 0}

/* ---------- layout helpers ---------- */
.row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
.row.top{align-items:flex-start}
.stack{display:flex;flex-direction:column;gap:12px}
/* `min(260px,100%)` rather than a bare 260px: `minmax(260px,1fr)` makes every
   column AT LEAST 260px even when the container is narrower, so on a phone the
   grid child is wider than the card holding it and every `width:100%` input
   inside it hangs off the right edge. The page does not scroll sideways — the
   card clips it — which is why four text gates and a screenshot all passed
   while the controls were visibly outside their card (S-6a, measured). */
.grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr));gap:16px}
.sp{flex:1}

/* ---------- buttons ----------
   Hover feedback is deliberately STRONGER than max-hrm's (they hover to
   --section-bg #F8FAFC, two shades off white): the owner reported it read as
   "ไม่ค่อยชัดเจน" on this harness, 2026-08-10. Every hover value below is
   still an approved token — no new colours were invented.

   ⚠️ Specificity, the bug this block was rewritten for: a bare
   `button:hover` (0,1,1) OUTRANKS `.success` (0,1,0), so the green
   "ยืนยันให้เข้า" button turned pale grey with white text the moment the
   mouse touched it — unreadable, and reported as "สีเพี้ยน". Every variant
   now states its OWN hover at equal-or-higher specificity. Adding a variant
   without its hover reopens the bug. */
button,.btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;
     height:40px;padding:0 16px;border-radius:var(--r-md);border:1px solid var(--input-border);
     font:inherit;font-size:15px;font-weight:500;cursor:pointer;white-space:nowrap;
     background:var(--page-bg);color:var(--body);
     transition:background .12s,border-color .12s,color .12s}
button:hover,.btn:hover{background:var(--app-bg);border-color:var(--ph)}
button:active,.btn:active{background:var(--border);border-color:var(--faint)}
button:focus-visible,.btn:focus-visible{outline:2px solid var(--primary-100);outline-offset:1px;
     border-color:var(--primary)}
button:disabled,.btn:disabled{opacity:.5;cursor:not-allowed}
button:disabled:hover,.btn:disabled:hover{background:var(--page-bg);border-color:var(--input-border)}
.btn.sm,button.sm{height:32px;padding:0 12px;font-size:14px}

button.primary,.btn-primary{background:var(--primary);color:#fff;border-color:var(--primary)}
button.primary:hover,.btn-primary:hover{background:var(--primary-600);border-color:var(--primary-600)}
button.primary:active,.btn-primary:active{background:var(--primary-700);border-color:var(--primary-700)}

button.danger,.btn-danger{background:var(--danger);color:#fff;border-color:var(--danger)}
button.danger:hover,.btn-danger:hover{background:var(--danger-text);border-color:var(--danger-text)}
button.danger:active,.btn-danger:active{background:var(--danger-text);filter:brightness(.92)}

button.success,.btn-success{background:var(--success);color:#fff;border-color:var(--success)}
button.success:hover,.btn-success:hover{background:var(--success-text);border-color:var(--success-text)}
button.success:active,.btn-success:active{background:var(--success-text);filter:brightness(.92)}

button.ghost,.btn-ghost{background:transparent;border-color:transparent;color:var(--muted)}
button.ghost:hover,.btn-ghost:hover{background:var(--border-soft);border-color:var(--border);color:var(--text)}
button.ghost:active,.btn-ghost:active{background:var(--border)}

.action{height:32px;padding:0 12px;font-size:14px}

/* ---------- forms ---------- */
input,select,textarea,.input,.selectbox{width:100%;height:40px;border:1px solid var(--input-border);
      border-radius:var(--r-md);padding:0 12px;font:inherit;font-size:15px;
      color:var(--body);background:var(--page-bg)}
textarea{height:auto;min-height:76px;padding:9px 12px;resize:vertical;line-height:1.55}
input::placeholder,textarea::placeholder{color:var(--ph)}
input:focus,select:focus,textarea:focus{outline:2px solid var(--primary-100);
      outline-offset:0;border-color:var(--primary)}
/* Owner 2026-08-10: "ขอปรับให้ checkbox ใหญ่ขึ้นอีกหน่อย".
   It also never had the size it claimed — `height:18px;height:auto` set the
   height twice and the second won, so the box rendered at the browser's
   default while the rule said 18. Sized explicitly now (the tick has to
   escape the 40px height every other input inherits) and given a pointer,
   because a control the mouse does not acknowledge reads as decoration. */
input[type=checkbox],input[type=radio]{width:22px;height:22px;min-height:22px;
     accent-color:var(--primary);cursor:pointer;vertical-align:middle;flex:none}
.check-row label{cursor:pointer}
.phone input[type=checkbox],.phone input[type=radio]{width:26px;height:26px;min-height:26px}
select{cursor:pointer}
.field{margin-bottom:16px}
.check-row{display:flex;gap:16px;align-items:center;flex-wrap:wrap}
.check-row label{display:inline-flex;align-items:center;gap:7px;font-size:15px;
                 color:var(--body);margin:0}
form .row + .row{margin-top:12px}

/* ---------- V2-style dropdown ----------
   Anatomy traced from max-hrm's V2Select.tsx (read-only, 2026-08-10): a
   BUTTON trigger + a panel that is positioned against the trigger's measured
   rect, options carrying a leading check slot, selected = primary-50 + primary
   text, highlighted = section-bg, and placement that FLIPS above the trigger
   when there is not enough room below.

   Their rule "no native <select> in a V2 surface" is why this exists: the
   native menu is the OS's, not the product's — it ignores every token here,
   and on macOS it opens ON TOP of the trigger (owner report, 2026-08-10).

   The real <select> stays in the DOM (visually hidden) and remains the value
   store, so page code that reads `.value` keeps working — visitor-ui.js only
   draws the surface. Their portal rule becomes `position:fixed` here: the
   panel is appended to <body>, so no ancestor's overflow can clip it. */
.v2-select{position:relative;display:block;width:100%}
.v2-select > select{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;
     clip:rect(0 0 0 0);overflow:hidden}
.v2-select-trigger{display:inline-flex;align-items:center;justify-content:space-between;gap:8px;
     width:100%;height:40px;padding:0 12px;border-radius:var(--r-md);
     border:1px solid var(--input-border);background:var(--page-bg);color:var(--body);
     font:inherit;font-size:15px;font-weight:500;text-align:left;cursor:pointer;
     transition:background .12s,border-color .12s}
.v2-select-trigger:hover{background:var(--app-bg);border-color:var(--ph)}
.v2-select-trigger:focus-visible,.v2-select.open .v2-select-trigger{outline:2px solid var(--primary-100);
     outline-offset:0;border-color:var(--primary)}
.v2-select-trigger .lab{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.v2-select-trigger .lab.ph{color:var(--ph);font-weight:400}
.v2-select-trigger .chev{flex:none;width:16px;height:16px;color:var(--faint);
     transition:transform .15s}
.v2-select.open .v2-select-trigger .chev{transform:rotate(180deg)}
.v2-select-trigger:disabled{opacity:.6;cursor:not-allowed}

/* `width:max-content` — the panel is at LEAST as wide as its trigger (the JS
   sets that as an inline min-width) and grows to its longest option. Without
   it a 72px flag trigger produced a 105px panel and "🇬🇧 English" wrapped onto
   two lines (owner 2026-08-11). Same rule V2Select uses: `min-w-full w-max`.
   The viewport cap keeps a long option from running off a phone. */
.v2-select-panel{position:fixed;z-index:1060;background:var(--page-bg);
     border:1px solid var(--border);border-radius:var(--r-lg);box-shadow:var(--shadow-pop);
     padding:4px 0;max-height:280px;overflow-y:auto;
     width:max-content;max-width:calc(100vw - 24px)}
.v2-select-option{white-space:nowrap}
.v2-select-option{display:flex;align-items:center;gap:8px;width:100%;padding:8px 12px;
     border:0;border-radius:0;background:none;font:inherit;font-size:15px;font-weight:400;
     color:var(--body);text-align:left;cursor:pointer;height:auto;white-space:normal;
     transition:background .1s}
.v2-select-option .tick{flex:none;width:16px;height:16px;color:var(--primary);visibility:hidden}
.v2-select-option[aria-selected="true"]{background:var(--primary-50);color:var(--primary);font-weight:500}
.v2-select-option[aria-selected="true"] .tick{visibility:visible}
.v2-select-option.hl{background:var(--section-bg);border-color:transparent}
.v2-select-option[aria-selected="true"].hl{background:var(--primary-100)}
.v2-select-empty{padding:14px 12px;text-align:center;font-size:12px;color:var(--faint)}

/* ---------- tables ---------- */
table{width:100%;border-collapse:separate;border-spacing:0;background:var(--page-bg);
      border:1px solid var(--border);border-radius:var(--r-lg);overflow:hidden}
th{background:var(--section-bg);font-size:12px;font-weight:600;color:var(--muted);
   text-align:left;padding:10px 14px;border-bottom:1px solid var(--border);white-space:nowrap}
td{padding:12px 14px;border-bottom:1px solid var(--border-soft);font-size:15px;vertical-align:middle}
tr:last-child td{border-bottom:0}
tbody tr:hover{background:var(--section-bg)}
td .meta{display:block}

/* ---------- badges · status pills · chips ---------- */
/* The status names are the API's OWN values, because every page renders
   `class="badge st-${visit.status}"` straight from the response. A kit that
   invented prettier names would silently render unstyled badges — the class
   list here IS the state machine (visitor-core visit-state.ts). */
.badge{display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:600;
       border-radius:999px;padding:3px 10px;white-space:nowrap;
       background:var(--st-all-bg);color:var(--st-all);border:1px solid var(--st-all-border)}
.st-awaiting_info,.st-awaiting_approval,.st-awaiting_security,.st-awaiting{
       background:var(--warning-bg);color:var(--warning-text);border-color:#FDE68A}
.st-approved{background:var(--st-active-bg);color:#1D4ED8;border-color:var(--st-active-border)}
.st-checked_in,.st-checked{background:var(--success-bg);color:var(--success-text);border-color:#A7F3D0}
.st-checked_out,.st-expired,.st-cancelled,.st-purged,.st-else{
       background:var(--st-all-bg);color:var(--st-all);border-color:var(--st-all-border)}
.st-rejected{background:var(--danger-bg);color:var(--danger-text);border-color:#FECACA}
.chip{display:inline-flex;align-items:center;gap:5px;font-size:12px;border:1px solid var(--border);
      border-radius:999px;padding:3px 10px;color:var(--muted);background:var(--page-bg)}
.chip.on{background:var(--primary-50);border-color:var(--primary-100);
         color:var(--primary-700);font-weight:600}
.dot{width:7px;height:7px;border-radius:99px;display:inline-block}

/* ---------- alerts ---------- */
.alert{display:flex;gap:10px;border-radius:var(--r-md);padding:10px 12px;font-size:13px;
       align-items:flex-start;margin-bottom:12px}
.alert.info,.info-box{background:var(--primary-50);color:var(--primary-700)}
.alert.warn,.warn-box{background:var(--warning-bg);color:var(--warning-text)}
.alert.good{background:var(--success-bg);color:var(--success-text)}
.alert.bad{background:var(--danger-bg);color:var(--danger-text)}
.warn-box,.info-box{border-radius:var(--r-md);padding:10px 12px;font-size:13px;margin-bottom:12px}

/* ---------- reception result card (mockup 04) ---------- */
.rec-card{border:1px solid var(--border);border-radius:var(--r-lg);padding:14px 16px;
     margin-bottom:12px;display:flex;gap:14px;flex-wrap:wrap;align-items:center;
     justify-content:space-between;background:var(--page-bg)}
.rec-main{min-width:260px}
.rec-main b{font-size:16px}
.rec-meta{font-size:13px;color:var(--faint)}
/* Vehicle plate is shown to reception/security on purpose — DEC-06 item 5:
   the guard must know which car to expect. */
.plate-chip{background:var(--primary-50);color:var(--primary-700);border-radius:var(--r-sm);
     padding:2px 8px;font-size:13px;font-weight:600}
.card-chip{background:var(--success-bg);color:var(--success-text);border-radius:var(--r-sm);
     padding:2px 8px;font-size:13px;font-weight:600}

/* ---------- emergency banner (spec v1 §4.8 — pinned on EVERY module page
     while suspended, until someone lifts it) ---------- */
.banner-emergency{display:none;background:var(--danger);color:#fff;padding:10px 20px;
     font-size:15px;font-weight:600}
.banner-emergency button{margin-left:12px;height:30px;padding:0 12px;font-size:13px;
     border:1px solid #fff;background:transparent;color:#fff}
.banner-emergency button:hover{background:rgba(255,255,255,.15);border-color:#fff}

/* ---------- empty state ---------- */
.empty{text-align:center;padding:40px 20px;color:var(--faint);background:var(--page-bg);
       border:1px dashed var(--input-border);border-radius:var(--r-lg)}
.empty b{display:block;color:var(--muted);font-size:15px;margin-bottom:4px}

/* ---------- toast ---------- */
.toast{position:fixed;right:20px;bottom:20px;z-index:60;background:var(--rail);color:#fff;
       padding:12px 16px;border-radius:var(--r-md);box-shadow:var(--shadow-pop);
       font-size:14px;max-width:420px;display:none}
.toast.show{display:block}

/* ---------- guest-facing surfaces (mobile-first: 48px controls, 16px text) ---------- */
.phone{max-width:430px;margin:0 auto;padding:16px}
.phone input,.phone select,.phone textarea,.phone button,.phone .btn{height:48px;font-size:16px}
.phone textarea{height:auto;min-height:88px}
.phone .card{padding:16px}

/* ============================================================================
   GUEST SURFACES — the MAX-HRM portal's brand treatment (B-9 · mockup 10,
   owner-approved 2026-08-10). Traced value-for-value from their
   PortalShell.tsx + _RegisterFormClient.tsx (read-only, 2026-08-10).

   Two of their decisions travel with the look and are not ours to relax:
   ① These values stay LITERAL, not tokenised. The field is fixed brand
      chrome: it must look identical under all eight tenant themes, so a
      theme-tracking token would be the wrong tool. This file is the one
      place allowed to name a colour (scripts/check-ui-tokens.sh enforces
      that pages never do).
   ② Phone FIRST — this surface's product definition is a phone: a visitor
      reaches it by scanning the QR on the door or tapping an SMS/email
      link. 48px controls, 16px text (below 16px iOS zooms the page on
      focus), 44px+ touch targets.

   Deliberately NOT copied: the admin login's three infinite animations.
   The max-hrm room dropped them on phones for battery and jank; inheriting
   the look without that decision would inherit a problem they solved.
   ========================================================================= */
.portal{position:relative;min-height:100dvh;
        background:linear-gradient(135deg,#060C18 0%,#0A1128 55%,#0C1535 100%)}
/* The field is FIXED so a form taller than the screen scrolls OVER it —
   without that, the card drags its own backdrop away and the page ends in
   bare gradient. */
.portal-field{position:fixed;inset:0;pointer-events:none}
.portal-field .grid{position:absolute;inset:0;
  background-image:linear-gradient(to right,rgba(124,58,237,.18) 1px,transparent 1px),
                   linear-gradient(to bottom,rgba(124,58,237,.18) 1px,transparent 1px);
  background-size:40px 40px}
.portal-field .glow{position:absolute;top:-80px;left:-70px;width:300px;height:300px;
  border-radius:9999px;background:rgba(147,51,234,.30);filter:blur(64px)}
.portal-screen{position:relative;display:flex;flex-direction:column;align-items:center;
  padding:28px 24px 40px;min-height:100dvh}
.portal-wordmark{display:flex;align-items:center;gap:10px}
/* The owner's lockup (2304×357, mark + wordmark, WHITE ink) replaced both the
   placeholder and the typed "MAX-VISITOR / Visitor Management" — owner
   2026-08-11: "ใช้ logo นี้แทนตำแหน่งข้อความทั้งหมด". Sized by HEIGHT so the
   6.45:1 ratio is never distorted; `max-width` keeps it inside a 390px phone
   with its 24px gutters.

   ⚠️ The ink is white, so this file only ever places it on a DARK surface —
   the brand field, the desktop aside, the harness header. A light-surface
   version does not exist yet (pending-work E-5); do not "fix" that by
   filtering or inverting the owner's artwork. */
.portal-logo{height:34px;width:auto;display:block}
/* In the header ROW the mark must yield: at 390px a 280px logo plus the
   language control plus the gutters is wider than the screen, and the whole
   page overflowed sideways — the picker left the viewport and the card was
   clipped. `min-width:0` is what lets a flex item shrink at all; without it
   the intrinsic image width wins and the row pushes the page open. */
.portal-topbar .portal-logo{height:30px;flex:0 1 auto;min-width:0;
  object-fit:contain;object-position:left center}
.portal-langpick{flex:0 0 auto}
@media (min-width:1024px){ .portal-aside .portal-logo{height:44px;max-width:340px} }

@keyframes portal-rise{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
.portal-card{animation:portal-rise 420ms cubic-bezier(.22,.9,.3,1) both;margin-top:20px;
  width:100%;max-width:360px;border-radius:16px;padding:20px;background:var(--page-bg);
  border:1px solid var(--border);box-shadow:0 18px 44px -14px rgba(2,6,23,.6)}
/* Removes the motion rather than shortening it — someone who asked for no
   movement did not ask for faster movement. */
@media (prefers-reduced-motion:reduce){.portal-card{animation:none}}
.portal-card h1{font-size:17px;font-weight:600;color:var(--text)}
.portal-card .sub{margin-top:4px;font-size:13px;line-height:1.6;color:var(--muted)}
.portal-card .body{margin-top:16px;display:flex;flex-direction:column;gap:22px}
.portal-card label{display:block;font-size:13px;color:var(--muted);margin-bottom:6px}
.portal-card input:not([type=checkbox]):not([type=radio]),
.portal-card textarea,.portal-card select{width:100%;height:48px;
  font-size:16px;padding:0 14px;border-radius:10px}
/* Belt and braces: the tick keeps its own size wherever it appears, and the
   label beside it takes the remaining width instead of a 40px strip. */
.consent input[type=checkbox]{width:26px;height:26px;min-height:26px;flex:none}
.consent label{flex:1;min-width:0}
.portal-card textarea{height:auto;min-height:84px;padding:12px 14px}
.portal-card .fld+.fld{margin-top:14px}

/* The numbers are wayfinding, not decoration: the form is longer than one
   phone screen, so they say how much is left. */
.grp{display:flex;flex-direction:column;gap:14px}
.grp-h{display:flex;flex-direction:column;gap:2px}
.grp-h h2{display:flex;align-items:center;gap:8px;font-size:15px;font-weight:600;color:var(--text)}
.grp-h .n{flex:none;width:22px;height:22px;border-radius:999px;background:var(--primary-50);
  color:var(--primary);font-size:12px;font-weight:600;display:flex;align-items:center;
  justify-content:center;font-variant-numeric:tabular-nums}
.grp-h p{font-size:12px;color:var(--faint)}

.segbar{display:flex;border:1px solid var(--input-border);border-radius:10px;overflow:hidden}
.segbar button{flex:1;height:48px;border:0;border-radius:0;background:var(--page-bg);
  color:var(--muted);font:inherit;font-size:16px;font-weight:500}
.segbar button+button{border-left:1px solid var(--input-border)}
.segbar button.on{background:var(--primary-50);color:var(--primary);font-weight:600}
.segbar button:hover{background:var(--section-bg)}
.segbar button.on:hover{background:var(--primary-50)}

/* The header row: mark on the LEFT, language on the RIGHT (owner 2026-08-11).
   The language control is a SELECT, not a pair of pills — DEC-VM-2026-08-08-11
   plans Chinese and Japanese, and a row of pills stops being a row at four.
   A select also keeps "add a language" down to adding an <option>, which is
   the data-only extension that decision requires. */
.portal-topbar{display:flex;align-items:center;justify-content:space-between;gap:12px;
  padding:14px 20px 0;position:relative;z-index:1}
/* Flag-only trigger: one fixed size no matter how many languages exist or
   how long their names are. The panel still says the name — a flag alone is
   not a label, it is a hint. */
.portal-langpick{width:auto;flex:none}
.portal-langpick .v2-select-trigger{min-width:72px;justify-content:center;gap:6px;
  font-size:20px;line-height:1;padding:0 10px}
.portal-langpick .v2-select-panel{font-size:15px}
.portal-langpick .v2-select-option{font-size:15px}
/* On the dark field the kit's default trigger (white box, grey border) reads
   as a hole punched in the brand panel. */
.portal-topbar .v2-select-trigger{background:rgba(255,255,255,.08);
  border-color:rgba(255,255,255,.25);color:#fff;height:44px}
.portal-topbar .v2-select-trigger:hover{background:rgba(255,255,255,.16);
  border-color:rgba(255,255,255,.45)}
.portal-topbar .v2-select-trigger .chev{color:#C7D2E5}
/* #7E8FB2 is MEASURED (5.50:1 on the field's darkest stop), not chosen by
   eye — their comment records the first attempt failing AA. Re-measure if
   either gradient end moves. */
.portal-trust{margin-top:22px;font-size:11px;letter-spacing:.02em;color:#7E8FB2;text-align:center}

/* A scrim, never a white bar: a white bar under a floating card cuts the card
   off and gives the screen two competing surfaces. */
.portal-actions{position:sticky;bottom:0;margin-top:24px;padding:32px 24px max(16px,env(safe-area-inset-bottom));
  background:linear-gradient(to top,#060C18 0%,#060C18 55%,transparent 100%);
  width:100%;display:flex;justify-content:center}
.portal-actions button{width:100%;max-width:360px;height:52px;font-size:16px;font-weight:600;
  border-radius:12px;background:var(--primary);color:#fff;border:0}
.portal-actions button:hover{background:var(--primary-600);border-color:var(--primary-600)}
/* Any button inside a guest card clears the same 44px floor. */
.portal-card button:not(.segbar button){min-height:44px}
.portal-check{width:56px;height:56px;border-radius:999px;background:var(--success-bg);
  display:flex;align-items:center;justify-content:center;margin:0 auto 12px;
  color:var(--success-text);font-size:26px}

/* ---------- guest surfaces on tablet + desktop (owner 2026-08-10) ----------
   max-hrm could not answer this for us: their employee portal is phone-ONLY
   by product definition, so PortalShell has no desktop composition at all.
   Our guests genuinely arrive both ways — the QR on the door is a phone, the
   invite email is usually a desktop — and a 360px card centred in a wide dark
   field reads as an unfinished page.

   So the phone layout stays exactly as approved and a desktop one is ADDED,
   traced from their ADMIN LOGIN (login/page.tsx, read-only): left brand
   panel, right white form column capped at 540px, language switcher top
   right, panel hidden below 1024px. Their breakpoint, not a guess. */

/* The aside carries its own copy of the brand field, so the fixed field can
   step aside on desktop instead of showing through the white column. */
.portal-aside{display:none;position:relative;overflow:hidden;padding:44px 40px;
  color:#fff;align-items:center;
  background:linear-gradient(135deg,#060C18 0%,#0A1128 55%,#0C1535 100%)}
.portal-aside::before{content:"";position:absolute;inset:0;
  background-image:linear-gradient(to right,rgba(124,58,237,.18) 1px,transparent 1px),
                   linear-gradient(to bottom,rgba(124,58,237,.18) 1px,transparent 1px);
  background-size:40px 40px;animation:pulse-grid 4s ease-in-out infinite}
/* Their admin login's two drifting orbs and the scan line, at their own values
   (login/page.tsx, read-only): 500px violet at 25%/25% and 420px cyan bottom
   right, both blur(72px) on a 14s drift with the second offset -7s so they
   never move in step; the scan line is a 1px cyan gradient crossing every 6s.
   Owner 2026-08-10: "animation และ ภาพด้านซ้าย ต้องแบ่ง 50:50 … เหมือน max-hrm".

   ⚠️ DESKTOP ONLY. The phone shell keeps the static field on purpose — the
   max-hrm room dropped this motion on phones for battery and jank, and a
   340px blur repainting behind an opaque card is the most expensive thing on
   the screen. Inheriting the look without that decision inherits the problem
   they already solved. */
.portal-aside .orb{position:absolute;border-radius:9999px;pointer-events:none;filter:blur(72px)}
.portal-aside .orb.a{top:25%;left:25%;width:500px;height:500px;background:rgba(147,51,234,.30);
  animation:portal-drift 14s ease-in-out infinite}
.portal-aside .orb.b{bottom:0;right:0;width:420px;height:420px;background:rgba(6,182,212,.25);
  animation:portal-drift 14s ease-in-out infinite;animation-delay:-7s}
.portal-aside .scanline{position:absolute;left:0;right:0;height:1px;pointer-events:none;
  background:linear-gradient(90deg,transparent,rgba(34,211,238,.6),transparent);
  box-shadow:0 0 20px rgba(34,211,238,.5);animation:portal-scan 6s linear infinite}
@keyframes pulse-grid{0%,100%{opacity:.55}50%{opacity:.95}}
@keyframes portal-drift{0%,100%{transform:translate(0,0)}50%{transform:translate(20px,-20px)}}
@keyframes portal-scan{0%{top:-2%}100%{top:102%}}
/* Someone who asked for no movement did not ask for slower movement. */
@media (prefers-reduced-motion:reduce){
  .portal-aside::before,.portal-aside .orb,.portal-aside .scanline{animation:none}
  .portal-aside .scanline{display:none}
}
/* Their login stacks the panel the same way: the mark sits at the TOP
   (p-12 pb-0), the message is vertically centred, and the quiet line rests at
   the bottom — not all three bunched in the middle, which is what mine did. */
.portal-aside .inner{position:relative;z-index:1;display:flex;flex-direction:column;
  gap:26px;width:100%;max-width:460px}
@media (min-width:1024px){
  .portal-aside{align-items:stretch}
  .portal-aside .inner{height:100%;justify-content:flex-start}
  .portal-aside .inner > h2{margin-top:auto}
  .portal-aside .inner > .trust{margin-top:auto;margin-bottom:0}
}
/* `pre-line` so the newline inside the translated headline renders as the
   two-line break the approved mockup shows. textContent preserves \n; without
   this the browser collapses it and the copy flattens (Codex b9-responsive
   FAIL). innerHTML would also "fix" it — and hand every translation string a
   script tag, which is not a trade this file makes. */
.portal-aside h2{font-size:30px;font-weight:700;line-height:1.35;color:#fff;
  white-space:pre-line}
.portal-steps{list-style:none;display:flex;flex-direction:column;gap:14px}
.portal-steps li{display:flex;gap:12px;align-items:flex-start;color:#C7D2E5;font-size:15px;line-height:1.6}
.portal-steps b{flex:none;width:26px;height:26px;border-radius:999px;background:rgba(255,255,255,.12);
  color:#fff;font-size:13px;display:flex;align-items:center;justify-content:center}
.portal-aside .trust{font-size:12px;color:#7E8FB2;line-height:1.7}
.portal-main{display:flex;flex-direction:column;min-height:100dvh;min-width:0}

/* Paired fields — only ever fields that are ONE thought (given/family name,
   phone/email). A two-column form that pairs unrelated fields is harder to
   read than one column, not easier. Single column on a phone: a thumb travels
   in one direction. */
.two-up{display:grid;grid-template-columns:1fr;gap:0}
.two-up .fld+.fld{margin-top:14px}

@media (min-width:768px){
  .portal-card{max-width:560px}
  .portal-screen{padding:36px 32px 40px}
  .two-up{grid-template-columns:1fr 1fr;gap:14px}
  .two-up .fld+.fld{margin-top:0}
}

@media (min-width:1024px){
  /* The page stops scrolling and the RIGHT COLUMN scrolls instead, so the
     brand panel stays put — it is chrome, not content (owner 2026-08-10:
     "ฝั่งซ้าย ต้อง fix อยู่กับที่สิ"). Their login does the same; mine had both
     columns in the page flow, so the panel slid away with the form. */
  .portal{display:flex;height:100dvh;overflow:hidden}
  .portal-field{display:none}          /* the aside is the brand field now */
  /* 50:50 — their `lg:w-1/2`, not the 44% I guessed (owner 2026-08-10). */
  .portal-aside{display:flex;width:50%;flex:none;height:100dvh;padding:48px}
  /* The right column is a THREE-part column: top bar · the part that scrolls ·
     the action bar. Previously the whole column scrolled and the bar was
     `sticky`, which left it floating mid-form with fields showing beneath it
     (owner's screenshot). A flex column cannot do that: the middle is the only
     scrollport, so the footer is always the footer. */
  .portal-main{flex:1;background:var(--page-bg);height:100dvh;overflow:hidden;
    display:flex;flex-direction:column}
  .portal-topbar{flex:none}
  .portal-screen{flex:1;min-height:0;overflow-y:auto;justify-content:flex-start;
    padding:8px 40px 32px}
  /* On the white column the card stops being a card — the page IS the card,
     exactly as their login's right panel behaves. `keep-card` opts back in
     for surfaces that are objects rather than forms: a pass is a ticket, and
     a confirmation is a statement. */
  .portal-card{margin-top:0;max-width:540px;box-shadow:none;border:0;padding:0;background:none;
    animation:none}
  .portal-card.keep-card{border:1px solid var(--border);box-shadow:var(--shadow-card);
    padding:24px;background:var(--page-bg);border-radius:16px;margin-top:8px}
  .portal-card h1{font-size:22px}
  /* The wordmark lives in the aside on this size — repeating it above the
     form would say the product's name twice on one screen. */
  .portal-main .portal-wordmark{display:none}
  .portal-topbar{padding:18px 24px;justify-content:flex-end}
  .portal-topbar .portal-logo{display:none}   /* the aside already shows it */
  .portal-topbar .v2-select-trigger{background:var(--page-bg);
    border-color:var(--input-border);color:var(--body)}
  .portal-topbar .v2-select-trigger:hover{background:var(--section-bg);border-color:var(--ph)}
  .portal-topbar .v2-select-trigger .chev{color:var(--faint)}
  .portal-trust{display:none}          /* the aside carries it */
  /* A scrim over white would be a grey smear; on this column the bar is a
     plain white ledge with a hairline, and the button stops being full-bleed. */
  .portal-actions{position:static;flex:none;margin-top:0;background:var(--page-bg);
    border-top:1px solid var(--border);justify-content:flex-end;padding:16px 40px}
  .portal-actions button{max-width:220px}
}

.phone .top,.guest-top{background:var(--rail);color:#fff;padding:12px 16px;display:flex;
        justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap}
.guest-top .dev{background:var(--warning-bg);color:var(--warning-text);font-size:11px;
        padding:2px 8px;border-radius:999px;font-weight:700}
/* DEC-11 item 2: the guest picks their own language on every guest surface */
.lang{display:flex;gap:4px}
.lang button{height:30px;padding:0 12px;font-size:13px;border-color:#64748B;
        background:transparent;color:#CBD5E1}
.lang button:hover{background:rgba(255,255,255,.12);border-color:#fff;color:#fff}
.lang button.on{background:#fff;color:var(--rail);border-color:#fff;font-weight:700}
/* PDPA consent block — never a bare tick, always the text beside it */
.consent{display:flex;gap:12px;align-items:flex-start;margin-top:14px;
        background:var(--section-bg);border:1px solid var(--border);
        border-radius:var(--r-md);padding:12px;font-size:14px}
.terms{white-space:pre-wrap;background:var(--warning-bg);border:1px solid #FDE68A;
        color:var(--warning-text);border-radius:var(--r-md);padding:10px 12px;
        font-size:13px;margin-top:10px}

/* the pass/QR surface */
.qr-box,.qr{display:flex;align-items:center;justify-content:center;background:var(--section-bg);
        border:2px dashed var(--input-border);border-radius:var(--r-lg);padding:16px;
        margin:16px auto 6px;color:var(--ph);font-size:13px;text-align:center}
.code-big,.code{font-size:38px;font-weight:800;letter-spacing:.06em;text-align:center;
          font-variant-numeric:tabular-nums;color:var(--text);margin:8px 0}
.pass,.dead{background:var(--page-bg);border-radius:16px;padding:22px;text-align:center;
          box-shadow:var(--shadow-card)}
.ok-box{background:var(--success-bg);border:1px solid #A7F3D0;color:var(--success-text);
          border-radius:var(--r-md);padding:16px;font-size:15px;text-align:center}
.error{background:var(--danger-bg);color:var(--danger-text);border-radius:var(--r-md);
          padding:10px 12px;margin-top:10px;font-size:14px}

/* ============================================================================
   APP SHELL — the staff screens' frame (C-2b).

   COPIED, not designed: every rule below is lifted verbatim from
   docs/mockups/v2-shared.css, which is what the owner approved in mockups
   01/02/04/07 and again in 13. The kit carried the `--rail` COLOUR and none
   of the layout, so the first staff screen would otherwise have had to invent
   a shell — and an invented shell is how the devhost pages drifted away from
   the mockups in the first place (B-6's origin).

   Scoping matters here. `.m`, `.i`, `.t`, `.co` are one and two letters long
   and only ever apply inside `.rail`, `.secnav` or `.apphead`. Left
   unscoped they would collide with anything, so they stay written the way the
   mockups write them.
   ============================================================================ */

.ic{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2;
    stroke-linecap:round;stroke-linejoin:round;flex:none}
.ic.sm{width:15px;height:15px}.ic.lg{width:22px;height:22px}
.btn-outline{background:var(--page-bg);color:var(--body);border-color:var(--input-border)}
.btn-outline:hover{background:var(--section-bg)}
.btn-danger-soft{background:var(--page-bg);color:var(--danger-text);border-color:#FECACA}
.iconbtn{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;
         border-radius:8px;border:1px solid var(--input-border);background:#fff;color:var(--muted);cursor:pointer}
.b-in{background:var(--success-bg);color:var(--success-text);border-color:#A7F3D0}
.b-out{background:var(--st-all-bg);color:var(--st-all);border-color:var(--st-all-border)}
/* `min-height` is the ONE value not copied from the mockup. There it is
   820px, because a drawing sits inside a framed preview; in a real window
   that leaves the rail stopping partway down with bare page under it. The
   layout is the approved one; the height is what a browser needs. */
/* `minmax(0,1fr)`, NOT `1fr`. A bare `1fr` track is `minmax(auto,1fr)`, and
   `auto` resolves to the column's MIN-CONTENT width — so a table declaring
   `min-width:720px` inside an `overflow-x:auto` wrapper pushes the whole
   column to 720px instead of scrolling inside it. The page then grows past
   the viewport, and under Chrome's mobile emulation the viewport grows to
   match, which is why a "390px" run measured 650. */
.shell{display:grid;grid-template-columns:64px 240px 1fr;min-height:100dvh}
.rail{background:var(--rail);display:flex;flex-direction:column;align-items:center;padding:12px 0;gap:6px}
.rail .logo{width:38px;height:38px;border-radius:9px;background:var(--primary);color:#fff;display:flex;
            align-items:center;justify-content:center;font-weight:700;font-size:15px;margin-bottom:10px}
.rail .m{width:40px;height:40px;border-radius:9px;display:flex;align-items:center;justify-content:center;
         color:#8A99AF;cursor:pointer}
.rail .m:hover{background:#24303F}
.rail .m.on{background:var(--primary);color:#fff}
.rail .sp{flex:1}
.secnav{background:var(--page-bg);border-right:1px solid var(--border);padding:14px 0}
.secnav .t{font-size:12px;font-weight:600;color:var(--faint);padding:4px 18px 8px;letter-spacing:.02em}
/* `text-decoration:none` because these are real <a> elements (shell.js): a
   nav item must survive middle-click and "open in new tab", which a div with
   a click handler cannot, and the underline is the only thing the anchor
   brings that the mockup does not draw. */
.secnav .i{display:flex;align-items:center;gap:10px;padding:10px 18px;font-size:15px;font-weight:500;
           color:var(--muted);cursor:pointer;border-right:2px solid transparent;text-decoration:none}
.secnav .i:hover{background:var(--section-bg)}
.secnav .i.on{border-right-color:var(--primary);background:var(--primary-50);color:var(--primary)}
.secnav .i .cnt{margin-left:auto;font-size:12px;font-weight:600;background:var(--warning-bg);
                color:var(--warning-text);border-radius:999px;padding:1px 8px}
.secnav .i.lock{opacity:.55;cursor:not-allowed}
.contentcol{display:flex;flex-direction:column;min-width:0}
.apphead{height:64px;background:var(--page-bg);border-bottom:1px solid var(--border);display:flex;
         align-items:center;gap:14px;padding:0 20px}
.apphead .co{display:flex;align-items:center;gap:8px;font-weight:500;border:1px solid var(--border);
             border-radius:8px;padding:7px 12px;font-size:15px}
.apphead .search{flex:1;max-width:420px;height:38px;border:1px solid var(--input-border);border-radius:8px;
                 display:flex;align-items:center;gap:8px;padding:0 12px;color:var(--ph);font-size:15px}
.apphead .sp{flex:1}
.pagebody{flex:1;background:var(--app-bg);display:flex;flex-direction:column;min-width:0}
.pagehead{display:flex;align-items:flex-start;gap:16px;padding:20px 24px 14px;background:var(--page-bg);border-bottom:1px solid var(--border)}
.pagehead .sub{font-size:12px;color:var(--faint);margin-top:3px}
.pagehead .sp{flex:1}
.toolbar{display:flex;gap:10px;align-items:center;padding:12px 24px;background:var(--page-bg);border-bottom:1px solid var(--border);flex-wrap:wrap}
.toolbar .grow{flex:1}
/* ⚠️ A toolbar's controls sit SIDE BY SIDE, and the kit gives every input
   `width:100%` (line ~183) — right for a field stacked in a form, and it puts
   one filter per line in a bar. Same family as the `main{max-width}` rule that
   shrink-wrapped a grid child in C-1: a sensible global meeting a layout it
   was not written for.

   Sizes are mockup 12 ②'s own (`.search` min-width 240px, `.sel` auto, both
   36px tall), so the filter bar matches the drawing rather than the form. */
.toolbar input[type=search],.toolbar input[type=text],.toolbar input[type=date],
.toolbar select{
  width:auto;height:36px;font-size:14px}
/* 300px, not 260: the visits list's placeholder names three things it
   searches ("ค้นหาเลขนัด / บริษัท / ชื่อพนักงานที่ถูกพบ") and 260px cut it
   mid-word — a placeholder that has to be guessed at is a placeholder that
   did not do its job. Mockup 02's own search box is 300px. */
.toolbar input[type=search],.toolbar input[type=text]{flex:0 1 300px;min-width:0}
/* The date box was added to the list above after a render showed it taking a
   line of its own: `input[type=date]` is not `[type=text]`, so the global
   `width:100%` still applied to it and only to it. */
.toolbar input[type=date]{flex:0 0 auto}
.toolbar select{min-width:150px}
.tbl{width:100%;border-collapse:collapse;background:var(--page-bg)}
.tbl th{font-size:15px;font-weight:600;text-align:left;color:var(--muted);background:var(--section-bg);
        padding:10px 16px;border-bottom:1px solid var(--border);white-space:nowrap}
.tbl td{padding:12px 16px;border-bottom:1px solid var(--border-soft);vertical-align:middle}
.tbl tr:hover td{background:var(--section-bg)}

/* ---------- what mockup 13 added on top of the approved shell ---------- */

/* Below 1024px the section nav's label moves into the page head, because the
   shell's 64+240 columns left the table ~530px at 834px and the row actions
   were CLIPPED — the buttons a person came to press were the part that fell
   off. Found by rendering mockup 13 and looking. */
.seclabel{display:none;font-size:12px;font-weight:600;color:var(--faint);
          letter-spacing:.02em;margin-bottom:2px}
/* Wide content scrolls inside its own container, never the page (CLAUDE §15).
   Load-bearing partner: `min-width:0` on `.contentcol` above — without it the
   grid track takes the table's min-content width and the whole page grows
   instead of this box scrolling. Measured at 390px: a 760px table inside a
   286px scroller, page scroll 0. */
.tablewrap{overflow-x:auto}

@media (max-width:1023px){
  /* THE SECTION NAV BECOMES A HORIZONTAL SCROLLING STRIP under the app head —
     owner decision 4, 2026-08-15 (`J-2026-08-15-25` item 4). Deliberately not
     a hamburger: every destination stays visible without a tap, and this
     product has few enough of them that they fit on one line.

     C-2b folded the nav away entirely, which cost nothing while it held ONE
     item and was recorded as a debt for whichever batch added a second. C-2c-b
     adds two, and this is that debt paid.

     The DOM is unchanged — only the grid is. The rail keeps column 1 across
     both rows; the nav takes row 1 of column 2 and the content takes row 2.
     Restructuring in CSS rather than moving the element means one markup for
     every width, so a screen cannot be right at 1440 and wrong at 390 because
     somebody edited one copy of the nav. */
  .shell{grid-template-columns:64px 1fr;grid-template-rows:auto 1fr;min-height:100dvh}
  .rail{grid-column:1;grid-row:1 / 3}
  .secnav{grid-column:2;grid-row:1;display:flex;gap:2px;padding:0 8px;
          border-right:0;border-bottom:1px solid var(--border);
          /* Scrolls INSIDE itself. The page must never scroll sideways
             (CLAUDE §15), and `contain` stops a swipe that runs out of strip
             from dragging the page behind it. */
          overflow-x:auto;overscroll-behavior-x:contain}
  .contentcol{grid-column:2;grid-row:2}
  /* The group heading is redundant once the items sit on one line, and it
     already reappears above the page title as `.seclabel`. */
  .secnav .t{display:none}
  .secnav .i{white-space:nowrap;padding:12px 14px;border-right:0;
             border-bottom:2px solid transparent}
  /* The active marker turns 90° with the strip: the base rule paints the
     right edge, which on a horizontal strip would underline nothing and
     stripe the item after it instead. */
  .secnav .i.on{border-right-color:transparent;border-bottom-color:var(--primary)}
  .seclabel{display:block}

  /* ---- what RENDERING at 390px showed, and no assertion could ----------
     Every text check passed on a page whose head was visibly broken: the
     tenant chip wrapped onto two lines, the account block was crushed into
     three narrow columns, and "⬆ นำเข้าจากไฟล์" came out one word per line —
     the same shape as the mockup-12 defect whose fix is commented in that
     file, arriving in a different element. CLAUDE §15 is not a formality.

     The head is a fixed 64px ROW at desktop; on a phone it has to be allowed
     to become as tall as its contents, and the contents have to stop being
     asked to fit on one line. */
  .apphead{height:auto;min-height:56px;flex-wrap:wrap;gap:10px;padding:10px 14px}
  .apphead .co{font-size:13px;padding:5px 10px;white-space:nowrap}
  /* The ROLE goes, the NAME stays. Two pieces of identity is one more than a
     phone has room for, and the name is the one that answers "am I signed in
     as the right person". */
  .apphead .meta{display:none}

  /* The title owns the first line; the actions flow onto the next as a row.
     `nowrap` on the controls is the actual repair for the one-word-per-line
     break — without it a flex item happily shrinks to its longest syllable. */
  .pagehead{flex-wrap:wrap;padding:16px 16px 12px;gap:12px}
  .pagehead .sp{display:none}
  .pagehead > div:first-child{flex:1 1 100%}
  .pagehead > a,.pagehead > button{white-space:nowrap}
  .toolbar{padding:12px 16px}
}

/* ---------- dialog: the two moments this screen has to get right ---------- */

.scrim{position:fixed;inset:0;background:rgba(2,6,23,.55);display:grid;
       place-items:center;padding:20px;z-index:70}
/* `margin:auto` is a REPAIR, not a preference. A modal <dialog> is centred by
   the browser's own `dialog{margin:auto}` — which this kit's `*{margin:0}`
   reset silently overrode, so every modal opened flush against the top-left
   corner. The reset is right and the UA rule is right; the collision is what
   needed saying.

   The backdrop is stated too: the default is nearly invisible, and a modal
   that does not dim what is behind it does not read as modal. */
.dlg{width:min(440px,100%);margin:auto;background:var(--page-bg);
     border:1px solid var(--border);border-radius:var(--r-lg);
     box-shadow:var(--shadow-pop);overflow:hidden;
     max-height:calc(100dvh - 40px);display:flex;flex-direction:column}
.dlg::backdrop{background:rgba(2,6,23,.55)}
.dlg .dh{padding:16px 20px;border-bottom:1px solid var(--border);font-weight:600}
.dlg .db{padding:20px;overflow:auto}
.dlg .df{padding:14px 20px;border-top:1px solid var(--border);display:flex;
         justify-content:flex-end;gap:10px;background:var(--section-bg)}

/* The generated password: big, monospaced, selectable. Somebody is copying
   this by eye onto a phone or a sticky note, and whether 0 reads as O decides
   whether they can sign in. */
.pw{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:22px;
    letter-spacing:.06em;background:var(--section-bg);border:1px dashed var(--input-border);
    border-radius:10px;padding:14px 16px;text-align:center;user-select:all;
    word-break:break-all}
.ckline{display:flex;gap:10px;align-items:flex-start;margin-top:14px;font-size:14px}
.ckline input{width:20px;height:20px;flex:none;margin-top:2px}

/* ---------- C-3b: status tabs, the list footer, and the right drawer ----------
   All three come from approved mockup 02/03 and are product vocabulary rather
   than one screen's private styling, so they live in the kit beside the shell
   they sit inside. */

.tabs{display:flex;gap:4px;padding:0 20px;border-bottom:1px solid var(--border);
      overflow-x:auto;scrollbar-width:thin}
.tabs .tab{appearance:none;background:none;border:0;border-bottom:2px solid transparent;
           padding:12px 14px;font:inherit;font-size:14px;color:var(--muted);
           cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:7px}
.tabs .tab:hover{color:var(--text)}
.tabs .tab.on{color:var(--primary);border-bottom-color:var(--primary);font-weight:600}
/* The count rides in a pill so a four-digit day does not shove the label. */
.tabs .tab .n{background:var(--section-bg);border-radius:999px;padding:1px 7px;
              font-size:12px;font-variant-numeric:tabular-nums}
.tabs .tab.on .n{background:var(--primary);color:#fff}

.footbar{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:12px 20px;
         border-top:1px solid var(--border);background:var(--section-bg);font-size:13px}
.footbar .stat{display:flex;align-items:center;gap:6px;color:var(--muted)}
.footbar .stat b{color:var(--text);font-variant-numeric:tabular-nums}

/* Two lines in one cell: a name over the thing that identifies it. Mockup 02
   uses it in four of its seven columns. */
.cell-2l .a{font-weight:500}
.cell-2l .b{font-size:12px;color:var(--muted);margin-top:2px}

/* The drawer (mockup 03): a <dialog> pinned to the right edge rather than
   centred, so `.dlg`'s `margin:auto` is deliberately overridden here — the
   same collision the comment above describes, resolved the other way because
   this one is meant to be off-centre. */
.drawer{width:min(480px,100%);max-width:100%;height:100dvh;max-height:100dvh;
        margin:0 0 0 auto;border-radius:0;border-right:0;
        display:flex;flex-direction:column}
.drawer .dh{display:flex;gap:12px;align-items:flex-start}
.drawer .db{flex:1}
/* On a phone the drawer IS the screen; a 480px panel with 40px of scrim
   beside it is neither one thing nor the other. */
@media (max-width:560px){.drawer{width:100%}}

/* A label/value pair, the drawer's whole vocabulary for showing a visit. */
.kv{display:flex;gap:10px;padding:5px 0;font-size:14px}
.kv b{flex:none;width:116px;color:var(--muted);font-weight:400}

/* Visually hidden, still read aloud. The users table's action column has no
   visible heading — a blank <th> is fine to look at and silent to a screen
   reader, which then announces an unnamed column. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
         clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* ---------- hiding must actually hide ----------
   `[hidden]` and a closed `<dialog>` are hidden by a UA rule of the LOWEST
   specificity, so any component rule that sets `display` beats them. This kit
   has several — `.alert{display:flex}` and `.dlg{display:flex}` among them —
   and the users screen shipped an empty red error bar plus a stray dialog
   sitting in the page flow because of it.

   The sign-in page never showed the fault: it carried its own
   `[hidden]{display:none !important}` in a page-level <style>. So the rule
   existed, privately, in the one file that happened to need it first — which
   is precisely how a design system ends up with a bug every new screen
   rediscovers.

   `!important` is deliberate and narrow: the whole point is to outrank
   component display rules, and there is no case where an element is both
   hidden and meant to be seen. */
[hidden]{display:none !important}
dialog:not([open]){display:none !important}
